Streams
Read many documents batch by batch with for await and Node.js streams, and follow changes with change streams.
await on a cursor reads every document into an array. To go through many documents without
holding them all, iterate the cursor with for await: it reads them batch by batch, as the server
returns them.
import { createWriteStream } from 'node:fs'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { ObjectId } from 'mongodb'
import { mongozod } from 'mongozod'
import * as mz from 'mongozod/mzod'
import * as z from 'zod'
const postSchema = z.object({
_id: mz.objectId()
.default(() => new ObjectId()),
title: z.string()
.min(1),
body: z.string()
.default(''),
status: z.enum(['draft', 'published'])
.default('draft')
})
const PostsModel = mongozod({
collection: 'posts',
db,
schema: postSchema
})
let words = 0
for await (const post of PostsModel.find({ status: 'published' }, { projection: { body: 1 } })) {
words += post.body.split(/\s+/).length
}Iterate with for await
Each batch gets its virtuals and its populate at once, with one query per reference. The
driver's batchSize option, in the options of find, sets how many documents a batch has. See
Operations for the rest of the cursor, such as closing it when a
loop ends early.
The cursor has no .map(), .next(), .hasNext() or .forEach(). for await covers them, and an
async generator maps the documents:
async function* titles() {
for await (const post of PostsModel.find({}, { projection: { title: 1 } })) {
yield post.title.toUpperCase()
}
}
for await (const title of titles()) console.log(title)Pipe into a Node.js stream
Readable.from() turns the cursor, or a generator over it, into a Node.js stream, for the APIs that
take a Readable. This writes the published posts to a file of
NDJSON, one per line, with pipeline:
async function* ndjson(posts: AsyncIterable<unknown>) {
for await (const post of posts) yield `${JSON.stringify(post)}\n`
}
await pipeline(
Readable.from(ndjson(PostsModel.find({ status: 'published' }))),
createWriteStream('posts.ndjson')
)Follow changes with change streams
A model has no watch: open a change stream on the driver's collection, PostsModel.collection,
which is typed with the schema:
const changes = PostsModel.collection.watch([{ $match: { operationType: 'insert' } }])
for await (const change of changes) {
if (change.operationType === 'insert') console.log('new post', change.fullDocument.title)
}It is the driver's
watch(),
with its options, such as fullDocument: 'updateLookup' for the document after each update, and
changes.close() to stop. Change streams need a replica set or a sharded cluster
(MongoDB manual).
A change stream reports a write once it is stored, so it can't validate, change or cancel it, and
its events don't go through the plugins of the model or the schema. A soft delete of
softDelete arrives as an update. With
db as a function, PostsModel.collection is the collection of the database
the function returns when you call it.
Common mistakes
awaiton a large cursor: it loads every document into memory. Usefor await, orReadable.from()for a stream.- Looking for
cursor.map()orcursor.forEach(): they don't exist. Usefor awaitand an async generator. - Expecting a change stream to validate: it reports writes after they are stored, without the schema or the plugins. Validate in the writes, and react in the stream.
Last updated on
MongoZod is an independent open-source project. It is not affiliated with, endorsed by or sponsored by MongoDB, Inc. MongoDB® is a registered trademark of MongoDB, Inc.