Files
edr-platform/apps/edr-freight-api/src/modules/exports/exports.module.ts
Nathnael 62f7b91315 feat(exports): dataset-driven table export, starting with bookings
Adds a parallel export system the reports module can also draw on. A dataset
describes a table's exportable fields — including related-entity detail the
list page never shows — and the engine assembles a query from whichever fields
the caller picked.

GET /exports                 catalog (metadata only; select/requires never ship)
GET /exports/:key/count      exact row count + per-format caps
GET /exports/:key/download   csv | xlsx | pdf

Two invariants carry the design:

- Every lazy join is a LEFT join, and ExportJoin has no 'kind' field to make
  anything else expressible. An inner join added because a checkbox was ticked
  would change the rowset, so two exports of the same filters would disagree on
  their row count.
- Because of that, the count cannot depend on field selection, so /count runs
  base + alwaysJoin only and is exact rather than an estimate. Verified: count
  and the delivered file both report 223 rows.

One-to-many relations (a booking's containers) aggregate in a correlated
subquery rather than joining, so a row can never multiply.

Export rides each dataset's existing view permission — no new permission keys
and no seeder change. Sensitive columns are simply never declared as fields:
raw gateway payloads, signature blobs, error dumps, raw jsonb snapshots,
internal user UUIDs and review notes are all absent by construction.

bookings ships 77 fields across 10 groups. scripts/validate-export-datasets.ts
EXPLAINs every dataset's widest query, its count query, and each field on its
own against the real database — the per-field pass is what catches a field
referencing a join it forgot to declare, which otherwise only fails when that
one field is picked alone.
2026-08-20 05:29:04 +00:00

24 lines
965 B
TypeScript

import { Module } from '@nestjs/common';
import { DocumentsModule } from '../billing/documents/documents.module';
import { UserTradeAccessModule } from '../user-trade-access/user-trade-access.module';
import { ExportRunnerService } from './export-runner.service';
import { ExportsController } from './exports.controller';
import { TabularExportService } from './tabular-export.service';
/**
* Generic table export: a dataset registry describing far more fields than each
* list page shows (related-entity detail included), plus the shared tabular
* writer (csv / xlsx / pdf) the reports module also writes through.
*
* `TabularExportService` is exported so ReportsModule can reuse it without
* pulling in the dataset machinery.
*/
@Module({
imports: [DocumentsModule, UserTradeAccessModule],
controllers: [ExportsController],
providers: [TabularExportService, ExportRunnerService],
exports: [TabularExportService],
})
export class ExportsModule {}