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

82 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.