first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# `export-service.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Service-level orchestration for the export plugin. This module wires the three exporter classes (`CsvExporter`, `JsonExporter`, `XlsxExporter`) behind a single `getExporter(format)` dispatcher, derives default column definitions from raw data via `inferColumns(data)`, and enforces the row + byte-size guards in `runExport(...)`. All guard failures throw `ExportError` carrying one of the four service-level codes (`UNSUPPORTED_FORMAT`, `ROW_LIMIT_EXCEEDED`, `FILE_SIZE_EXCEEDED`, `PROVIDER_FETCH_FAILED`) rather than the raw `Error` upstream used.
## Features
- `getExporter(format)` returns a fresh `Exporter` instance per call — no shared state across exports.
- `inferColumns(data)` collapses heterogeneous row keys into a stable column order (first-seen).
- `runExport(provider, format, columns?, filters?, options?, config?)` is the single async path used by the route handler — every guard lives here.
- `DEFAULT_EXPORT_CONFIG` captures the upstream defaults (50 000 rows, 50 MiB).
- Failures from `provider.fetch` are wrapped in `ExportError(PROVIDER_FETCH_FAILED)` with the original error preserved as `cause`.
## Usage: Basic
```ts
import {
runExport,
DEFAULT_EXPORT_CONFIG,
type DataProvider,
} from "@suaveplan/api-plugin-export";
const provider: DataProvider = {
resource: "users",
fetch: async () => [{ id: 1 }],
};
const result = await runExport(provider, "csv", undefined, undefined, undefined, DEFAULT_EXPORT_CONFIG);
console.log(result.content);
```
## Usage: Advanced
```ts
import {
runExport,
type ExportServiceConfig,
} from "@suaveplan/api-plugin-export";
const config: ExportServiceConfig = {
maxRows: 1_000,
maxFileSizeBytes: 1024 * 1024,
defaultFormat: "csv",
};
try {
const result = await runExport(
myProvider,
"json",
[{ key: "id", header: "ID" }],
{ active: true },
{ pretty: true },
config
);
// …
} catch (err) {
if (err instanceof ExportError && err.code === "EXPORT.ROW_LIMIT_EXCEEDED") {
// tell the user to narrow their filters
}
throw err;
}
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `ExportServiceConfig` | interface | `{ maxRows, maxFileSizeBytes, defaultFormat }`. |
| `DEFAULT_EXPORT_CONFIG` | const | Upstream defaults: 50 000 rows, 50 MiB, `csv`. |
| `getExporter(format)` | function | Throws `EXPORT.UNSUPPORTED_FORMAT` on unknown. |
| `inferColumns(data)` | function | Returns `[]` on empty data. |
| `runExport(provider, format, columns?, filters?, options?, config?)` | async function | Throws `EXPORT.*` on guard breach. |
## Implementation Notes
The two guards run in a specific order: row count is checked *before* the exporter runs so a runaway provider doesn't burn CPU encoding fields the route layer is about to reject. File size is checked *after* encoding because cell-level transforms and XML/JSON overhead can push a "small" row count over the byte cap. The `default:` branch in `getExporter` is reachable only if a caller bypasses the Zod boundary (the schema enumerates `csv|json|xlsx`); the `format as string` cast in the error message is intentional — we want the actual value the caller passed to surface in logs, not the post-narrowing `never`. `provider.fetch` errors are wrapped rather than thrown raw because the route layer needs to distinguish a *bad provider* (502-class) from a *bad request* (400-class), and the `ExportError` code is the contract that drives that branching upstream.

View File

@@ -0,0 +1,44 @@
# `registry-augmentation.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This file is a type-only `declare module "@suaveplan/api-plugin/service-registry"` block. It adds two slots to the shared `ServiceRegistry` interface so downstream plugins can resolve `ctx.getService("exportProviders")` and `ctx.getService("exportJobs")` with full typing — `Map<string, DataProvider>` and `Map<string, ExportJob>` respectively. The file produces no emitted runtime code; the only side effect is the type augmentation, which is why `src/index.ts` imports it for its side effects (so consumers using the package barrel pick up the slot types automatically).
## Features
- Adds `exportProviders: Map<string, DataProvider>` to `ServiceRegistry`.
- Adds `exportJobs: Map<string, ExportJob>` to `ServiceRegistry`.
- No runtime cost — the file emits no JavaScript.
## Usage: Basic
```ts
import "@suaveplan/api-plugin-export"; // pulls the augmentation through the barrel
// Now ctx.getService("exportProviders") is typed Map<string, DataProvider> | undefined
```
## Usage: Advanced
```ts
import "@suaveplan/api-plugin-export/services/registry-augmentation";
// In a downstream plugin's onInit:
const providers = ctx.getService("exportProviders");
if (!providers) throw new Error("export plugin not initialised");
providers.set("orders", myOrderProvider);
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `ServiceRegistry.exportProviders` | augmentation | `Map<string, DataProvider>`. |
| `ServiceRegistry.exportJobs` | augmentation | `Map<string, ExportJob>`. |
## Implementation Notes
The augmentation pattern is identical to `@suaveplan/api-plugin-database`'s `registry-augmentation.ts` — both files exist purely to add slots to the shared registry interface defined by `@suaveplan/api-plugin/service-registry`. Without this file, every call site would have to cast the `getService("exportProviders")` return value, defeating the type safety that the `ServiceRegistry` interface was designed to provide. The two slots cover the two distinct concerns the plugin owns: the *registry* of data providers (writable by every consumer that wants to expose a resource for export) and the *store* of jobs (mutated by the route layer, observable by anyone tracking export progress). Exposing both is a deliberate departure from upstream, which exposed them via `ctx.registerService(...)` at runtime but never declared the corresponding types.