Skip to main content

Letting AI Write Your FlowQuery — And Why You Still Need to Review It

· 3 min read
Gabriel Paunescu
Founder CTO Neologic

AI can chain FlowQuery calls with impressive accuracy — until it silently drops a tenant scope or builds an unindexed aggregation pipeline. Here's how to catch the mistakes that compile but corrupt.

The Premise

FlowQuery is Logic Bee's chainable ORM for tenant-scoped database operations. Its fluent API reads like pseudocode, which makes it a dream for AI code generation. The problem? Syntactically correct FlowQuery can still be semantically dangerous.

The Story

A developer asks the AI to build a reporting hook that aggregates monthly revenue across all posted invoices. The AI generates clean, readable code:

const invoices = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.invoiceNaoQueryOptions)
.query({ 'data.status': 'posted' })
.getMany(undefined, 'FinanceInterface.Invoice')

Looks perfect. But during code review, the developer catches three issues that would have gone unnoticed in a test environment.

Where AI FlowQuery Goes Wrong

1. Missing Tenant Boundaries

The most critical mistake is also the quietest. When AI generates a helper function that internally creates a new FlowQuery chain, it sometimes forgets to pass the user context:

// ❌ AI-generated helper — no user scoping
async function getLineItems(fc, docId) {
return fc.docs.flowQuery()
.query({ 'data.parentId': docId }) // missing .user()
.getMany()
}
// ✅ Corrected
async function getLineItems(fc, bob, docId) {
return fc.docs.flowQuery()
.user(bob.flowUser) // always scope to tenant
.flowOptions(eventOptions.lineNaoQueryOptions)
.query({ 'data.parentId': docId })
.getMany(undefined, 'FinanceInterface.InvoiceLine')
}

2. Fetching Too Much Data

AI tends to use .getMany() when .getOne() would suffice, or fetches full documents when only a few fields are needed. In production with thousands of documents, this matters:

// ❌ AI default: fetch everything
const allOrders = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.orderNaoQueryOptions)
.query({ 'data.customerId': customerId })
.getMany()

// ✅ Better: limit and project
const recentOrders = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.orderNaoQueryOptions)
.query({ 'data.customerId': customerId })
.sort({ 'data.createdAt': -1 })
.limit(10)
.getMany(undefined, 'SalesInterface.Order')

3. Aggregation Pipeline Misuse

When asked to "sum" or "group" data, AI sometimes fetches all documents into memory and reduces in JavaScript instead of using database-level aggregation:

// ❌ AI approach: fetch-then-reduce
const invoices = await fc.docs.flowQuery()...getMany()
const total = invoices.reduce((sum, inv) => sum + inv.data.amount, 0)

// ✅ Better: use the database
const pipeline = [
{ $match: { 'data.status': 'posted' } },
{ $group: { _id: null, total: { $sum: '$data.amount' } } }
]

The 3-Point FlowQuery Review

Every time you review AI-generated FlowQuery code, check these three things:

Scoping

  • .user(bob.flowUser) on every chain — including helper functions
  • .flowOptions(eventOptions.*) matches the correct collection

Performance

  • .getMany() has appropriate limits or pagination
  • Aggregations happen at the database level, not in JavaScript
  • Queries use indexed fields

Correctness

  • TypeScript interface passed to .getMany(undefined, 'Interface.Type') for type safety
  • .getOne() used when only one document is expected
  • Null checks after .getOne() calls

The Takeaway

FlowQuery's fluent API makes AI-generated code look deceptively correct. The developer's job isn't to rewrite it — it's to verify the three dimensions the AI consistently underweights: tenant scoping, query performance, and data volume awareness.

Trust the syntax. Verify the semantics.