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 withformat: csv | json | xlsx.export.download.failed{reason}— labelled withreason: 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.