Skip to main content

Reading documents

Every read is a terminal method — calling one triggers resolution, compilation, and execution. The chain that leads up to it (filters, pagination, projection, views) stays the same whatever terminal you use; only the return shape changes.

Every read terminal accepts an optional interfacePath string for type casting via NaoInterfaceValidPaths. Pass it and the returned documents are typed as FlowDocument<YourInterface>.

getMany

The workhorse: zero or more matching documents, wrapped in a SearchResponse.

const result = await bob.flowQuery('finance/bill')
.status('posted')
.limit(50)
.getMany('FinanceInterface.Bill')

result.ok // true
result.data // FlowDocument<FinanceInterface.Bill>[]
result.meta // { totalDocuments?: 142, last?: boolean, queryTimeMs?: 17 }

.all() removes the default 20-doc limit:

const everything = await bob.flowQuery('finance/bill')
.status('posted')
.all()
.getMany()

getOne

Returns the first match or null. Never throws for "not found."

const bill = await bob.flowQuery('finance/bill')
.docId('B00000048')
.getOne('FinanceInterface.Bill')
// → FlowDocument<FinanceInterface.Bill> | null

getById

Shorthand for .docId(id).getOne():

const bill = await bob.flowQuery('finance/bill')
.getById('B00000048', 'FinanceInterface.Bill')

getFirst

Like getOne but throws FlowDocumentNotFoundError when nothing matches. Use it when the absence of the document should halt the hook.

const bill = await bob.flowQuery('finance/bill')
.docId(docId)
.getFirst('FinanceInterface.Bill')

getOrDefault

Returns the document if found, otherwise the default you provide:

const config = await bob.flowQuery('settings/config')
.docId('DEFAULT')
.getOrDefault({ locale: 'en-US' })

exists

Boolean check. Cheap — the compiler omits projection and limits the cursor to 1.

const has = await bob.flowQuery('finance/bill')
.docId('B00000048')
.exists()

count

Returns a SearchResponse with meta.totalDocuments populated. data is empty.

const res = await bob.flowQuery('finance/bill')
.status('posted')
.count()
res.meta.totalDocuments // 42

pluck

Pull one field from every matching document into a flat array.

const vendorIds = await bob.flowQuery('finance/bill')
.status('posted')
.pluck<string>('data.vendorId')
// ['V001', 'V002', 'V003', ...]

The projection is set to the single field; nothing else is transferred over the wire.

distinct

Unique values for a single field:

const currencies = await bob.flowQuery('finance/bill')
.status('posted')
.distinct<string>('data.currency')
// ['USD', 'EUR']

paginate

Page-based pagination with metadata. Arguments: page number (1-based), page size, and optional interfacePath.

const page = await bob.flowQuery('contacts/contact')
.status('active')
.paginate(2, 25, 'AccountsInterface.Contact')

page.data // FlowDocument<AccountsInterface.Contact>[]
page.totalPages
page.hasNext
page.hasPrev

If you need manual pagination (e.g. cursor-based or with a custom stable sort), use .skip().limit().sortAsc() and getMany.

getManyInBatches

Async iterator for streaming large result sets without loading them all at once. .batch(size) configures the page size.

for await (const batch of bob.flowQuery('finance/bill')
.status('posted')
.batch(500)
.getManyInBatches('FinanceInterface.Bill')) {

await processInBulk(batch.data)
// batch.data, batch.count, batch.first, batch.last, batch.totalCount
}

getSettings

Shortcut for reading a document that's known to be a settings document — enforces the settings convention and returns the single row.

const settings = await bob.flowQuery('finance/settings').getSettings()

Projection

Trim the wire payload. .projection() replaces the default projection outright; .addToProjection() augments it.

const result = await bob.flowQuery('finance/bill')
.status('posted')
.projection({ 'data.amount': 1, 'data.status': 1, docId: 1 })
.limit(5)
.getMany()

If you don't call either, the Mongo backend applies a sensible default: data, dataPublic, fullData, docId, immutable, businessUnitId, info, _id, docType, namespace, companyId, status, deploymentId, naoQueryOptions.

Sorting

.sortAsc('data.createdAt')
.sortDesc('data.dueDate')
.sort([{ field: 'data.amount', dir: 'asc' }])

Sorts are applied in the order they're added.

Views (lookups)

Views are named lookup pipelines declared on the document definition. Adding them produces an aggregation pipeline instead of a simple find:

const bills = await bob.flowQuery('finance/bill')
.status('posted')
.withView('get-vendor')
.withView('get-line-items')
.getMany('FinanceInterface.Bill')

// With optional post-view filter
.withView('get-vendor', { 'vendor.data.country': 'US' })

// All views defined on the document
.withAllViews()

Watch — change streams

Real-time change streams for filters that make sense to subscribe to:

const sub = bob.flowQuery('finance/bill')
.status('posted')
.watch({ debounceMs: 500 })

sub.on('change', (event) => { /* … */ })

See Advanced for details and caveats.

Cloning for parallel queries

Need a count and a page of data from the same filter? .clone() makes a deep copy so both branches can execute concurrently without interference:

const base = bob.flowQuery('finance/bill')
.status('posted')
.sortDesc('data.dueDate')

const [countRes, dataRes] = await Promise.all([
base.clone().count(),
base.clone().limit(10).getMany('FinanceInterface.Bill'),
])

Next: Writing documents →