Pipeline builder reference
Reference for the Prisma ORM MongoDB pipeline builder's stages, accumulators, expression helpers, and write methods.
The pipeline builder gives you a typed way to build MongoDB aggregation pipelines. You reach it through db.query, chain aggregation stages onto a starting collection, and finish the chain with the call that builds the query. It is the MongoDB counterpart to the SQL query builder: a lower-level API for the queries the ORM client doesn't express.
Everything the builder produces becomes a MongoDB aggregation pipeline. There is no find() or distinct() here. Use db.orm or rawCommand() for those. To fetch a single document, filter and then limit(1). Use the ORM client (db.orm) for everyday reads and writes across models and relations. Reach for the pipeline builder when you need to group documents, to join another collection and keep transforming the result, or to write the result into a collection ($out and $merge). This page documents every stage, accumulator, expression helper, and write method.
The pipeline builder targets MongoDB only. PostgreSQL has no pipeline builder: use the SQL query builder for PostgreSQL joins and aggregates, and the ORM client for everyday queries on either database.
Example schema
The examples run against the schema below, the same MongoDB schema as the ORM client page. Article and Tutorial are two kinds of Post, stored in one posts collection and told apart by the value of the kind field. @@base(Post, "article") and @@discriminator(kind) set that up. See @@base on the ORM client page. @@type("mongo/string@1") says the enum is stored as a MongoDB string, and @1 is the version of that type id, always @1 today. None of that matters to a pipeline: kind is an ordinary field you can filter and group on.
Expand for the example schema
enum UserRole {
@@type("mongo/string@1")
Admin = "admin"
Author = "author"
Reader = "reader"
}
type Address {
street String
city String
zip String?
country String
}
model User {
id ObjectId @id @map("_id")
name String
email String
bio String?
role UserRole
address Address?
posts Post[]
@@map("users")
}
model Post {
id ObjectId @id @map("_id")
title String
content String
kind String
authorId ObjectId
createdAt DateTime
author User @relation(fields: [authorId], references: [id])
@@discriminator(kind)
@@index([authorId])
@@index([createdAt(sort: Desc), authorId])
@@map("posts")
}
model Article {
summary String
@@base(Post, "article")
@@unique([summary])
}
model Tutorial {
difficulty String
duration Int
@@base(Post, "tutorial")
}Entry points
Every pipeline starts with db.query.from('posts'), naming the collection to aggregate over. It ends with a call that turns the chain into a query you can run. Between them you chain stages.
db is the client you create with mongo(...), whose dbName option is the name of the MongoDB database to use. See Transactions and runtime for every option and for how to create the client. Three Prisma ORM import paths appear on this page: @prisma/orm-mongo/runtime holds the mongo() client, @prisma/orm-mongo/query-builder holds mongoQuery, fn, acc, and expr, and @prisma/orm-mongo/query-ast/execution holds the Mongo*Stage classes, MongoAggFieldRef, and MongoFieldFilter. Every example holds the built query in a variable named plan.
Prisma ORM 8 renames schema.prisma to contract.prisma. Run npx prisma contract emit before any code on this page compiles: it writes contract.json, used at run time, and contract.d.ts, used for types. The example below imports contract.json with with { type: 'json' }, which works with the TypeScript settings prisma orm init writes for you. Coming from Prisma ORM 7 has the setup steps.
from()
Enter the pipeline builder on a collection.
Remarks
from()takes the collection name, the same name you use ondb.orm('posts','users'), not the model namePost. The collection name is the model's@@mapvalue, or the model name with a lowercase first letter when there is no@@map.- Passing an unknown collection name throws right away, with an error whose
codeisORM.MODEL_UNKNOWNand whose message isUnknown root: "<name>". Valid roots: ...(the message says root; it means the collection name). from()returns a builder you chain stages onto. CallinsertOne()andinsertMany()onfrom()directly, before any stage.- You can build the same pipelines without a client.
import { mongoQuery } from '@prisma/orm-mongo/query-builder', thenmongoQuery({ contractJson }).from('posts'). ImportcontractJsonthe same way the example below does.mongoQueryonly builds queries. It never runs one, so you still need a client'sruntimeto get results. Use it in tests, or to build a query once and run it through any client you like.
Options
| Name | Type | Required | Description |
|---|---|---|---|
collection | Collection name (string literal) | Yes | The collection to aggregate over. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts') | A builder you chain stages onto, then finish with build(). |
Examples
Enter the builder on a collection
import mongo from '@prisma/orm-mongo/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
const db = mongo<Contract>({ contractJson, url: process.env.MONGODB_URL, dbName: 'app' });
const runtime = await db.runtime();
const plan = db.query.from('posts').build();
const posts = await runtime.query(plan);Building and executing a pipeline
build() returns the built query, which the examples hold in a variable named plan. Nothing runs until you pass it to runtime.query(...). Every later example reuses the db and runtime from the example above.
Remarks
- On MongoDB,
db.runtime()returns a promise, so you must await it:const runtime = await db.runtime(). Hold it in a variable and reuse it. awaitthat call for an array of documents, or writefor await (const doc of runtime.query(plan)) { ... }to take one document at a time.- There is no
db.executeon the MongoDB client. - Read results are decoded. If every stage in your pipeline is one of
match(),sort(),limit(),skip(),sample(),project(),addFields(), andvectorSearch(),_idcomes back as a hex string. If any stage is not in that list, includinglookup()andgroup(), every document comes back raw, with each value as MongoDB stores it. An_idthat MongoDB generated is then anObjectIdobject from themongodbpackage, the MongoDB driver Prisma ORM uses. Compare those ids as strings, aslookup()shows. ADateTimefield comes back as a JavaScriptDateeither way.
Examples
Execute through the client
const plan = db.query.from('posts').sort({ createdAt: 1 }).build();
const posts = await runtime.query(plan);Pipeline stages
Stages transform the documents flowing through the pipeline. Chain them in order; each stage's output feeds the next. The (f) => ... callbacks reference the current document's fields, one property per field.
match()
Filter documents by a predicate.
Remarks
match()takes a callback whosefargument has one property per field, and returns a filter expression ((f) => f.kind.eq('tutorial')).- Scalar fields expose these filter operators:
eq,ne,gt,gte,lt,lte,in,nin,exists, andtype. They work like the MongoDB query operators of the same name ($ne,$gt,$in,$exists,$type, and so on). - To compare two computed values rather than a field against a constant, wrap the comparison in
expr(...):match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023)))).fnholds the helpers that build expressions, one per MongoDB operator.expr()wraps one of those values somatch()accepts it. Thefn.*helpers take expressions, not plain values, so wrap every constant infn.literal(). See Expression helpers.
Where each form goes. .node is the raw MongoDB expression inside an fn.* value:
| Call site | What you pass |
|---|---|
project(), addFields(), redact(), and the arguments of an accumulator | The fn.* value itself. |
The condition argument of fn.cond(), and the stages that take an options object | The .node property of the fn.* value. |
match() | The fn.* value wrapped in expr(...). |
Options
| Name | Type | Required | Description |
|---|---|---|---|
predicate | Callback (f) => FilterExpression | Yes | The condition documents must satisfy. |
Return type
| Return type | Example | Description |
|---|---|---|
| Filtered builder | db.query.from('posts').match(...) | A builder narrowed by the filter, chainable into more stages, write methods, or build(). |
Examples
Filter by a field
const plan = db.query
.from('posts')
.match((f) => f.kind.eq('tutorial'))
.build();
const tutorials = await runtime.query(plan);Aggregation-expression predicate
import { fn, expr } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2023))))
.build();
const recent = await runtime.query(plan);_id equality filters do not work through the pipeline builderFiltering by _id equality inside match() never matches any document. Neither a hex string (f._id.eq('507f...')) nor an ObjectId matches. There is no supported way to filter by _id equality through the typed pipeline builder. To fetch one document by its id, use the ORM client, which converts the hex string for you. The pipeline builder does not convert it:
const postId = '6650f1c2a1b2c3d4e5f60003';
const post = await db.orm.posts.where({ _id: postId }).first();For an _id filter inside a pipeline, use rawCommand(), which places a real ObjectId directly in a raw pipeline document.
sort()
Order documents by a field spec.
Remarks
sort()takes a plain object spec:{ field: 1 }ascending,{ field: -1 }descending. Multiple keys sort in the order they appear.
Options
| Name | Type | Required | Description |
|---|---|---|---|
spec | { [field]: 1 | -1 } | Yes | The sort key(s) and direction(s). |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').sort({ createdAt: -1 }) | A builder with an ordering applied. |
Examples
Sort descending
const plan = db.query.from('posts').sort({ createdAt: -1 }).build();
const newestFirst = await runtime.query(plan);limit()
Cap the number of documents.
Remarks
- Combine
sort()thenlimit(1)to fetch a single document: the pipeline builder has nofirst().
Options
| Name | Type | Required | Description |
|---|---|---|---|
count | number | Yes | Maximum number of documents to return. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').limit(1) | A builder limited to count documents. |
Examples
Fetch a single document
const plan = db.query.from('posts').sort({ createdAt: 1 }).limit(1).build();
const [oldest] = await runtime.query(plan);skip()
Offset into the sorted result set.
Options
| Name | Type | Required | Description |
|---|---|---|---|
count | number | Yes | Number of documents to skip. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').skip(1) | A builder offset by count documents. |
Examples
Paginate
const plan = db.query.from('posts').sort({ createdAt: 1 }).skip(1).build();
const afterFirst = await runtime.query(plan);sample()
Draw a random subset of documents.
Options
| Name | Type | Required | Description |
|---|---|---|---|
size | number | Yes | Number of documents to sample. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').sample(1) | A builder emitting a random subset. |
Examples
Random subset
const plan = db.query.from('posts').sample(1).build();
const oneRandom = await runtime.query(plan);addFields()
Compute new fields and attach them to each document.
Remarks
addFields()takes a callback returning an object of new field names to computed expression-helper values. Existing fields are preserved.
Options
| Name | Type | Required | Description |
|---|---|---|---|
spec | Callback (f) => ({ [field]: Expression }) | Yes | The new fields to compute. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').addFields(...) | A builder whose documents carry the new fields. |
Examples
Attach a computed field
import { fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.addFields((f) => ({ shoutTitle: fn.toUpper(f.title) }))
.build();
const withShout = await runtime.query(plan);lookup()
Join documents from another collection ($lookup).
Remarks
lookup()takes a callback that builds the join withfrom(collection).on((local, foreign) => ({ local, foreign })).as(name): the other collection, the two fields to match on, and the name of the output array field.- The object returned by
on()has two fixed keys,localandforeign. Write({ local: local.authorId, foreign: foreign._id }). - The joined documents arrive in an array under the
asname. When nothing matches, that array is empty. - A joined document's
_idand the_idof the document it was joined to are bothObjectIdobjects, and twoObjectIdobjects are never===even when they hold the same id. Compare them as strings:String(post.author[0]._id) === String(post.authorId).
Options
| Name | Type | Required | Description |
|---|---|---|---|
builder | Callback (from) => from(collection).on(...).as(name) | Yes | The join specification. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').lookup(...) | A builder whose documents carry the joined array. |
Examples
Join a foreign collection
const plan = db.query
.from('posts')
.match((f) => f.title.eq('Hello world'))
.lookup((from) =>
from('users')
.on((local, foreign) => ({ local: local.authorId, foreign: foreign._id }))
.as('author'),
)
.build();
const withAuthor = await runtime.query(plan);project()
Reshape each document.
Remarks
project()has two forms. A key-list form narrows to the named fields (project('title', 'kind'));_idis retained implicitly even when not listed.- A callback form computes a projection spec (
project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))). Write1to keep a field, or an expression to compute it. There is no0form here: the callback form cannot drop a field.
Options
| Name | Type | Required | Description |
|---|---|---|---|
...fields | Field names (string) | Key-list form | The fields to keep. |
spec | Callback (f) => ({ [field]: 1 | Expression }) | Callback form | The projection specification. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').project('title', 'kind') | A builder projected to the given shape. |
Examples
Key-list form
const plan = db.query.from('posts').project('title', 'kind').build();
const trimmed = await runtime.query(plan);Callback form
import { fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.project((f) => ({ title: 1, shout: fn.toUpper(f.title) }))
.build();
const projected = await runtime.query(plan);unwind()
Unroll an array field into one document per element.
Remarks
unwind(field, { preserveNullAndEmptyArrays? })maps to MongoDB's$unwind. Name the field holding the array. TypeScript accepts any field of the document; nothing checks that it holds an array.
Options
| Name | Type | Required | Description |
|---|---|---|---|
field | Field name (string) | Yes | The array field to unroll. |
options.preserveNullAndEmptyArrays | boolean | No | Keep documents whose array is null, missing, or empty. Defaults to false. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').unwind('tags') | A builder emitting one document per array element. |
Examples
Unroll an array field
This example needs a tags String[] field on Post, which the example schema does not have.
const plan = db.query
.from('posts')
.match((f) => f.title.eq('Hello world'))
.unwind('tags')
.sort({ tags: 1 })
.build();
const perTag = await runtime.query(plan);group()
Group documents by a key and compute per-group aggregates.
Remarks
group()takes a callback that receives only thefargument, one property per field ((f) => ...), and returns a spec object. The spec's_idsets the grouping key; every other key must be an accumulator.accholds the accumulatorsgroup()accepts, such as a count or a sum, documented under Accumulators. Import it and use it inside the callback, not as a second callback argument:import { acc } from '@prisma/orm-mongo/query-builder', thenacc.count(),acc.push(f.title). The callback takes one argument; there is no(f, acc) => ...form.- A
_id: nullkey groups the whole collection into a single bucket. - A non-accumulator value for a non-
_idkey throws as soon as you callgroup(), with an error whosecodeisORM.ARGUMENT_INVALID:group() field "<name>" must use an accumulator (e.g. acc.sum(), acc.count()). Got "<kind>" expression.Anullvalue for a key other than_idthrows with the same code and the messagegroup() field "<name>" must not be null. Only _id can be null.
Options
| Name | Type | Required | Description |
|---|---|---|---|
spec | Callback (f) => ({ _id: keyExpression | null, [alias]: acc.* }) | Yes | The grouping key and per-group accumulators. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').group(...) | A builder emitting one document per group. |
Examples
Group by a field
import { acc } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.group((f) => ({
_id: f.authorId,
postCount: acc.count(),
titles: acc.push(f.title),
}))
.build();
const perAuthor = await runtime.query(plan);Group the whole collection
import { acc } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.group((f) => ({ _id: null, total: acc.count(), latest: acc.max(f.createdAt) }))
.build();
const [summary] = await runtime.query(plan);For Prisma ORM 7 users, groupBy becomes a group() stage on the pipeline builder:
- const perAuthor = await prisma.post.groupBy({ by: ['authorId'], _count: true });
+ const perAuthor = await runtime.query(
+ db.query.from('posts').group((f) => ({ _id: f.authorId, postCount: acc.count() })).build(),
+ );replaceRoot()
Promote a computed sub-document to the top level.
Remarks
replaceRoot()takes a callback returning an expression that evaluates to an object. A bare scalar is rejected by MongoDB at runtime ('newRoot' expression must evaluate to an object).- A common pattern is to promote the first element of a
lookup()array withfn.arrayElemAt(f.author, fn.literal(0)).
Options
| Name | Type | Required | Description |
|---|---|---|---|
spec | Callback (f) => documentExpression | Yes | The document to promote to the root. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').replaceRoot(...) | A builder whose documents are the promoted sub-document. |
Examples
Promote a looked-up document
import { fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.match((f) => f.title.eq('Hello world'))
.lookup((from) =>
from('users')
.on((local, foreign) => ({ local: local.authorId, foreign: foreign._id }))
.as('author'),
)
.replaceRoot((f) => fn.arrayElemAt(f.author, fn.literal(0)))
.build();
const authors = await runtime.query(plan);count()
Reduce the pipeline to a single document holding the count. It is not acc.count(), which counts the documents in one group inside group().
Options
| Name | Type | Required | Description |
|---|---|---|---|
field | string | Yes | The output field name for the count. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').count('total') | A builder emitting one { [field]: n } document. |
Examples
Count documents
const plan = db.query.from('posts').count('total').build();
const [{ total }] = await runtime.query(plan);sortByCount()
Group by an expression and sort descending by group size.
Remarks
sortByCount()takes a callback returning the grouping expression. It emits one document per distinct value, each{ _id, count }, sorted bycountdescending.
Options
| Name | Type | Required | Description |
|---|---|---|---|
expression | Callback (f) => expression | Yes | The value to group and count by. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').sortByCount((f) => f.kind) | A builder emitting { _id, count } documents. |
Examples
Count occurrences of a field value
const plan = db.query
.from('posts')
.sortByCount((f) => f.kind)
.build();
const byKind = await runtime.query(plan);redact()
Keep or prune documents (and sub-documents) based on an expression.
Remarks
redact()maps to MongoDB's$redact, whose expression must evaluate to one of MongoDB's$$variables:$$KEEP,$$DESCEND, or$$PRUNE.- Write those variables with
f.rawPath('$KEEP')andf.rawPath('$PRUNE').f.rawPath(path)names a field or a MongoDB$$variable by its raw string, with no checking against your model, and it prefixes$to the string you give it. There is no expression helper for a$$variable, andfn.literal(...)cannot stand in: the server rejects bothfn.literal('KEEP')andfn.literal('$$KEEP'). - For which
fn.*values need their.nodeproperty here, see the table undermatch(). For anything more involved, userawCommand(), where you write the$redactstage with$$KEEPand$$PRUNEdirectly.
Options
| Name | Type | Required | Description |
|---|---|---|---|
spec | Callback (f) => expression | Yes | An expression evaluating to $$KEEP, $$DESCEND, or $$PRUNE. |
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').redact(...) | A builder that keeps or prunes each document. |
Examples
Keep tutorials and prune everything else
import { fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query
.from('posts')
.redact((f) =>
fn.cond(fn.eq(f.kind, fn.literal('tutorial')).node, f.rawPath('$KEEP'), f.rawPath('$PRUNE')),
)
.build();
const tutorials = await runtime.query(plan);Option-object stages
Several stages take a single MongoDB stage options object rather than a typed (f) => ... callback. Each one carries the same meaning it has in MongoDB. Where an option below is called a raw expression, it wants a MongoDB expression object rather than a typed builder value: build one with the fn.* helpers and pass its .node property, or name a field with MongoAggFieldRef.of('duration'), which is already a raw expression and takes no .node.
Remarks
bucket({ groupBy, boundaries, default_?, output? })sorts documents into the ranges you name.defaultis a reserved word in JavaScript, so the key isdefault_.groupByis a raw expression.bucketAuto({ groupBy, buckets, output?, granularity? })sorts them into a given number of ranges, chosen so each holds about the same number of documents.groupByis a raw expression.granularityis a string that MongoDB reads as the series it rounds the bucket boundaries to. Prisma ORM passes it through unchanged, accepts any string, and MongoDB rejects the ones it does not know.geoNear({ near, distanceField, spherical?, maxDistance?, minDistance?, query?, key?, distanceMultiplier?, includeLocs? })sorts documents by their distance from a point and writes that distance intodistanceField.nearis the point to measure from, and Prisma ORM passes it through to MongoDB unchanged.graphLookup({ from, startWith, connectFromField, connectToField, as, maxDepth?, depthField?, restrictSearchWithMatch? })follows links from document to document through one collection and collects what it reaches intoas.startWithis a raw expression.setWindowFields({ partitionBy?, sortBy?, output })computes a value for each document from the documents around it, such as a running total.partitionByis a raw expression.outputis an object mapping each new field name to{ operator, window? }, whereoperatoris a raw expression, usually an accumulator:{ runningTotal: { operator: MongoAggAccumulator.sum(MongoAggFieldRef.of('duration')), window: { documents: ['unbounded', 0] } } }.densify({ field, partitionByFields?, range })adds documents to fill the gaps in a sequence of numbers or dates.rangeis{ step, unit?, bounds }, whereboundsis'full','partition', or a pair of start and end values.fill({ partitionBy?, partitionByFields?, sortBy?, output })replaces missing or null values with a value you choose.partitionByis a raw expression.outputis an object mapping each field name to{ method }or{ value }, wherevalueis an expression andmethodis a string that Prisma ORM passes through to MongoDB unchanged.facet(facets)takes an object mapping each output field name to an array of stages.unionWith(collection, pipeline?)takes a collection name and an optional array of stages.- The stages you pass to
facetandunionWithare classes, one class per MongoDB stage, namedMongo<Stage>Stage. Import them, along withMongoAggFieldRefandMongoAggAccumulator, from@prisma/orm-mongo/query-ast/execution. Each class takes the same arguments as the MongoDB stage it builds. The three used below areMongoCountStage(field),MongoSortStage(spec), andMongoLimitStage(n).
Examples
Concatenate another collection with unionWith()
const plan = db.query.from('posts').unionWith('users').build();
const combined = await runtime.query(plan);Run several sub-pipelines with facet()
MongoFieldFilter is the filter class from @prisma/orm-mongo/query-ast/execution. See MongoFieldFilter on the ORM client page for its methods, such as gt and in.
import { MongoCountStage, MongoFieldFilter, MongoLimitStage, MongoMatchStage, MongoSortStage } from '@prisma/orm-mongo/query-ast/execution';
const plan = db.query
.from('posts')
.facet({
totalCount: [new MongoCountStage('count')],
newest: [new MongoSortStage({ createdAt: -1 }), new MongoLimitStage(2)],
tutorials: [
new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial')),
new MongoCountStage('count'),
],
})
.build();
const [facets] = await runtime.query(plan);
// facets.totalCount is [{ count: <number of posts> }]; facets.newest is the two newest postsSort into ranges with bucket()
MongoAggFieldRef.of() takes any field path as a string and does not check it against your models. duration is a field on Tutorial, so posts without it fall into the Other bucket.
import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution';
const plan = db.query
.from('posts')
.bucket({
groupBy: MongoAggFieldRef.of('duration'),
boundaries: [0, 15, 60],
default_: 'Other',
})
.build();
const buckets = await runtime.query(plan);
// one document per bucket, each { _id: <lower boundary>, count: n }Atlas-only stages
search(), searchMeta(), and vectorSearch() build MongoDB Atlas Search stages ($search, $searchMeta, $vectorSearch).
Remarks
- These stages need MongoDB Atlas. Nothing stops you from building them against a database that is not on Atlas. The error only appears when you run the query.
search(config, index?)takes an Atlas Search operator object, for example{ text: { query: 'hello', path: 'title' } }, and an optionalindex, which names the Atlas Search index to use.searchMeta(config, index?)takes the same two arguments and returns counts and facets instead of documents.vectorSearch({ index, path, queryVector, numCandidates, limit, filter? })takes the index name, the field holding the vectors, the vector to search for as an array of numbers, how many candidates to consider, how many documents to return, and an optional filter object.
Return type
| Return type | Example | Description |
|---|---|---|
| Pipeline builder | db.query.from('posts').search({ text: { query: 'hello', path: 'title' } }) | A builder with an Atlas Search stage. Executes only on MongoDB Atlas. |
Accumulators
Accumulators compute per-group values inside a group() stage. _id: null in the examples below means one group holding every document. Import them from @prisma/orm-mongo/query-builder:
import { acc } from '@prisma/orm-mongo/query-builder';The builder exposes nineteen accumulators:
| Accumulator | Signature | Description |
|---|---|---|
acc.count() | count() | Number of documents in the group. |
acc.sum(expr) | sum(expression) | Sum of the expression across the group. |
acc.avg(expr) | avg(expression) | Average of the expression. |
acc.min(expr) | min(expression) | Minimum value. |
acc.max(expr) | max(expression) | Maximum value. |
acc.first(expr) | first(expression) | Value from the first document in the group. |
acc.last(expr) | last(expression) | Value from the last document in the group. |
acc.push(expr) | push(expression) | Array of the expression's value from every document. |
acc.addToSet(expr) | addToSet(expression) | Array of distinct values. |
acc.firstN({ input, n }) | firstN({ input, n }) | First n values. |
acc.lastN({ input, n }) | lastN({ input, n }) | Last n values. |
acc.maxN({ input, n }) | maxN({ input, n }) | The n largest values. |
acc.minN({ input, n }) | minN({ input, n }) | The n smallest values. |
acc.top({ output, sortBy }) | top({ output, sortBy }) | The output value from the document that sorts first under sortBy. |
acc.bottom({ output, sortBy }) | bottom({ output, sortBy }) | The output value from the document that sorts last under sortBy. |
acc.topN({ output, sortBy, n }) | topN({ output, sortBy, n }) | The output values from the first n documents under sortBy. |
acc.bottomN({ output, sortBy, n }) | bottomN({ output, sortBy, n }) | The output values from the last n documents under sortBy. |
acc.stdDevPop(expr) | stdDevPop(expression) | Population standard deviation. |
acc.stdDevSamp(expr) | stdDevSamp(expression) | Sample standard deviation. |
firstN,lastN,maxN, andminNtake{ input, n }.inputis the expression whose values you collect.topandbottomtake{ output, sortBy }.topNandbottomNtake the same two plusn.outputis the expression whose value you want back from each chosen document.sortByis a plain object such as{ createdAt: 1 }, where1sorts ascending and-1sorts descending. Its keys are plain strings because they name fields, not values.- Wherever a signature takes
n, wrap the number: writen: fn.literal(2), notn: 2.fn.literalcomes from the expression helpers below.
Put a sort() before the group() when you use acc.first() or acc.last(). Without one, the document MongoDB calls first is arbitrary and can change between runs.
Examples
count() and sum()
acc.sum(fn.literal(1)) adds 1 for each document, which counts them. To add up a field instead, pass the field, as in acc.sum(f.views) on a collection with a numeric views field. The example schema's posts has no such field, so that call needs a collection of your own.
import { acc, fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query.from('posts')
.group((f) => ({ _id: null, total: acc.count(), counted: acc.sum(fn.literal(1)) })).build();
const [{ total, counted }] = await runtime.query(plan);
// { total: 2, counted: 2 }avg(), min(), and max()
const plan = db.query.from('posts')
.group((f) => ({ _id: null, earliest: acc.min(f.createdAt), avgYear: acc.avg(fn.year(f.createdAt)) }))
.build();
const [stats] = await runtime.query(plan);
// { earliest: <Date>, avgYear: 2024 }first() and last()
const plan = db.query.from('posts').sort({ createdAt: 1 })
.group((f) => ({ _id: null, firstTitle: acc.first(f.title), lastTitle: acc.last(f.title) }))
.build();
const [{ firstTitle, lastTitle }] = await runtime.query(plan);
// { firstTitle: 'Hello world', lastTitle: 'Tutorial one' }push() and addToSet()
Both take one expression: group((f) => ({ _id: null, allTitles: acc.push(f.title), distinctKinds: acc.addToSet(f.kind) })). allTitles holds one entry per document. distinctKinds holds each value once.
firstN() (an N-variant)
const plan = db.query.from('posts').sort({ createdAt: 1 })
.group((f) => ({
_id: null,
firstTwo: acc.firstN({ input: f.title, n: fn.literal(2) }),
newestTitle: acc.top({ output: f.title, sortBy: { createdAt: -1 } }), // the title string, not a document
}))
.build();
const [{ firstTwo, newestTitle }] = await runtime.query(plan);
// firstTwo is ['Hello world', 'Tutorial one']Expression helpers
Expression helpers (fn.*) build the computed values used inside stages like addFields(), project(), group() accumulators, and match() (via expr()). Inside a stage, write f.title or fn.toUpper(...), never 'hello' or 3 on their own. Wrap a constant in fn.literal('hello'). The update operators on the write methods and the MongoFieldFilter methods work the other way round. They take ordinary JavaScript values, so write f.bio.set('hello'), not f.bio.set(fn.literal('hello')). Import the helpers from @prisma/orm-mongo/query-builder:
import { fn } from '@prisma/orm-mongo/query-builder';Each helper is the camelCase name of the MongoDB aggregation operator without its $ prefix, so $toUpper is fn.toUpper and $dateToString is fn.dateToString. Five helpers are named differently:
| MongoDB operator | Helper |
|---|---|
$in | fn.isIn |
$type | fn.typeOf |
$toString | fn.toString_ |
$first and $last | fn.firstElem and fn.lastElem |
The first three are renamed because in and typeof are reserved words in JavaScript and toString is already a method on every JavaScript object.
Here is every helper. Anything missing from this list has no helper, including $and, $or, $not, $switch, $ifNull, $map, $reduce, and $filter.
| Group | Helpers |
|---|---|
| Arithmetic | add, subtract, multiply, divide |
| Strings | concat, toLower, toUpper, substr, substrBytes, trim, ltrim, rtrim, split, strLenCP, strLenBytes, replaceOne, replaceAll |
| Regular expressions | regexMatch, regexFind, regexFindAll |
| Dates | year, month, dayOfMonth, hour, minute, second, millisecond, dateToString, dateFromString, dateDiff, dateAdd, dateSubtract, dateTrunc |
| Comparison | cmp, eq, ne, gt, gte, lt, lte |
| Arrays | size, arrayElemAt, concatArrays, firstElem, lastElem, isIn, indexOfArray, isArray, reverseArray, slice, zip, range |
| Sets | setUnion, setIntersection, setDifference, setEquals, setIsSubset, anyElementTrue, allElementsTrue |
| Type conversion | typeOf, convert, toInt, toLong, toDouble, toDecimal, toString_, toObjectId, toBool, toDate |
| Objects | objectToArray, arrayToObject, getField, setField |
| Constants and branching | literal, cond |
fn.literal(value) wraps a constant so MongoDB reads it as a value and not as the name of a field. .node is the raw MongoDB expression inside an fn.* value, and a field reference such as f.title has one too. Passing an expr(...) value where .node is expected throws a TypeError. This table is the whole rule for when to write .node:
| Call site | What you pass |
|---|---|
project(), addFields(), redact(), and the arguments of an accumulator | The fn.* value itself. |
The condition argument of fn.cond() | Its .node property. |
f.stage.set(), f.stage.replaceRoot(), and f.stage.replaceWith(), documented under Update operation forms | The .node property. |
The groupBy, startWith, and partitionBy options of bucket(), bucketAuto(), graphLookup(), setWindowFields(), and fill() | The .node property. |
match() | The fn.* value wrapped in expr(...). |
expr() is what lets a match() filter compare computed values. Import it beside fn:
import { expr, fn } from '@prisma/orm-mongo/query-builder';
const plan = db.query.from('posts').match((f) => expr(fn.gt(fn.year(f.createdAt), fn.literal(2024)))).build();For OR, AND, or NOT, pass match() a filter you build yourself instead of a callback:
import { MongoAndExpr, MongoFieldFilter, MongoOrExpr } from '@prisma/orm-mongo/query-ast/execution';
const kindFilter = MongoOrExpr.of([
MongoFieldFilter.eq('kind', 'tutorial'),
MongoFieldFilter.eq('kind', 'guide'),
]);
const plan = db.query.from('posts').match(kindFilter).build();
const rows = await runtime.query(plan);
// rows are the documents whose kind is either tutorial or guideRemarks
- There is no
fn.*helper for OR, AND, or NOT. In amatch()filter, build the filter yourself:MongoOrExpr.of([...])is OR,MongoAndExpr.of([...])is AND, and.not()on any filter is NOT. The example above shows OR. - Each entry in those lists is a
MongoFieldFilter, which haseq,neq,gt,gte,lt,lte,in,nin,isNull, andisNotNull. These are the same filter methods the ORM client uses, described under Combinators. A filter you build yourself names the field as a string, as inMongoFieldFilter.eq('kind', 'tutorial'), and nothing checks that string against your model. - "Not equal" has two spellings on this page. The filter method is
MongoFieldFilter.neqand the expression helper isfn.ne. - Inside a computed value in
project()oraddFields()there is no way to write OR, AND, or NOT. Write that stage yourself withpipe(), or write the whole command withrawCommand(). fn.slice(array, ...rest)andfn.range(start, end, step)take positional arguments, and so does every helper not listed in the table below. This page does not give the positional order for those helpers. Look each one up in MongoDB's aggregation operator reference. Each argument is an expression from thefargument or afn.literal(...)value.fn.toObjectId()cannot take afn.literal()value.fn.toObjectId(fn.literal(id))throws aTypeError. There is no way to turn a string id into anObjectIdinside a pipeline. Build theObjectIdin your own code instead and put it in a raw$matchwithrawCommand(), as the last example on this page does.
These helpers take one object of named arguments instead of positional ones:
| Helper | Arguments |
|---|---|
fn.dateDiff | { startDate, endDate, unit, timezone?, startOfWeek? } |
fn.dateTrunc | { date, unit, binSize?, timezone?, startOfWeek? } |
fn.dateAdd | { startDate, unit, amount, timezone? } |
fn.dateSubtract | The same keys as fn.dateAdd. |
fn.dateToString | { date, format?, timezone?, onNull? } |
fn.dateFromString | { dateString, format?, timezone?, onError?, onNull? } |
fn.trim, fn.ltrim, and fn.rtrim | { input, chars? } |
fn.regexMatch, fn.regexFind, and fn.regexFindAll | { input, regex, options? } |
fn.replaceOne and fn.replaceAll | { input, find, replacement } |
fn.zip | { inputs, useLongestLength?, defaults? } |
fn.getField | { field, input? } |
fn.setField | { field, input, value } |
fn.convert | { input, to, onError?, onNull? } |
In fn.convert, to takes any MongoDB type name written as an expression, such as fn.literal('int') or fn.literal('objectId').
Examples
Arithmetic in a projection
const plan = db.query.from('posts')
.project((f) => ({ title: 1, computed: fn.add(fn.literal(1), fn.multiply(fn.literal(2), fn.literal(3))) }))
.build();
const rows = await runtime.query(plan);
// computed is 7 for every documentString composition
project((f) => ({ shout: fn.concat(fn.toUpper(f.title), fn.literal('!')) })) puts the title in upper case next to a !.
Branch with cond()
const plan = db.query.from('posts')
.project((f) => ({
title: 1,
label: fn.cond(fn.eq(f.kind, fn.literal('tutorial')).node, fn.literal('is-tutorial'), fn.literal('not-tutorial')),
}))
.build();
const rows = await runtime.query(plan);pipe()
Append an arbitrary raw pipeline stage the typed methods don't cover.
Remarks
pipe(stage)appends a raw stage that you construct yourself, and keeps the row type you already had. The stages are theMongo*Stageclasses, one per MongoDB stage, from@prisma/orm-mongo/query-ast/execution. The class name isMongo, then the stage name without its$, thenStage, so$unwindisMongoUnwindStage. The example below usesMongoMatchStageandMongoCountStage. The same module exportsMongoFieldFilter, which builds the filter aMongoMatchStagetakes.pipe<NewShape>(stage)declares the row type the rows have after that stage.NewShapeis a type you write yourself, as at the end of the example below.- After a
pipe()stage the TypeScript type no longer describes the rows. They come back exactly as MongoDB returns them, so a date is aDateand an_idthat MongoDB generated is anObjectIdfrom themongodbpackage, the driver Prisma ORM runs your queries through. - Prefer a typed stage where one exists.
pipe()adds one raw stage to a typed chain, andrawCommand()replaces the whole command.
Examples
Append a raw stage
import { MongoCountStage, MongoFieldFilter, MongoMatchStage } from '@prisma/orm-mongo/query-ast/execution';
const plan = db.query.from('posts')
.pipe(new MongoMatchStage(MongoFieldFilter.eq('kind', 'tutorial'))).build();
const rows = await runtime.query(plan);
type Counted = { total: number };
const countPlan = db.query.from('posts').pipe<Counted>(new MongoCountStage('total')).build();Read methods
Finish a read chain with build(), then run the built query with runtime.query(...).
build() and aggregate()
Compile the pipeline into a query you can run.
Remarks
- Write
build().aggregate()is another name for the same method. Neither runs the query: pass the result toruntime.query(...)to fetch documents.
Write methods
The pipeline builder can write, not just read. Write methods are available at three points: on the root collection, after a match() filter, and at the end of any pipeline, where out() and merge() write the output into a collection. A write method returns the built query on its own, so there is no build() call after one. The examples below name that built query plan.
Updater callbacks (the (f) => [...] argument to the update methods) must return an array of operations. This is a firm rule:
- Always return an array. Returning a bare operation,
f.bio.set('x')rather than[f.bio.set('x')], throws as soon as you call the write method. - An empty array throws an error whose
codeisORM.MUTATION_DATA_MISSING:Updater returned no operations. Return at least one update from the callback .... - An updater is either all operator form or all pipeline form. Operator form calls an operator on a field, as in
f.bio.set(value). Pipeline form can read other fields of the same document, and it callsf.stage.*. Typef.stageexactly:stageis not a placeholder for a stage name. You cannot mix the two in one updater. TypeScript rejects the mixed array, and at run time it throws an error whosecodeisORM.ARGUMENT_INVALID:Cannot mix .... - Applying the same operator to the same field twice in one updater throws an error whose
codeisORM.ARGUMENT_INVALID:Update spec collision: ....
Root-level writes
insertOne, insertMany, updateAll, deleteAll, and upsertOne are available on the collection you name in from(...), before you add any stage.
Remarks
- These return a built query. Run it with
runtime.query(...). - Write results are result objects, not documents:
insertOnegives{ insertedId },insertManygives{ insertedIds, insertedCount }, the update methods andupsertOnegive{ matchedCount, modifiedCount }, plusupsertedCountandupsertedIdwhen an upsert inserted a document, and the delete methods give{ deletedCount }. A write returns one result object, as the single row of the result, which is why every example here reads it withconst [result] = .... - The document you pass to
insertOne/insertManyis a plain record. TypeScript requires no particular field, and the builder does not check the record against your contract. You can leave nullable fields out. The examples below passnullfor them to make the document shape explicit. - You can leave
_idout and let MongoDB assign one. - On the way in, values are stored exactly as you pass them, so pass a
Datefor a date field and anObjectIdfrom themongodbpackage for an id field. Nothing turns a string into either. This is the write direction only. Reads are decoded as the first half of this page describes. match()filter values follow the same rule as the values you insert. The one value you cannot filter on this way is_id. An_idfilter written inmatch()never matches anything, so userawCommand()instead. Thematch()warning gives the detail, and the last example on this page shows the form that works.insertMany([])throws an error whosecodeisORM.MUTATION_DATA_MISSING.
Examples
insertOne() and insertMany()
const onePlan = db.query.from('users')
.insertOne({ name: 'Carol', email: 'carol@example.com', bio: null, role: 'author', address: null });
const [insertOneResult] = await runtime.query(onePlan);
// { insertedId: <ObjectId> }
const manyPlan = db.query.from('users').insertMany([
{ name: 'Carol', email: 'carol@example.com', bio: null, role: 'author', address: null },
{ name: 'Dave', email: 'dave@example.com', bio: null, role: 'reader', address: null },
]);
const [insertManyResult] = await runtime.query(manyPlan);
// { insertedIds: [<ObjectId>, <ObjectId>], insertedCount: 2 }updateAll() (array-of-ops updater)
db.query.from('users').updateAll((f) => [f.bio.set('everyone now has bio')]) builds the update. Pass it to runtime.query(...) to run it.
deleteAll()
db.query.from('users').deleteAll() builds a delete for every document. Pass it to runtime.query(...) and read deletedCount from the single result object.
upsertOne() on the root (filter, then updater)
upsertOne(filterCallback, updaterCallback). The first callback returns the filter. The second returns the array of update operations.
const plan = db.query.from('users')
.upsertOne((f) => f.email.eq('erin@example.com'), (f) => [f.name.set('Erin'), f.email.set('erin@example.com')]);
const [result] = await runtime.query(plan);
// inserts when no document matches: result.upsertedId is definedWrites after match()
After a match() filter, updateMany, updateOne, deleteMany, deleteOne, upsertOne, findOneAndUpdate, and findOneAndDelete write against the matched documents.
Remarks
- The
match()filter supplies the write's filter, so you do not repeat it. - After
match(),upsertOne()takes the updater callback only (the filter comes frommatch()). updateOne/deleteOneaffect at most one matching document.updateMany/deleteManyaffect all matches. Which document that is, this builder gives you no way to choose.findOneAndUpdate()andfindOneAndDelete()return the document exactly as themongodbdriver gives it, so its_idis anObjectIdfrom themongodbpackage even though the TypeScript type saysstring. To get the id as a string, writeString(result._id).findOneAndUpdate()andfindOneAndDelete()are offered straight aftermatch()and nowhere else. TypeScript withdraws them after every other stage,sort()andskip()included. If a cast gets one past TypeScript after askip(), it throws an error whosecodeisORM.OPERATION_UNSUPPORTED.
Examples
updateMany() and updateOne()
const manyPlan = db.query.from('users').match((f) => f.role.eq('author'))
.updateMany((f) => [f.bio.set('matched-many')]);
await runtime.query(manyPlan);
const onePlan = db.query.from('users').match((f) => f.email.eq('alice@example.com'))
.updateOne((f) => [f.bio.set('single-update')]);
const [result] = await runtime.query(onePlan);
// { matchedCount: 1, modifiedCount: 1 }deleteMany() and deleteOne()
const plan = db.query.from('users').match((f) => f.role.eq('author')).deleteMany();
const [result] = await runtime.query(plan);
// { deletedCount: 2 }upsertOne() after match() (updater only)
db.query.from('users').match((f) => f.email.eq('alice@example.com')).upsertOne((f) => [f.bio.set('upserted via match')]) builds the upsert. On a hit, result.modifiedCount is 1.
findOneAndUpdate() and returnDocument
findOneAndUpdate() returns the matched document. Its second argument is optional. That argument takes returnDocument, which controls which version you get back: 'before' returns the document before the update, 'after' returns it after. The default is 'after'. The same argument takes upsert, which defaults to false and inserts a document when nothing matches.
const plan = db.query.from('users').match((f) => f.email.eq('alice@example.com'))
.findOneAndUpdate((f) => [f.bio.set('changed')], { returnDocument: 'before' });
const [beforeDoc] = await runtime.query(plan);
// beforeDoc.bio is the value from before the updatereturnDocument, not returnNewDocumentUse returnDocument: 'before' | 'after'. returnNewDocument is not a valid option name. TypeScript rejects it and suggests returnDocument. If a cast gets it past TypeScript, the unknown key is ignored and you silently get the default 'after'.
findOneAndDelete()
db.query.from('users').match((f) => f.email.eq('alice@example.com')).findOneAndDelete() builds the delete. Its single result row is the removed document.
Update operation forms
Inside an updater callback, each operation targets a field. There are two mutually exclusive forms.
Remarks
- Operator form: call an operator on the
fargument, one operator per field. There are fourteen, in the table below. - Pipeline form: aggregation-pipeline update stages through
f.stage.*, for examplef.stage.set({ bio: f.name.node }). The four stages areset,unset,replaceRoot, andreplaceWith. - The two forms cannot be mixed in a single updater (see the write-methods intro). An updater is entirely operator form or entirely pipeline form.
Operator form takes these arguments:
| Operator | What you pass |
|---|---|
set(value) | The new value. |
unset() | Nothing. Removes the field. |
rename(newName) | The new field name, as a string. |
inc(amount) | A number to add. A negative number subtracts. |
mul(factor) | A number to multiply the stored value by. |
min(value) | A value, written only if it is lower than the stored one. |
max(value) | A value, written only if it is higher than the stored one. |
push(value) | One value to append to an array field. |
addToSet(value) | One value, appended only if the array does not already hold it. |
pop(direction) | 1 removes the last element, -1 the first. The argument is optional and defaults to 1. |
pull(value) | The value to remove from an array field. |
pullAll(values) | An array of values to remove. |
currentDate() | Nothing. Writes the current date. |
setOnInsert(value) | A value written only when an upsert inserts a new document, so it does nothing outside upsertOne(). |
Examples
Operator form
const plan = db.query.from('users').match((f) => f.role.eq('author'))
.updateMany((f) => [f.bio.set('operator form')]);
await runtime.query(plan);
// on a collection with a numeric views field and an array tags field (posts has neither):
// updateMany((f) => [f.views.inc(1), f.tags.push('mongodb')])Pipeline form (f.stage.*)
const plan = db.query.from('users').match((f) => f.role.eq('author'))
.updateMany((f) => [f.stage.set({ bio: f.name.node })]);
await runtime.query(plan);
// each author's bio is set to that author's own namePipeline write methods: out() and merge()
out() and merge() write the pipeline's output into a collection ($out / $merge).
Remarks
out(collection)writes the pipeline output into a destination collection, replacing its contents. Pass a second argument, the database name as a string, to write into a different database:out('users_snapshot', 'archive').merge({ into })writes the pipeline output into a target collection, merging with existing documents.intois a collection name or{ db, coll }. The options object also takeson, which is one field name as a string or several as an array of strings, pluswhenMatchedandwhenNotMatched.whenMatchedtakes'replace','keepExisting','merge', or'fail', or an array of update stages, which areMongoAddFieldsStage,MongoProjectStage, andMongoReplaceRootStageobjects from@prisma/orm-mongo/query-ast/execution.MongoAddFieldsStagetakes one object of field names and expressions, as innew MongoAddFieldsStage({ ... }).whenNotMatchedtakes'insert','discard', or'fail'. A misspelled value fails at the database rather than in TypeScript.- Both end the chain. They return a built query you run with
runtime.query(...), and that call returns an empty array, because the output goes to the destination collection and not to your program.
Examples
Write the output with out()
db.query.from('users').out('users_snapshot') builds the write. After you pass it to runtime.query(...), the users_snapshot collection holds the pipeline output.
Merge with merge()
const plan = db.query.from('users')
.merge({ into: 'users_archive', on: 'email', whenMatched: 'merge', whenNotMatched: 'insert' });
await runtime.query(plan);rawCommand()
Run a raw MongoDB aggregate command through the pipeline builder.
Remarks
db.query.rawCommand(command)takes a command you build yourself, for examplenew RawAggregateCommand(collection, pipeline), and sends it as you wrote it. The rows come back asunknown, so you type them yourself.- Use it for anything the typed builder cannot express, including
_idequality filters (see thematch()warning) and MongoDB's$redactstage. - For the full raw MongoDB API (raw collection methods, untyped writes, and rows that come back as the driver gives them), see Raw queries.
Examples
Run a raw aggregate pipeline
import { RawAggregateCommand } from '@prisma/orm-mongo/query-ast/execution';
const plan = db.query.rawCommand(new RawAggregateCommand('posts', [{ $count: 'total' }]));
const rows = await runtime.query(plan);
// [{ total: 2 }]Filter by _id (the way that works)
import { ObjectId } from 'mongodb';
const plan = db.query.rawCommand(
new RawAggregateCommand('posts', [{ $match: { _id: new ObjectId(postId) } }]),
);
const rows = await runtime.query(plan);
// a real ObjectId in a raw pipeline document matches correctly