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 →