# 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`. 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` 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`.