Skip to main content

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

ConcernLegacyNew
Entry pointbob.flow.query('finance/bill')bob.flowQuery('finance/bill')
Get collectionflowGlobal.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 statusflowDoc.statuses.getStatusByType('mutable') → status array.statusMutable()
Filter by data-typeManual field filter by slug-resolved field path.dataType(slug, ...values)
Add viewSet naoQueryOptions.views[].withView('get-vendor')
Raw MongoDirect $and / $or injection.query({ ... })
SessionflowDoc.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()
PaginationManual limit + skip + count query.paginate(page, size, iPath)
Field value helpersflowDoc.dataTypes.getByIndex(...).statusRef(), .fieldDataType(), .slugDataType()
Atomic updatesflowData(...) field-level API.mutate().set(...).updateOne()
BulkLegacy 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.query objects 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 ↑