@ftisindia/report-builder
v0.2.0
Published
Backend-first dynamic report engine for the FTIS foundation starter: versioned JSON definitions over registered data sources, an allowlist-only query compiler with keyset pagination, publish-time index/plan linting, delegated row actions with drift-safe b
Readme
@ftisindia/report-builder
Backend-first dynamic report engine for the FTIS foundation starter
(@ftisindia/create-app). A report is a declared, versioned view over one
registered data source, plus the verbs users may apply to it — filter, sort,
search, paginate, export, and act on rows. Definitions are versioned JSON
documents stored in your database; the behaviors they reference — sources and
row actions — are code your app registers. The engine compiles every grid
interaction from one declarative QuerySpec into safe, parameterized,
keyset-paginated SQL whose index backing was checked against the catalog and
the planner at publish time — all behind framework-free ports.
This package requires an FTIS starter app. It is the engine core only: it compiles and tests without NestJS, Prisma, CASL — and without
@ftisindia/form-builder(the standalone guarantee: form-backed sources are an optional adapter in the app's glue module, never a core dependency). It is wired into a generated app by thesrc/modules/reportsglue module that@ftisindia/create-appscaffolds. It is not usable standalone.
What's inside
- Definition format + meta-schema —
ReportDefinition, linted at save and publish time. The SQL boundary rule is structural: definitions reference data only symbolically (path/columnId); SQL expressions exist only in code-ownedSourceManifests, and SQL-shaped strings in definitions are rejected. - The query compiler — allowlist-only identifier resolution, typed
per-column operators (
containson a number column is a 400, not a seq scan), org/rowScope predicates injected first, keyset-only pagination (row-value form for uniform direction, the OR-ladder for mixed, null surrogates, pinned collations), HMAC-signed cursors bound to (report, version, sort, filters), corrected count strategies (none / estimated / exact-capped with the subquery LIMIT shape), and trgm/tsvector search compilation. - Publish-time performance lint — declared indexes verified against the
catalog (expression, operator class, collation, org-leading order), EXPLAIN
run on representative shapes with plan hashes recorded, per-tier gates
(live row cap / indexed catalog+plan / materialized relation + staleness).
Failure is constructive: the error carries the exact migration SQL —
generated columns with hashed
rb_*names, immutable expressions (datetimes stay ISO-8601 UTC text +COLLATE "C"), andCREATE INDEXstatements. - Row actions — verbs that DELEGATE to the owning domain, never a second write path. Attach-time + execute-time permission checks, the byFilter prepare/execute token protocol with drift detection, an idempotency ledger, and keyset-ordered transactional batches.
- Tags & labels — org-scoped row annotations the report layer owns, with
the
$tagsvirtual column (hasTag/hasAnyTagcompile to indexed EXISTS) and a curated-vocabulary seam. - Exports — streaming always (CSV and a dependency-light OOXML/XLSX
writer), snapshot-consistent (one REPEATABLE READ snapshot per export,
duration-bounded), sync/async split by actual size,
exportable: falsestripping, and audit-as-PII-egress hooks. - Saved views — version-stamped specs with
ok | degraded | incompatiblecompatibility verdicts instead of silent 500s after a new publish. - Ports & seams —
ReportDefinitionStore,SavedViewStore,ExportJobStore,BulkActionRunStore,RowTagStore,QueryExecutor,SnapshotRunner,CatalogPort,ReportsJobQueue,ExportFileSink,ResultCache;ReportsContext,ReportsAuthorization,ReportsAuditSink,TxRunner. The app's glue module implements these over its own services. prisma/reports.prisma— canonical model snippet. Your app ownsschema.prismaand migrations; copy the snippet in and migrate. The glue module's boot check verifies the database matchesREPORTS_ENGINE_SCHEMA_VERSION— including the two partial unique indexes Prisma cannot express.
Boundary contract
The core never imports NestJS, Prisma, CASL, class-validator, the FTIS
template, or @ftisindia/form-builder — enforced by this package's eslint
config and proven in CI by building and testing the package in isolation.
Engine errors are engine-typed (ReportSpecError, ReportCursorError,
ReportTokenError, ReportLintError, ReportQueryBudgetError,
ReportDriftError, …) and mapped to HTTP responses only in the app's glue.
License
PolyForm Noncommercial 1.0.0 — free for any noncommercial purpose; commercial use requires a separate license from ftisindia.com.
