Migration from legacy Flow Query
@logic-bee/flow-query (the legacy API, reachable as bob.flow.query(...))
has been superseded by @logic-bee/data-flow (reachable as
bob.flowQuery(...)). Both coexist — hooks can use either or both.
New code should use the new API; existing hooks can migrate one
query at a time.
At a glance
| Concern | Legacy | New |
|---|---|---|
| Entry point | bob.flow.query('finance/bill') | bob.flowQuery('finance/bill') |
| Get collection | flowGlobal.getFlowCollection('finance/bill') | Implicit — the builder handles it |
| Configure target | .flowOptions({ docName: 'bill', ... }) | Implicit — derived from path |
| Read many | .data.getManyNew(options, iPath) | .getMany(iPath) |
| Read one | .data.getOne(options, iPath) | .getOne(iPath) / .getById(id, iPath) |
| Filter by status | flowDoc.statuses.getStatusByType('mutable') → status array | .statusMutable() |
| Filter by data-type | Manual field filter by slug-resolved field path | .dataType(slug, ...values) |
| Add view | Set naoQueryOptions.views[] | .withView('get-vendor') |
| Raw Mongo | Direct $and / $or injection | .query({ ... }) |
| Session | flowDoc.docs.createDbSession() | Automatic; .skipSession() to opt out |
| Insert | .data.insertOne(payload) / insertMany | .insertOne(payload) / .insertMany([...]) |
| Full update | .data.updateOne(...) | .updateOne(payload) / .updateMany(...) |
| Patch | .data.updatePatchOne(...) | .patchOne(payload) / .patchMany(...) |
| Status change | .data.updateStatus({ status }) | .setStatus('posted') |
| Soft delete | .data.deleteOne() | .deleteOne() |
| Count | .data.count() | .count() |
| Pagination | Manual limit + skip + count query | .paginate(page, size, iPath) |
| Field value helpers | flowDoc.dataTypes.getByIndex(...) | .statusRef(), .fieldDataType(), .slugDataType() |
| Atomic updates | flowData(...) field-level API | .mutate().set(...).updateOne() |
| Bulk | Legacy bulk transaction API | .bulk() — two-phase model |
Side-by-side examples
Reading many
Legacy:
const flowCollection = bob.flowGlobal.getFlowCollection('finance/bill')
const flowQuery = await flowCollection.docs.flowQuery()
.addPolicy('canUpdate')
.user(bob.flowUser)
.flowOptions({ docName: 'bill' })
const bills = await flowQuery.data.getManyNew({
options: {
query: { 'data.amount': { $gt: 1000 } },
sort: { 'data.dueDate': -1 },
limit: 50,
},
}, 'FinanceInterface.Bill')
New:
const bills = await bob.flowQuery('finance/bill')
.addPolicy('canUpdate')
.where('data.amount', 'gt', 1000)
.sortDesc('data.dueDate')
.limit(50)
.getMany('FinanceInterface.Bill')
Reading one by docId
Legacy:
const bill = await flowQuery.data.getOne(
{ options: { query: { docId: 'B00000048' } } },
'FinanceInterface.Bill',
)
New:
const bill = await bob.flowQuery('finance/bill')
.getById('B00000048', 'FinanceInterface.Bill')
Status filter
Legacy:
const mutable = flowDoc.statuses.getStatusByType('mutable').map(s => s.value)
const bills = await flowQuery.data.getManyNew({
options: { query: { status: { $in: mutable } } },
})
New:
const bills = await bob.flowQuery('finance/bill')
.statusMutable()
.getMany()
Data-type filter
Legacy:
// Resolve slug → field name manually:
const field = flowDoc.dataTypes.getBySlug('bill-payment-reconciliation-status').dataIndex
const overdueValue = 'overdue'
const bills = await flowQuery.data.getManyNew({
options: { query: { [field]: overdueValue } },
})
New:
const bills = await bob.flowQuery('finance/bill')
.dataType('bill-payment-reconciliation-status', 'overdue')
.getMany()
Inserting
Legacy:
await flowQuery.data.insertOne({ data: { amount: 1000, vendorId: 'V001' } })
New:
await bob.flowQuery('finance/bill')
.insertOne({ amount: 1000, vendorId: 'V001' })
Status transition
Legacy:
await flowQuery.data.updateStatus({ status: 'posted' })
New:
await bob.flowQuery('finance/bill').docId(docId).setStatus('posted')
Field-level atomic update (replacing flowData)
Legacy — flowData pattern:
await flowData.incrementField('data.lineCount', 1)
await flowData.pushToArray('data.tags', 'urgent')
await flowData.setField('data.amount', 5000)
await flowData.save()
New — mutate():
await bob.flowQuery('finance/bill')
.docId(docId)
.mutate()
.inc({ 'data.lineCount': 1 })
.push({ 'data.tags': 'urgent' })
.set({ 'data.amount': 5000 })
.updateOne()
Resolving a status value for a write payload
Legacy:
const posted = flowDoc.statuses.getByValue('posted')?.value
if (!posted) throw new Error(`Status 'posted' not defined`)
data.status = posted
New:
data.status = bob
.flowQuery('finance/journalItem')
.statusRef()
.value('posted')
// throws with a helpful message listing available statuses if 'posted' isn't registered
Resolving a data-type value for a write payload
Legacy:
const dt = flowDoc.dataTypes.getByIndex('ledgerAccessRightsDataTypeId')
const value = dt.data.find(d => d.value === 'user-managed')?.value
if (!value) throw new Error('…')
data.ledgerAccessRightsDataTypeId = value
New:
data.ledgerAccessRightsDataTypeId = bob
.flowQuery('finance/journalItem')
.fieldDataType('ledgerAccessRightsDataTypeId')
.value('user-managed')
Bulk
Legacy — single-phase:
const bulk = flowCollection.bulk()
bulk.insertOne(...)
bulk.updateOne(...)
await bulk.execute()
// Side effects (hooks, actions) had to be invoked manually after, with
// ad-hoc error handling.
New — two-phase:
const bulk = bob.flowQuery('vendors/vendor').bulk()
// Phase 1 — DB
bulk.add(bob.flowQuery('vendors/vendor').insert({ ... }))
bulk.add(bob.flowQuery('vendors/vendor').docId(id).mutate().set({ ... }).asUpdateOne())
// Phase 2 — side-effects (run after DB commits)
bulk.addHook(
bob.executeHook('notify-vendor-update').requestPayload({ ... }).skipSession(),
{ throwOnError: false },
)
const result = await bulk.execute({ concurrency: 10 })
Coexistence
Both APIs can live in the same hook. The legacy API still owns its
collections, its flowGlobal, and its session; the new API reads
the same definitions from the shared flowGlobal singleton, so
definitions don't have to be mirrored anywhere. Migrations can be
incremental — one query at a time — with no big-bang cutover.
When to migrate a given hook
Migrate when one of the following is true:
- You're touching the hook anyway (refactor, bug fix, new feature).
- The hook uses
flowData— the new.mutate()API is both safer (atomic) and more concise. - The hook builds raw
naoQueryOptions.queryobjects by hand — the new.where*()and.status*()helpers are typed and schema-aware. - The hook composes its own pagination with count queries — switch
to
.paginate().
Leave the rest alone until there's a reason to touch them. The legacy API is not deprecated.
Need help?
- Start with Overview for the pipeline model.
- References is the page to read if you have any hardcoded status strings or data-type strings in your codebase.
- Bulk changes the most from legacy — if your hook uses bulk heavily, read that page end-to-end before porting.
Back to the top: Overview ↑