Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
import { EXPORT_CODES, ExportError } from "../../error-codes/error-codes.js";
import { getExportInstruments } from "../../otel/instruments.js";
import {
getExporter,
inferColumns,
} from "../../services/export/export-service.js";
import type { ExportJobStore } from "../../store/types/types.js";
import type {
DataProvider,
ExportJob,
ExportOptions,
} from "../../types/types.js";
/**
* Dependencies for {@link runDownloadExport}. Mirrors the relevant slice
* of `ExportRoutesDeps` so the download handler can be unit-tested in
* isolation from the rest of the route group.
*/
export interface DownloadDeps {
/** Persistent store (preferred). When present, the handler reads
* the job through `jobStore.get(id)`. */
jobStore?: ExportJobStore | undefined;
/** In-memory fallback used when `jobStore` is omitted — the existing
* `Map<string, ExportJob>` slot shipped with the initial plugin. */
jobs: Map<string, ExportJob>;
/** Registered providers keyed by `resource`. */
providers: Map<string, DataProvider>;
}
/**
* Discriminated outcome of {@link runDownloadExport}. The route caller
* branches on `kind` and either streams `body` on the `ok` branch or
* passes the carried `ExportError` to the problem-details renderer.
*/
export type DownloadOutcome =
| {
kind: "ok";
body: string;
contentType: string;
filename: string;
format: ExportJob["format"];
}
| { kind: "not-found"; error: ExportError }
| { kind: "not-ready"; error: ExportError }
| { kind: "provider-not-found"; error: ExportError }
| { kind: "failed"; error: ExportError };
/**
* Resolve a job from either the persistent `jobStore` (preferred) or
* the in-memory `jobs` map fallback.
*/
async function resolveJob(
deps: DownloadDeps,
id: string
): Promise<ExportJob | null> {
if (deps.jobStore !== undefined) {
return deps.jobStore.get(id);
}
return deps.jobs.get(id) ?? null;
}
/**
* Implementation of `GET /:id/download`. Looks up the job, validates
* it is `completed`, re-fetches the source rows through the registered
* `DataProvider`, re-renders the body via the existing format writer,
* and returns a discriminated outcome. Every failure branch instantiates
* an `ExportError` so the OpenSpec error-code registry stays exercised
* from a production path.
*/
export async function runDownloadExport(
deps: DownloadDeps,
id: string
): Promise<DownloadOutcome> {
const instruments = getExportInstruments();
const job = await resolveJob(deps, id);
if (job === null) {
const err = new ExportError(
EXPORT_CODES.JOB_NOT_FOUND,
`Export job not found: ${id}`,
{ context: { id, statusCode: 404 } }
);
instruments.downloadFailed.add(1, { reason: "not-found" });
return { kind: "not-found", error: err };
}
if (job.status !== "completed") {
const err = new ExportError(
EXPORT_CODES.JOB_NOT_READY,
`Export job ${id} is not ready for download (status: ${job.status})`,
{ context: { id, status: job.status, statusCode: 409 } }
);
instruments.downloadFailed.add(1, { reason: "not-ready" });
return { kind: "not-ready", error: err };
}
const provider = deps.providers.get(job.resource);
if (provider === undefined) {
const err = new ExportError(
EXPORT_CODES.PROVIDER_NOT_FOUND,
`No data provider registered for resource: ${job.resource}`,
{
context: {
id,
resource: job.resource,
statusCode: 404,
},
}
);
instruments.downloadFailed.add(1, { reason: "provider-not-found" });
return { kind: "provider-not-found", error: err };
}
try {
const data = await provider.fetch(job.filters);
const columns = inferColumns(data);
const exporter = getExporter(job.format);
const options: ExportOptions = {};
const result = exporter.export(data, columns, options);
const filename = job.filename ?? result.filename;
instruments.downloadServed.add(1, { format: job.format });
return {
kind: "ok",
body: result.content,
contentType: result.mimeType,
filename,
format: job.format,
};
} catch (cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
const err = new ExportError(
EXPORT_CODES.PROVIDER_FETCH_FAILED,
`Download for job ${id} failed: ${detail}`,
{
cause,
context: {
id,
resource: job.resource,
statusCode: 502,
},
}
);
instruments.downloadFailed.add(1, { reason: "failed" });
return { kind: "failed", error: err };
}
}
/**
* Build the value of the `Content-Disposition` header for a download
* response. The filename is escaped per RFC 6266 §4.1 — every `"` is
* doubled, every `\` is doubled, and non-ASCII / control characters
* are replaced with `_`.
*/
export function buildContentDisposition(filename: string): string {
let out = "";
for (const ch of filename) {
// `ch` is a non-empty code-point, so `codePointAt(0)` is defined.
const code = ch.codePointAt(0) as number;
if (code < 0x20 || code === 0x7f) {
out += "_";
} else if (ch === '"' || ch === "\\") {
out += `\\${ch}`;
} else {
out += ch;
}
}
return `attachment; filename="${out}"`;
}