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 →