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,100 @@
<!-- markdownlint-disable MD024 -->
# 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 "<id>" 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"`
<!-- markdownlint-enable MD024 -->