Files
2026-05-27 23:29:53 +01:00

2.9 KiB

Design — add-export-download-handler

Goal

Close the deferred GET /:id/download endpoint without expanding scope into §4 streaming / S3 / signed-URL work. The handler reads the persisted job, re-runs the registered DataProvider, re-renders through the existing writer, and streams the result back. Errors flow through a per-package RFC 7807 problem-details renderer so consumers branch on the existing EXPORT.* codes.

File layout

packages/api/api-plugin-export/src/
  error-codes/
    error-codes.ts                   ← add JOB_NOT_READY
  otel/
    instruments.ts                   ← add downloadServed / downloadFailed
  routes/
    download/
      download.ts                    ← runDownloadExport(deps, jobId)
      download.md
      download.test.ts
    exports/
      exports.ts                     ← extend deps + mount /:id/download
    problem-json/
      problem-json.ts                ← RFC 7807 renderer (per-package)
      problem-json.md
      problem-json.test.ts
  plugin/
    plugin.ts                        ← thread exportJobStore through to routes

Discriminated outcome

runDownloadExport(...) returns a union so the route registration call site stays free of c.json(...) typing. The four failure variants each carry the data the renderer needs:

type DownloadOutcome =
  | { kind: "ok"; body: string; contentType: string; filename: string; format: ExportFormat }
  | { kind: "not-found"; id: string }
  | { kind: "not-ready"; id: string; status: ExportStatus }
  | { kind: "provider-not-found"; resource: string }
  | { kind: "failed"; reason: string; cause: unknown };

Every branch throws / instantiates an ExportError so the spec-conformance pin keeps every EXPORT_* code reachable from a production path.

jobStore injection vs the in-memory jobs Map

ExportRoutesDeps.jobStore? is added as an optional dependency. When absent, the download handler builds an inline ExportJobStore adapter over deps.jobs so the route works in lightweight tests and host apps that opt out of SurrealDB. When present, the handler routes all reads through the store — consistent with the persistence narrative in 2026-05-19-wire-surreal-export-store.

OTel labels

  • export.download.served{format} — labelled with format: csv | json | xlsx.
  • export.download.failed{reason} — labelled with reason: not-found | not-ready | provider-not-found | failed.

Tests assert via flushTestMetrics() against the existing in-memory MeterProvider in tests/setup/polyfills.ts.

Status-code mapping

Outcome HTTP Code
ok 200 n/a
not-found 404 EXPORT.JOB_NOT_FOUND
not-ready 409 EXPORT.JOB_NOT_READY
provider-not-found 404 EXPORT.PROVIDER_NOT_FOUND
failed 502 EXPORT.PROVIDER_FETCH_FAILED

The mapping lives in the per-package problem-json renderer via context.statusCode attached at throw time.