7.8 KiB
Capability Spec Delta: api-plugin-import (wire-surreal-import-store)
Package: @suaveplan/api-plugin-import
Status: PROPOSED
Every requirement below carries a
**Verified by:**line authored at draft time.
ADDED Requirements
Requirement: SurrealImportStore — durable ImportStoreAdapter
The package SHALL ship SurrealImportStore implements ImportStoreAdapter at src/store/surreal/surreal-import-store.ts, providing the same create / get / update / delete / list surface as MemoryImportStore against a SurrealAdapter backend from @suaveplan/surrealdb. All SurrealQL placeholders SHALL be positional ($p0, $p1, …) per the canonical pattern in @suaveplan/api-plugin-events. The constructor SHALL accept a SurrealImportStoreConfig validated through Zod at the boundary; an invalid config SHALL throw ImportError with code === IMPORT_CODES.INVALID_OPTIONS and cause instanceof ZodError. The store SHALL accept a custom table name (default "import_job"). create SHALL upsert the record by stringified id; update on a missing id SHALL throw an error whose message contains Job "<id>" not found (parity with MemoryImportStore). Adapter-layer failures SHALL be wrapped as ImportError(IMPORT_CODES.STORAGE_ERROR) with the underlying error as cause. The store SHALL increment import.store.writes_total{outcome:"success"|"error"} on every create / update / delete, and import.store.reads_total{outcome:"success"|"error"} on every get / list.
Scenario: create emits a CREATE SET statement with positional params
- WHEN
surrealStore.create(record)runs withrecord.id = "j1" - THEN the issued SurQL begins with
CREATE import_job:j1 SETand the parameter array contains the record'sownerId,status, numeric counters, and timestamp strings in the order they appear in theSETclause
Scenario: get returns undefined when no row exists
- WHEN
surrealStore.get("missing")runs against an empty backend - THEN the call resolves to
undefinedandimport.store.reads_total{outcome:"success"}is incremented (a miss is still a successful read)
Scenario: update on missing id throws Job "" not found
- WHEN
surrealStore.update("missing", { status: "completed" })runs on a store with no record"missing" - THEN the call rejects with an
Errorwhose message containsJob "missing" not found
Scenario: list filters by status, ownerId, limit, offset
- WHEN
surrealStore.list({ status: "completed", ownerId: "alice", limit: 10, offset: 5 })runs - THEN the SurQL contains
WHERE status = $p0 AND owner_id = $p1,LIMIT $p2,START $p3and the parameter array is["completed", "alice", 10, 5]
Scenario: adapter failure surfaces as STORAGE_ERROR
- WHEN the underlying
SurrealAdapter.querythrows - THEN the store's call rejects with an
ImportErrorwhosecode === IMPORT_CODES.STORAGE_ERRORandcauseis the original throwable
Scenario: invalid config throws INVALID_OPTIONS
- WHEN
new SurrealImportStore({ db: adapter, table: "" })runs - THEN it throws
ImportErrorwithcode === IMPORT_CODES.INVALID_OPTIONSandcause instanceof ZodError
Verified by: src/store/surreal/surreal-import-store.test.ts::"create issues a CREATE SET statement with positional params" AND src/store/surreal/surreal-import-store.test.ts::"get returns undefined when no row exists" AND src/store/surreal/surreal-import-store.test.ts::"update on missing id throws Job not found" AND src/store/surreal/surreal-import-store.test.ts::"list filters by status, ownerId, limit, offset" AND src/store/surreal/surreal-import-store.test.ts::"adapter failure surfaces as STORAGE_ERROR" AND src/store/surreal/surreal-import-store.test.ts::"invalid config throws INVALID_OPTIONS"
Requirement: SurrealDB schema migration
The package SHALL ship surql/tables/import_job.surql defining the import_job table SCHEMAFULL with fields owner_id (string), status (string) constrained to the ImportJobStatus enum, total_rows (int), processed_rows (int), success_count (int), error_count (int), errors (array), metadata (option<object>), created_at (datetime), updated_at (datetime), completed_at (option<datetime>), plus an index on owner_id. The migration SHALL use the canonical DEFINE TABLE OVERWRITE / DEFINE FIELD OVERWRITE form mirroring packages/api/api-plugin-events/surql/tables/event.surql. Hosts MAY apply the migration via @suaveplan/api-plugin-database's migration runner or directly via adapter.query(readFileSync(...)).
Scenario: migration declares the canonical schema
- WHEN the integration test reads and applies
surql/tables/import_job.surql - THEN the SurrealDB instance subsequently accepts a
CREATE import_job:j1 SET owner_id = 'alice', status = 'created', …statement and rejects astatus = 'bogus'write
Verified by: tests/integration/surreal-import-store.test.ts::"migration enables canonical writes and rejects invalid status"
Requirement: Integration round-trip against a real SurrealDB instance
The package SHALL ship tests/integration/surreal-import-store.test.ts using SurrealDBFixture from @suaveplan/testing-integration-surrealdb. The test SHALL apply the .surql migration once in beforeAll, then create a record, get it back, update it, list with a status filter, and delete it — all against the real container. The integration test SHALL also assert that update("missing", …) rejects with the Job "missing" not found message and that the errors array round-trips intact.
Scenario: full round-trip against testcontainer
- WHEN the integration test runs against a started
SurrealDBFixture - THEN each store operation completes successfully and the final
getafterdeletereturnsundefined
Verified by: tests/integration/surreal-import-store.test.ts::"round-trips a record through the real SurrealDB instance" AND tests/integration/surreal-import-store.test.ts::"update on missing id surfaces the canonical message against a real instance"
Requirement: OTel store counters
The package SHALL extend getImportInstruments() to expose two additional counters — import.store.writes_total and import.store.reads_total — both labelled by outcome. The counter pair SHALL be cached on the same bundle as the existing four instruments and SHALL rebind through resetImportInstruments(). The existing four-instrument scenarios SHALL continue to pass (additive extension, no breaking shape change). SurrealImportStore SHALL emit on every adapter call; MemoryImportStore is out of scope for this requirement (it is an in-process test/dev adapter without observable backend health).
Scenario: bundle exposes the two new counters
- WHEN a caller invokes
getImportInstruments() - THEN the returned object exposes
storeWritesTotalandstoreReadsTotalCounter instruments in addition to the existing four
Verified by: src/otel/instruments.test.ts::"bundle exposes the store write and read counters"
Requirement: Subpath export for the SurrealDB store
The package SHALL declare the subpath export ./store/surreal/surreal-import-store in package.json#exports so consumers can tree-shake the SurrealDB store without pulling in the in-memory implementation. The src/store/surreal/index.ts barrel SHALL use explicit named re-exports (no export *).
Scenario: subpath export exists and the barrel uses named exports
- WHEN the package's
exportsmap is enumerated and the new barrel is read - THEN the map contains
./store/surreal/surreal-import-storeand the barrel contains noexport *lines
Verified by: src/api-plugin-import.spec-conformance.test.ts::"package exposes a subpath export for the SurrealDB store"