# 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`. 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 : 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 ` with an AND-conjunction `WHERE` clause and a sibling `SELECT count() FROM ` (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"`