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
| Need | Use |
|---|---|
| CRUD on a known datatype | Repository CRUD |
| Build complex filters from a UI form | Dynamic Query (find, count) |
| Aggregation pipeline — group, sum, bucket | Dynamic Query (aggregate) |
| Bulk read/write across many collections | Dynamic 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).
/dynamic-query/findJWT+APIKEY/dynamic-query/findOneJWT+APIKEY/dynamic-query/aggregateJWT+APIKEY/dynamic-query/countJWT+APIKEY/dynamic-query/distinctJWT+APIKEY/dynamic-query/insertOneJWT+APIKEY/dynamic-query/insertManyJWT+APIKEY/dynamic-query/updateOneJWT+APIKEY/dynamic-query/updateManyJWT+APIKEY/dynamic-query/replaceOneJWT+APIKEY/dynamic-query/deleteOneJWT+APIKEY/dynamic-query/deleteManyJWT+APIKEY/dynamic-query/bulkWriteJWT+APIKEYLegacy /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:
- The wrapper still applies. To set a field, use
data.<field>:{ $set: { 'data.status': 'closed' } }. - 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 addpk: orgIdto your filter — it's injected. - For large
findresults, pass{ limit, skip }and paginate. There is no cursor support over HTTP. aggregatepipelines that scan a whole collection should be reserved for nightly jobs; for live UIs, narrow with$matchfirst.- 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.