Files
suaveplan/openspec/archive/2026-05-19-add-api-plugin-export/proposal.md
2026-05-27 23:29:53 +01:00

9.2 KiB
Raw Blame History

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 a PluginDescriptor honouring caller-supplied options (the upstream _config-discard bug is fixed at migration time).
  • ADDED: CsvExporter, JsonExporter, XlsxExporter — three RFC-4180 / JSON / SpreadsheetML implementations of the Exporter interface.
  • ADDED: getExporter(format), inferColumns(data), runExport(...), DEFAULT_EXPORT_CONFIG — service-layer helpers.
  • ADDED: createExportRoutes(deps) — OpenAPIHono with GET /, POST /, GET /:id, DELETE /:id.
  • ADDED: registerDataProvider(providers, provider) — providers-map helper.
  • ADDED: EXPORT_CODES + ExportError extending SuaveplanError — replaces the raw new Error(...) throws from upstream export.service.ts.
  • ADDED: ExportInstruments + getExportInstruments() + resetExportInstruments() — OTel Counter + Histogram bundle.
  • ADDED: Zod schemas ExportFormatSchema, ExportStatusSchema, ColumnDefinitionSchema, CreateExportJobSchema, ExportJobSchema, ExportJobListQuerySchema, ExportPluginOptionsSchema.
  • ADDED: ServiceRegistry slots exportProviders and exportJobs via declare 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 — every EXPORT_* code constructs an ExportError with 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, runExport row + size guards (throw ExportError).
  • 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; onInit registers exportProviders + exportJobs; onReady honours caller-supplied options and mounts routes via ctx.getService("app"); onClose clears both maps.
  • src/otel/instruments.test.ts — emits all four instruments through a real in-memory MeterProvider.
  • src/api-plugin-export.spec-conformance.test.ts — every EXPORT_* code is thrown from a non-test file; side-effect imports of error-codes and registry-augmentation are present in index.ts.

Migration impact

None — additive only. No genesis package currently consumes @suaveplan/api-plugin-export.