9.2 KiB
Ship @suaveplan/api-plugin-export
Package: @suaveplan/api-plugin-export
Status: PROPOSED
Type: new-package
Why
The genesis monorepo needs a data-export plugin for @suaveplan/api that turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob. The upstream package at ../suaveplan/packages/server/api-plugin-export/ (v1.0.5) ships three self-contained exporter classes, a DataProvider registry, a job-lifecycle store, and an OpenAPIHono route group at /exports. This change rebuilds that surface at genesis-quality (SuaveplanError codes, OTel instruments asserted with a real MeterProvider, 100 % istanbul coverage, sibling .md for every implementation file, no mocks, no vi.mock, no export * in feature barrels).
What changes
- ADDED:
createExportPlugin(options?)— returns aPluginDescriptorhonouring caller-supplied options (the upstream_config-discard bug is fixed at migration time). - ADDED:
CsvExporter,JsonExporter,XlsxExporter— three RFC-4180 / JSON / SpreadsheetML implementations of theExporterinterface. - ADDED:
getExporter(format),inferColumns(data),runExport(...),DEFAULT_EXPORT_CONFIG— service-layer helpers. - ADDED:
createExportRoutes(deps)—OpenAPIHonowithGET /,POST /,GET /:id,DELETE /:id. - ADDED:
registerDataProvider(providers, provider)— providers-map helper. - ADDED:
EXPORT_CODES+ExportErrorextendingSuaveplanError— replaces the rawnew Error(...)throws from upstreamexport.service.ts. - ADDED:
ExportInstruments+getExportInstruments()+resetExportInstruments()— OTelCounter+Histogrambundle. - ADDED: Zod schemas
ExportFormatSchema,ExportStatusSchema,ColumnDefinitionSchema,CreateExportJobSchema,ExportJobSchema,ExportJobListQuerySchema,ExportPluginOptionsSchema. - ADDED:
ServiceRegistryslotsexportProvidersandexportJobsviadeclare module "@suaveplan/api-plugin/service-registry".
Out of scope
This change ships the upstream §2 “Current-state assessment” surface only. The upstream enterprise/api-plugin-export.md §3–§4 enhancements (streaming exporters, S3-backed blob store, auth/permissions/rate-limit/idempotency middleware, OOXML/ZIP writer, scheduled exports, signed download URLs, Parquet/Markdown formats, benchmark harness) are explicitly deferred — they are listed as “feature-rich enhancements” in the source spec, not as part of the current functional surface, and each requires a downstream plugin that has not yet landed on genesis (e.g. @suaveplan/data-masking, @suaveplan/storage-s3, @suaveplan/api-plugin-rate-limit consumer wiring, @suaveplan/queue).
Excluded from this migration
| Upstream symbol / behaviour | Justification |
|---|---|
ctx.app.route(basePath, routes) mount in onReady |
Genesis PluginContext has no app field; routes are exposed via ctx.getService("app") in onReady (same pattern as @suaveplan/api-plugin-audit). Public route surface is preserved. |
ctx.eventBus.emit("export:initialized" / "export:ready" / "export:started" / "export:completed") |
Genesis EventBusLike is structural and does not declare these event keys in ServiceRegistry. Replaced with structured logger calls + OTel counter; the onExportStarted / onExportCompleted callback hooks remain on createExportRoutes deps so consumers can wire any bus they want. |
declare module "@suaveplan/api" { interface EventMap { ... } } |
Same reason as above — no EventMap interface on genesis @suaveplan/api. |
GET /:id/download streaming endpoint |
Upstream enterprise/api-plugin-export.md §2 "Smells" notes the response uses downloadUrl: "/{jobId}/download" but no /:id/download handler exists in routes/exports.ts. The endpoint is part of §3 "Required enterprise-quality improvements" and §4 "Streaming exporters" — deferred to follow-up. The downloadUrl field is retained for forward compatibility. |
| §3 "Job persistence" via SurrealDB | Upstream §2 ships the in-memory Map<string, ExportJob>. SurrealDB persistence is §3 work and requires surql/ migrations + integration tests against @suaveplan/api-plugin-database. Deferred. |
| §4 streaming / S3 / OOXML / auth / rate-limit / idempotency / audit / signed URLs / Parquet / Markdown / scheduled exports / benchmarks | All listed as “feature-rich enhancements” in upstream §4. Deferred per Out-of-scope above. |
Source mapping
This change derives from ../suaveplan/enterprise/api-plugin-export.md and the upstream source under ../suaveplan/packages/server/api-plugin-export/src/.
| Requirement | Source lines | Notes |
|---|---|---|
Plugin name "export" v1.0.0, optionalDependencies: ["database", "events"], tags: ["export","data","csv","json","xlsx"] |
enterprise/api-plugin-export.md L18 + src/index.ts:44-50 |
verbatim |
Config schema basePath / maxRows (1–1 000 000) / maxFileSizeBytes (≥ 1 KiB, default 50 MiB) / defaultFormat (csv|json|xlsx) |
enterprise/api-plugin-export.md L18 + src/index.ts:21-30 |
verbatim |
Honour factory-time config — merge caller-supplied options with getOwnConfig() |
enterprise/api-plugin-export.md L34 + L43 |
upstream bug fixed at migration time |
Three exporter classes: CsvExporter, JsonExporter, XlsxExporter implementing Exporter |
enterprise/api-plugin-export.md L17 + src/exporters/{csv,json,xlsx}/*.ts |
verbatim |
RFC 4180 CSV: configurable delimiter, optional headers, quote-escaping, ISO-8601 dates, text/csv MIME |
enterprise/api-plugin-export.md L26 + src/exporters/csv/csv.exporter.ts |
verbatim |
JSON: column projection + optional pretty-print, application/json MIME |
enterprise/api-plugin-export.md L27 + src/exporters/json/json.exporter.ts |
verbatim |
| XLSX (SpreadsheetML XML): configurable sheet name, header row, OOXML MIME, A→Z→AA column letters | enterprise/api-plugin-export.md L28 + src/exporters/xlsx/xlsx.exporter.ts |
verbatim |
Service helpers: getExporter, inferColumns, runExport, DEFAULT_EXPORT_CONFIG with row/size guards |
enterprise/api-plugin-export.md L24 + src/services/export/export.service.ts |
verbatim, with ExportError replacing raw Error |
Routes: GET /, POST /, GET /:id, DELETE /:id on OpenAPIHono |
enterprise/api-plugin-export.md L25 + src/routes/exports/exports.ts |
verbatim (download endpoint excluded — see above) |
In-memory Map<string, ExportJob> job store with pending|processing|completed|failed FSM |
enterprise/api-plugin-export.md L25 + L31 + src/routes/exports/exports.ts:140-213 |
verbatim |
Zod boundaries on every public option bundle (CreateExportJobSchema, ExportJobListQuerySchema, ExportJobSchema) |
enterprise/api-plugin-export.md L23 + src/types.ts:32-87 |
verbatim |
registerDataProvider(providers, provider) helper |
src/index.ts:109-114 |
verbatim |
Requirements with no source line
| Requirement | Justification |
|---|---|
EXPORT_CODES + ExportError extends SuaveplanError |
Genesis-wide policy (CLAUDE.md §“Error hierarchy”). Upstream throws raw Error. |
ExportInstruments + getExportInstruments + resetExportInstruments with export.jobs_total{outcome}, export.rows_total, export.bytes_total, export.duration_ms |
Genesis-wide OTel policy. Upstream has no OTel. |
ServiceRegistry slots exportProviders / exportJobs via declare module |
Required because genesis PluginContext exposes services only via the augmented registry — there is no upstream equivalent because the upstream PluginContext is structurally different (has ctx.app, ctx.eventBus.emit(K, payload), etc.). |
100 % istanbul coverage; sibling .md ≥ 200 words per impl file |
Genesis-wide policy. |
spec-conformance.test.ts pinning every EXPORT_* code to ≥ 1 production throw site |
Genesis policy (§13). |
Verification approach
src/error-codes/error-codes.test.ts— everyEXPORT_*code constructs anExportErrorwith the right narrowed code.src/exporters/csv/csv.test.ts— RFC 4180 quoting / delimiter / header / ISO-8601 date round-trip.src/exporters/json/json.test.ts— projection, pretty-print, transform application.src/exporters/xlsx/xlsx.test.ts— sheet name, header row, column letter math A→Z→AA, escaping.src/services/export/export-service.test.ts—getExporter,inferColumns,runExportrow + size guards (throwExportError).src/routes/exports/exports.test.ts—GET /filter+pagination,POST /happy + 404 + 400 (row cap) + 400 (size cap) + 400 (provider error),GET /:id+ 404,DELETE /:id+ 404.src/plugin/plugin.test.ts— Zod validation rejects bad options;onInitregistersexportProviders+exportJobs;onReadyhonours caller-supplied options and mounts routes viactx.getService("app");onCloseclears both maps.src/otel/instruments.test.ts— emits all four instruments through a real in-memoryMeterProvider.src/api-plugin-export.spec-conformance.test.ts— everyEXPORT_*code is thrown from a non-test file; side-effect imports oferror-codesandregistry-augmentationare present inindex.ts.
Migration impact
None — additive only. No genesis package currently consumes @suaveplan/api-plugin-export.