Documentation

Dynamic queries

A direct MongoDB-shaped query surface for filtering, segmentation, and rule evaluation.

The Dynamic Query module is a thin proxy over the MongoDB driver. Where the repository controller takes opinionated { query, options } envelopes, dynamic-query mirrors the driver verb-for-verb: find, findOne, aggregate, count, distinct, insertOne, updateOne, deleteOne, bulkWrite. Audience segmentation, the rule engine, and the automation builder all sit on top of this.

When to use which

NeedUse
CRUD on a known datatypeRepository CRUD
Build complex filters from a UI formDynamic Query (find, count)
Aggregation pipeline — group, sum, bucketDynamic Query (aggregate)
Bulk read/write across many collectionsDynamic Query (bulkWrite)

Both surfaces enforce tenancy via orgid. Both honor the BaseModel\<T\> envelope — data.fieldName is where your fields live.

Endpoints

The controller is mounted at both /dynamic-query/* and /mongodb/* (alias). All write paths require @RequirePermissions(create|update|delete).

POST/dynamic-query/findJWT+APIKEY
POST/dynamic-query/findOneJWT+APIKEY
POST/dynamic-query/aggregateJWT+APIKEY
POST/dynamic-query/countJWT+APIKEY
POST/dynamic-query/distinctJWT+APIKEY
POST/dynamic-query/insertOneJWT+APIKEY
POST/dynamic-query/insertManyJWT+APIKEY
POST/dynamic-query/updateOneJWT+APIKEY
POST/dynamic-query/updateManyJWT+APIKEY
POST/dynamic-query/replaceOneJWT+APIKEY
POST/dynamic-query/deleteOneJWT+APIKEY
POST/dynamic-query/deleteManyJWT+APIKEY
POST/dynamic-query/bulkWriteJWT+APIKEY

Legacy /dynamic-query/query, /dynamic-query/update, /dynamic-query/delete are still available and route through the older handleDynamicQuery path — prefer the named verbs above.

Request shape

Every body has the same skeleton:

// confirm shape against AppEngine: src/dynamic-query/dynamic-query.interface.ts
interface FindRequest {
  collection: string;     // datatype, e.g. "product", "contact", "lead"
  filter?: Document;      // MongoDB filter
  options?: {
    sort?: Document;      // e.g. { 'data.createdAt': -1 }
    limit?: number;
    skip?: number;
    projection?: Document;
  };
}

Other verbs swap filter for update, replacement, documents, pipeline, etc. — see the interface file for the exact union.

find

const res = await fetch('https://appengine.appmint.io/dynamic-query/find', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'orgid': orgId,
    'Authorization': `Bearer ${jwt}`,
  },
  body: JSON.stringify({
    collection: 'lead',
    filter: {
      'data.status': 'qualified',
      'data.score': { $gte: 70 },
    },
    options: {
      sort: { 'data.score': -1 },
      limit: 50,
    },
  }),
}).then(r => r.json());

aggregate

For grouping, bucketing, and lookups. Pass a standard MongoDB pipeline:

curl -X POST https://appengine.appmint.io/dynamic-query/aggregate \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "collection":"order",
    "pipeline":[
      {"$match":{"data.status":"paid"}},
      {"$group":{"_id":"$data.customerId","total":{"$sum":"$data.amount"}}},
      {"$sort":{"total":-1}},
      {"$limit":20}
    ]
  }'

count and distinct

# count
curl -X POST https://appengine.appmint.io/dynamic-query/count \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"collection":"contact","filter":{"data.tags":"vip"}}'

# distinct
curl -X POST https://appengine.appmint.io/dynamic-query/distinct \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"collection":"contact","field":"data.country","filter":{"data.status":"active"}}'

Writes

updateOne, updateMany, replaceOne, insertOne, insertMany, deleteOne, deleteMany, and bulkWrite all follow the MongoDB driver shape. Mind two differences:

  1. The wrapper still applies. To set a field, use data.<field>: { $set: { 'data.status': 'closed' } }.
  2. Direct writes through this controller skip domain-controller business logic (pricing recalc, inventory hold, etc.). Use a domain controller when one exists.
curl -X POST https://appengine.appmint.io/dynamic-query/updateMany \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "collection":"contact",
    "filter":{"data.tags":"newsletter-2024"},
    "update":{"$set":{"data.tags":"newsletter-2025"}}
  }'

Bulk operations

bulkWrite packages a mix of operations into one round-trip. Each entry is a standard MongoDB bulk operation descriptor:

{
  "collection": "product",
  "operations": [
    { "insertOne": { "document": { "data": { "sku": "NEW-1" } } } },
    { "updateOne": {
      "filter": { "data.sku": "OLD-1" },
      "update": { "$set": { "data.status": "discontinued" } }
    } },
    { "deleteOne": { "filter": { "data.sku": "JUNK-1" } } }
  ],
  "options": { "ordered": false }
}

Building filters in a UI

The audience builder, segment editor, and rule conditions all serialize their UI state as a JSON filter document and POST it to /dynamic-query/find (or /count for "estimated reach"). When you build a similar UI:

  • Keep operator names exactly as MongoDB writes them: $gte, $in, $regex.
  • Always prefix field paths with data. so they line up with the wrapper.
  • For nested arrays, use $elemMatch, not dot-paths past the array.
  • For "is set" / "is empty", use { $exists: true } and { $eq: null } respectively.

Performance notes

  • The repository service auto-scopes every query to the calling orgid. You don't need to add pk: orgId to your filter — it's injected.
  • For large find results, pass { limit, skip } and paginate. There is no cursor support over HTTP.
  • aggregate pipelines that scan a whole collection should be reserved for nightly jobs; for live UIs, narrow with $match first.
  • Indexes are pre-created on common data.* fields per datatype. If you add a custom collection and find queries slow, contact platform support to add an index.

For the operator catalog, see MongoDB operators.