8.9 KiB
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
ExportJobStoreis 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 thenget("a") - THEN the resolved job deep-equals the input
Scenario: list filter conjunction
- WHEN three jobs differ on
status/formatand the query is{ status: "completed", format: "csv", limit: 10, offset: 0 } - THEN only the matching job appears in
itemsandtotal === 1
Scenario: pagination
- WHEN five jobs are stored and the query is
{ limit: 2, offset: 2 } - THEN
items.length === 2andtotal === 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 thenget("abc")resolves - THEN the resolved job carries the same
id,format,resource,status,createdAtas the input
Scenario: list with filters
- WHEN three rows are seeded (two
completed, onefailed) andlist({ status: "completed", limit: 10, offset: 0 })is called - THEN
items.length === 2andtotal === 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
ExportErrorwhosecode === "EXPORT.JOB_PERSISTENCE_FAILED"and whosecauseis the underlying error
Scenario: invalid config rejects at construction time
- WHEN
new SurrealExportStore({})is called without adbadapter - THEN
ExportErroris thrown withcode === "EXPORT.INVALID_CONFIG"and theZodErroris attached ascause
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/**/*.tsnon-test files - THEN
JOB_PERSISTENCE_FAILEDis present insrc/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.tsis read - THEN the file contains both
./error-codes/error-codes.jsand./services/registry-augmentation.jsimport 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"