Skip to main content

Filtering

The builder filters fall into a few groups: status filters, data-type filters, field-level filters (.where*()), document targeting (.docId* / .mongoIds), the raw-query escape hatch, policies, and scope escape hatches. All filters AND together — call as many as you like, in any order.

Status filters

Status is a first-class concept on every document. You can filter by concrete value or by semantic category.

// Concrete status (OR within a single call)
.status('posted')
.status('draft', 'awaiting-approval')

// Semantic categories
.statusMutable() // all non-immutable statuses
.statusImmutable() // all immutable statuses
.statusFinal() // final success + final failure
.statusFinalSuccess() // final success only
.statusFinalFailure() // final failure only
.statusIntermediary() // non-final statuses only
.statusDefault() // the single default status

// Exclusion — chainable, call multiple times
.statusExclude('void')
.statusExclude('deleted')

Semantic categories are expanded at resolution time against the document's StatusGroup, so the same query adapts automatically as the definition evolves.

Soft-deleted documents

Soft-deleted documents (envelope status = 'deleted') are hidden from every query by default. Opt-in to see them:

const rows = await bob.flowQuery('vendors/vendor')
.docId(vendorId)
.includeDeleted()
.getOne()

Data-type filters

Filter by the value stored in a data-type-bound field. Use the data-type slug — the resolver maps the slug to the concrete field path defined on the document.

.dataType('bill-payment-reconciliation-status', 'overdue')
.dataType('bill-payment-reconciliation-status', 'overdue', 'unpaid') // OR

.dataTypeExclude('bill-payment-reconciliation-status', 'paid')

Field-level filters

.where() takes a dot-notation field path, an operator, and a value. Operators: eq, ne, gt, gte, lt, lte, contains, startsWith, endsWith, in, nin, exists, notExists, regex.

.where('data.amount', 'gt', 1000)
.where('data.vendorId', 'eq', 'V00001')
.where('data.tags', 'in', ['urgent', 'priority'])
.where('data.notes', 'contains', 'reconciliation')
.where('data.attachments', 'exists')

Multiple .where*() calls compose with AND. Contradictory filters produce zero results, not an error.

Shorthand helpers

.whereIn('data.status',  ['draft', 'posted'])
.whereNotIn('data.status', ['void', 'deleted'])

.whereExists('data.dueDate')
.whereNotExists('data.cancelledAt')

.whereContains('data.name', 'acme') // regex-based partial match
.whereBetween('data.amount', 100, 50_000) // inclusive: $gte + $lte

Raw query escape hatch

For anything the helpers don't cover, feed the compiler a native Mongo fragment:

.query({ docId: { $regex: '^B', $options: 'i' } })
.query({ 'data.metadata.internal': { $exists: true, $ne: null } })

The fragment is merged into the compiled query; it's an escape hatch, not a replacement for the structured filters. Prefer .where*() where possible so plugins, caches, and analytics see the intent.

Document targeting

.docId('B00000048')                                 // single
.docId('B00000048', 'B00000049') // multiple (varargs)
.docIds(['B00000048', 'B00000049']) // multiple (array)

.mongoIds(['6507a1b2c3d4e5f6a7b8c9d0']) // target by _id
.extraDocumentIds(['AUX_ID_1', 'AUX_ID_2']) // additional ids

docIds([]) deliberately returns zero results — an empty allow-list is not the same as "no filter."

Policies

Apply a named document policy to the query. Policies are defined on the document and enforce fine-grained access rules (e.g. canUpdate, canViewPII):

.addPolicy('canUpdate')
.addPolicy('canViewPII')

Scope escape hatches

Every query is implicitly scoped by the calling user's membership, company, and business unit. These escape hatches lift the scope when you need cross-tenant or system-level reads:

.skipMembership()              // bypass membership scoping
.skipCompany() // bypass company scoping
.skipBusinessUnit() // bypass business unit scoping
.setCompanyId('COMP-42') // pin a specific company
.setSessionTransactionId(id) // pin a specific session
.skipSession() // bypass the debe transaction entirely

Use these deliberately. The default scoping is what prevents one tenant from seeing another's data.

Putting it together

const claims = await bob.flowQuery('finance/expense-claim')
.statusMutable()
.dataType('expense-category', 'travel', 'meals')
.where('data.amount', 'gt', 25)
.whereExists('data.receiptUrl')
.addPolicy('canApprove')
.sortDesc('data.submittedAt')
.limit(100)
.getMany('FinanceInterface.ExpenseClaim')

Next: References →