Files
suaveplan/openspec/archive/2026-05-19-wire-surreal-import-store/specs/api-plugin-import/spec.md
2026-05-27 23:29:53 +01:00

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 with record.id = "j1"
  • THEN the issued SurQL begins with CREATE import_job:j1 SET and the parameter array contains the record's ownerId, status, numeric counters, and timestamp strings in the order they appear in the SET clause

Scenario: get returns undefined when no row exists

  • WHEN surrealStore.get("missing") runs against an empty backend
  • THEN the call resolves to undefined and import.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 Error whose message contains Job "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 $p3 and the parameter array is ["completed", "alice", 10, 5]

Scenario: adapter failure surfaces as STORAGE_ERROR

  • WHEN the underlying SurrealAdapter.query throws
  • THEN the store's call rejects with an ImportError whose code === IMPORT_CODES.STORAGE_ERROR and cause is the original throwable

Scenario: invalid config throws INVALID_OPTIONS

  • WHEN new SurrealImportStore({ db: adapter, table: "" }) runs
  • THEN it throws ImportError with code === IMPORT_CODES.INVALID_OPTIONS and cause 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 a status = '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 get after delete returns undefined

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 storeWritesTotal and storeReadsTotal Counter 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 exports map is enumerated and the new barrel is read
  • THEN the map contains ./store/surreal/surreal-import-store and the barrel contains no export * lines

Verified by: src/api-plugin-import.spec-conformance.test.ts::"package exposes a subpath export for the SurrealDB store"