Initial Commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# 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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user