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,138 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-plugin-export (wire-surreal-export-store)
**Package**: `@suaveplan/api-plugin-export`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: Export job store contract (`ExportJobStore`)
The package SHALL expose an `ExportJobStore` interface defining the persistence contract for export jobs. The interface SHALL declare async methods `get(id)`, `set(id, job)`, `delete(id)`, `list(query?)`, `has(id)`, and `clear()`. `list(query?)` SHALL accept an `ExportJobStoreListQuery` carrying optional `status`, `format`, `resource` filters and required `limit`, `offset` pagination, returning `{ items: ExportJob[]; total: number }`. The interface SHALL be runtime-free (a type-only export) so consumers can implement it without pulling in any backend dependency.
#### Scenario: interface members
- **WHEN** `ExportJobStore` is consumed as a TypeScript type
- **THEN** it declares the six async methods above with the documented signatures
**Verified by:** `src/store/memory/memory-store.test.ts::"MemoryExportJobStore satisfies the ExportJobStore contract"`
---
### Requirement: In-memory backend (`MemoryExportJobStore`)
`MemoryExportJobStore` SHALL implement `ExportJobStore` over an internal `Map<string, ExportJob>`. Every method SHALL resolve synchronously-derived results through a `Promise`. `list({ status, format, resource, limit, offset })` SHALL apply each provided filter as an AND-conjunction over the in-memory map values, then window with `offset`/`limit`, returning `total` as the pre-window match count.
#### Scenario: round-trip a job
- **WHEN** `set("a", job)` is called and then `get("a")`
- **THEN** the resolved job deep-equals the input
#### Scenario: list filter conjunction
- **WHEN** three jobs differ on `status` / `format` and the query is `{ status: "completed", format: "csv", limit: 10, offset: 0 }`
- **THEN** only the matching job appears in `items` and `total === 1`
#### Scenario: pagination
- **WHEN** five jobs are stored and the query is `{ limit: 2, offset: 2 }`
- **THEN** `items.length === 2` and `total === 5`
#### Scenario: clear drains the store
- **WHEN** `clear()` is called after jobs are stored
- **THEN** `list()` resolves with `{ items: [], total: 0 }`
**Verified by:** `src/store/memory/memory-store.test.ts::"set + get round-trips a job"` AND `src/store/memory/memory-store.test.ts::"list applies AND-conjunction filters"` AND `src/store/memory/memory-store.test.ts::"list paginates with offset and limit"` AND `src/store/memory/memory-store.test.ts::"clear empties the store"`
---
### Requirement: SurrealDB backend (`SurrealExportStore`)
`SurrealExportStore` SHALL implement `ExportJobStore` over a SurrealDB `export_job` table reached through `@suaveplan/surrealdb` `SurrealAdapter`. All SurQL SHALL use positional `$p0…$pN` placeholders (genesis-canonical pattern; the adapter rewrites positional arrays into named bindings). The constructor SHALL accept a Zod-validated `SurrealExportStoreConfig` carrying `db: SurrealAdapter` and an optional `tableName: string` defaulting to `"export_job"`. Every method SHALL wrap underlying driver / network / parse failures in `ExportError(EXPORT_CODES.JOB_PERSISTENCE_FAILED)` with the original error chained as `cause`. `set(id, job)` SHALL upsert the row (`UPDATE <tableName>:<id> CONTENT $p0 RETURN AFTER`). `delete(id)` SHALL be idempotent — deleting a missing row SHALL resolve without throwing. `list({ status, format, resource, limit, offset })` SHALL emit a `SELECT * FROM <tableName>` with an AND-conjunction `WHERE` clause and a sibling `SELECT count() FROM <tableName>` (also filtered) to derive `total` in a single round trip via `db.query(...)` returning two result sets.
#### Scenario: round-trip a job through SurrealDB
- **WHEN** `set("abc", job)` resolves and then `get("abc")` resolves
- **THEN** the resolved job carries the same `id`, `format`, `resource`, `status`, `createdAt` as the input
#### Scenario: list with filters
- **WHEN** three rows are seeded (two `completed`, one `failed`) and `list({ status: "completed", limit: 10, offset: 0 })` is called
- **THEN** `items.length === 2` and `total === 2`
#### Scenario: delete idempotency
- **WHEN** `delete("missing-id")` is called against an empty table
- **THEN** the call resolves with no error
#### Scenario: driver failure surfaces JOB_PERSISTENCE_FAILED
- **WHEN** the underlying adapter `query(...)` rejects (e.g. connection drop, schema mismatch)
- **THEN** the call rejects with `ExportError` whose `code === "EXPORT.JOB_PERSISTENCE_FAILED"` and whose `cause` is the underlying error
#### Scenario: invalid config rejects at construction time
- **WHEN** `new SurrealExportStore({})` is called without a `db` adapter
- **THEN** `ExportError` is thrown with `code === "EXPORT.INVALID_CONFIG"` and the `ZodError` is attached as `cause`
**Verified by:** `tests/integration/surreal-store.test.ts::"set + get round-trips a job through SurrealDB"` AND `tests/integration/surreal-store.test.ts::"list applies status filter and pagination through SurrealDB"` AND `tests/integration/surreal-store.test.ts::"delete on a missing row is idempotent"` AND `src/store/surreal/surreal-store.test.ts::"driver failure surfaces JOB_PERSISTENCE_FAILED"` AND `src/store/surreal/surreal-store.test.ts::"invalid config rejects with INVALID_CONFIG"`
---
### Requirement: `JOB_PERSISTENCE_FAILED` error code
`EXPORT_CODES` SHALL include the additional code `JOB_PERSISTENCE_FAILED`. The code SHALL be thrown from at least one production path — the `SurrealExportStore` driver-failure branch — and SHALL be pinned by `api-plugin-export.spec-conformance.test.ts`.
#### Scenario: registry membership
- **WHEN** `Object.values(EXPORT_CODES)` is enumerated
- **THEN** the result contains `"EXPORT.JOB_PERSISTENCE_FAILED"`
#### Scenario: conformance pin
- **WHEN** the spec-conformance suite walks the registry against `src/**/*.ts` non-test files
- **THEN** `JOB_PERSISTENCE_FAILED` is present in `src/store/surreal/surreal-store.ts`
**Verified by:** `src/error-codes/error-codes.test.ts::"JOB_PERSISTENCE_FAILED narrows via the public type union"` AND `src/api-plugin-export.spec-conformance.test.ts::"every EXPORT_* code is thrown from a production path"`
---
### Requirement: SurrealDB migration (`export_job.surql`)
The package SHALL ship `surql/tables/export_job.surql` defining the `export_job` table as `SCHEMAFULL` with one `DEFINE FIELD OVERWRITE` per `ExportJobSchema` property (`format`, `resource`, `status`, `filters`, `filename`, `total_rows`, `exported_rows`, `file_size`, `error`, `created_at`, `completed_at`) and indexes on `(status)`, `(format)`, `(resource)`, `(created_at)`. The migration SHALL be idempotent (`DEFINE TABLE OVERWRITE`, `DEFINE FIELD OVERWRITE`, `DEFINE INDEX OVERWRITE`) so consecutive boots succeed.
#### Scenario: migration applies cleanly
- **WHEN** the migration is loaded twice against a fresh SurrealDB testcontainer
- **THEN** both applies succeed without error
**Verified by:** `tests/integration/surreal-store.test.ts::"export_job migration is idempotent"`
---
## MODIFIED Requirements
### Requirement: Public surface and side-effect imports
The package root barrel SHALL import `./error-codes/error-codes.js` and `./services/registry-augmentation.js` for their side effects, and SHALL re-export every public symbol by name (no `export *` in any feature-module barrel). The barrel SHALL additionally re-export `ExportJobStore` (type), `MemoryExportJobStore`, `SurrealExportStore`, `SurrealExportStoreConfig` (type), and the `JOB_PERSISTENCE_FAILED` member of `EXPORT_CODES`. `package.json` SHALL declare `sideEffects: false` and ship subpath exports for `./plugin/plugin`, `./services/export/export-service`, `./routes/exports/exports`, `./exporters/csv/csv`, `./exporters/json/json`, `./exporters/xlsx/xlsx`, `./error-codes/error-codes`, `./otel/instruments`, `./types/types`, `./store/types/types`, `./store/memory/memory-store`, `./store/surreal/surreal-store`. The `files` array SHALL include `"surql"` so the migration ships to npm.
#### Scenario: side-effect imports present
- **WHEN** `src/index.ts` is read
- **THEN** the file contains both `./error-codes/error-codes.js` and `./services/registry-augmentation.js` import statements
#### Scenario: no `export *` in feature barrels
- **WHEN** `grep -rn "^\s*export\s\+\(type\s\+\)\?\*\s\+from" src/` is run against the package
- **THEN** no matches are found
#### Scenario: store subpath exports resolve
- **WHEN** a consumer imports from `@suaveplan/api-plugin-export/store/surreal/surreal-store`
- **THEN** the import resolves to a module exporting `SurrealExportStore`
**Verified by:** `src/api-plugin-export.spec-conformance.test.ts::"side-effect imports of error-codes and registry-augmentation are present in index.ts"` AND `src/api-plugin-export.spec-conformance.test.ts::"store subpath exports resolve from package.json"`