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,279 @@
# OpenSpec Change: enterprise-surrealdb
- **Change ID**: enterprise-surrealdb
- **Package**: `@suaveplan/surrealdb`
- **Category**: universal
- **Target path**: `universal/database/surrealdb`
- **Status**: PROPOSED
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/surrealdb.md`
---
## Summary
Deliver `@suaveplan/surrealdb` on the genesis branch from scratch at enterprise quality. The package provides a universal SurrealDB client wrapper with automatic version detection (v1/v2/v3), connection lifecycle management (connect/disconnect/singleton registry), schema discovery and loading, a migration runner, and a pluggable adapter layer that normalises the raw `surrealdb.js` driver API into a stable typed surface. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for every public options type, OTel instrumentation on all async operations, `AbortSignal` cancellation throughout, retry integration via `@suaveplan/retry`, property-based tests (fast-check), and a mitata benchmark suite. The SurrealDB-specific migration runner (internal to this package in main) is split out to `@suaveplan/migration` on genesis; only the connection + adapter surface lives here.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | Base class for `SurrealError`; `defineCodes` for `SURREAL.*` codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` (injectable for timeout helpers) |
| `@suaveplan/db` (`workspace:^`) | `DatabaseAdapter` interface, singleton registry helpers (`getSingleton`/`closeSingleton`) |
| `@suaveplan/retry` (`workspace:^`) | `RetryStrategy`, `withRetry` — wired on connect and query paths |
| `surrealdb` (`catalog:database`) | Raw SurrealDB JS driver |
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on connect, query, disconnect |
| `tslib` (`catalog:build`) | Runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`).
---
## Preserved public API surface (from main branch)
All symbols below are carried forward; signatures may gain optional genesis-only fields but existing call sites remain valid.
### Connection
```ts
interface SurrealDBConfig {
url: string
namespace: string
database: string
auth: { username: string; password: string }
}
function getDefaultConfig(): SurrealDBConfig
function createConnection(config?: Partial<SurrealDBConfig>): Promise<Surreal>
function connectWith(config?: Partial<SurrealDBConfig>): Promise<Surreal>
function getSingleton(config?: Partial<SurrealDBConfig>): Promise<Surreal>
function hasSingleton(): boolean
function closeSingleton(): Promise<void>
```
### Adapter
```ts
// Re-exported from surrealdb driver (not re-wrapped — direct pass-through for types)
type Patch
type RecordIdValue
class RecordId
class StringRecordId
class Surreal
class Table
// Adapter base interface
interface SurrealAdapter {
connect(config: SurrealDBConfig): Promise<void>
disconnect(): Promise<void>
query<T>(sql: string, params?: Record<string, unknown>): Promise<T[][]>
create<T>(table: string, data: Partial<T>): Promise<T>
select<T>(thing: string): Promise<T[]>
update<T>(thing: string, data: Partial<T>): Promise<T>
merge<T>(thing: string, data: Partial<T>): Promise<T>
delete(thing: string): Promise<void>
readonly connectionState: ConnectionState
}
class SurrealAdapter implements SurrealAdapter
```
### Schema utilities
```ts
function getDefaultSchemaDir(): string
function discoverSchemas(dir: string, options?: DiscoverOptions): Promise<SchemaFile[]>
function filterSchemas(files: SchemaFile[], filter: SchemaFilter): SchemaFile[]
function getSchemasToLoad(dir: string, filter?: SchemaFilter): Promise<SchemaFile[]>
function readSchemaContent(file: SchemaFile): Promise<string>
const DEFAULT_SCHEMA_ORDER: readonly string[]
interface SchemaFile { name: string; path: string; content?: string }
type SchemaFilter = (file: SchemaFile) => boolean
```
### Init
```ts
function initDatabase(db: Surreal, options: InitOptions): Promise<InitResult>
function initAndClose(options: DbConfig): Promise<InitResult>
function createInitializer(options: InitOptions): (db: Surreal) => Promise<InitResult>
interface InitOptions {
schemaDir?: string
schemas?: SchemaFile[]
filter?: SchemaFilter
hooks?: SchemaHook[]
dryRun?: boolean
}
interface InitResult { applied: string[]; skipped: string[]; errors: string[] }
interface DbConfig extends InitOptions {
connection?: Partial<SurrealDBConfig>
}
type FilterStrategy = 'include' | 'exclude'
interface SchemaHook { before?: HookFn; after?: HookFn }
type AdvancedInitOptions = InitOptions & { parallel?: boolean; timeout?: number }
```
### Common types
```ts
type SurrealVersion = string
type MajorVersion = 1 | 2 | 3
interface VersionInfo { version: SurrealVersion; major: MajorVersion; minor: number; patch: number; prerelease?: string }
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'authenticated' | 'error'
interface RecordId { tb: string; id: string | number }
type RecordResult<T = unknown> = T & { id: RecordId }
type QueryStatementResult<T = unknown> = { status: 'OK' | 'ERR'; time?: string; result?: T; error?: string }
type RawQueryResult<T = unknown> = QueryStatementResult<T>[]
type QueryResult<T = unknown> = T[][]
interface AuthCredentials { username: string; password: string }
```
### Error codes
```ts
type SurrealErrorCode =
| 'SURREAL_CONNECT_FAILED'
| 'SURREAL_QUERY_FAILED'
| 'SURREAL_AUTH_FAILED'
| 'SURREAL_NOT_CONNECTED'
| 'SURREAL_TIMEOUT'
| 'SURREAL_VERSION_UNSUPPORTED'
| 'SURREAL_SCHEMA_LOAD_FAILED'
| 'SURREAL_INIT_FAILED'
```
---
## New additions for genesis
### 1. Structured `SurrealError`
`SurrealError` extends `SuaveplanError` from `@suaveplan/error`, carrying a `SurrealErrorCode` discriminant. All internal throw sites use named factory helpers.
### 2. Zod schemas for all public option types
- `SurrealDBConfigSchema`
- `InitOptionsSchema`
- `DiscoverOptionsSchema`
- `SchemaFileSchema`
- `AdvancedInitOptionsSchema`
### 3. Retry integration
`createConnection` and `SurrealAdapter.query` accept an optional `retry?: RetryStrategyOptions` field. When provided the connection/query is wrapped with `@suaveplan/retry`'s `withRetry`. Default: no retry (caller opt-in).
### 4. AbortSignal throughout
All async public functions accept an optional `signal?: AbortSignal`. Pending connections abort immediately on signal; in-flight queries are cancelled via the driver's abort mechanism where supported.
### 5. OTel instrumentation
| Operation | Signal |
|---|---|
| `createConnection` | span `surreal.connect`, attributes `surreal.url`, `surreal.namespace` |
| `SurrealAdapter.query` | span `surreal.query`, histogram `surreal.query_duration_ms`, counter `surreal.query_total` |
| `SurrealAdapter.connect` | counter `surreal.connect_total` |
| `closeSingleton` | span `surreal.disconnect` |
| `initDatabase` | span `surreal.init`, attribute `surreal.schema_count` |
### 6. Version detector utility
`detectVersion(db: Surreal): Promise<VersionInfo>` — calls `db.version()`, parses semver, returns typed `VersionInfo`. Throws `SurrealError` with code `SURREAL_VERSION_UNSUPPORTED` for major versions outside 1–3.
### 7. `DatabaseAdapter` bridge
`SurrealAdapter` implements `@suaveplan/db`'s `DatabaseAdapter` interface so it can be consumed by `@suaveplan/migration` and other db-layer packages without tight coupling to `surrealdb.js`.
### 8. Property-based tests (fast-check)
- `detectVersion` parser: for arbitrary semver strings, assert round-trip correctness of `major.minor.patch`.
- `filterSchemas`: for N random schema files with random names, assert that include/exclude filter with the same predicate is the complement of the other.
- `SurrealAdapter` state machine: transitions are deterministic — `connect` moves to `connecting` → `connected`; `disconnect` from any state moves to `disconnected`.
### 9. Benchmark suite
- `createConnection_cold`: connect + authenticate + disconnect (requires live SurrealDB or skip with `SKIP_DB_BENCHMARKS`).
- `query_hot`: 1000 `SELECT * FROM test` queries against a pre-connected adapter (requires live DB).
- `detectVersion_parse`: 10 000 semver parse iterations (pure, no IO).
---
## File layout
```text
universal/database/surrealdb/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts # Public barrel
error-codes.ts # SURREAL.* codes
constants/
index.ts # DEFAULT_SCHEMA_ORDER, connection defaults
types/
common.ts # All shared TS types
common.md
connection/
connection/
connection.ts # createConnection, connectWith, getSingleton, hasSingleton, closeSingleton
connection.md
connection.types.ts # SurrealDBConfig
connection.test.ts
index.ts
adapters/
surreal-adapter/
surreal-adapter.ts # SurrealAdapter class
surreal-adapter.md
surreal-adapter.types.ts # SurrealAdapter interface
surreal-adapter.test.ts
index.ts
schema/
discovery/
discovery.ts # discoverSchemas, filterSchemas, getSchemasToLoad, readSchemaContent
discovery.md
discovery.types.ts
discovery.test.ts
index.ts
init/
init/
init.ts # initDatabase, initAndClose, createInitializer
init.md
init.types.ts # InitOptions, InitResult, DbConfig, SchemaHook
init.test.ts
index.ts
utils/
version-detector.ts # detectVersion
version-detector.md
version-detector.test.ts
__benchmarks__/
surreal.bench.ts
tests/
integration.test.ts # Full connect → query → disconnect (real SurrealDB via docker)
property.test.ts # fast-check property tests
```
---
## Impact
**Downstream packages**: `@suaveplan/migration` depends on `SurrealAdapter` as a `DatabaseAdapter` implementation. No other genesis package currently requires it.
**Breaking changes from main-branch API**: none. All main-branch call sites compile unchanged; new optional fields (`retry`, `signal`, `logger`) are additive.
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/retry` must be published to Verdaccio before this package can be built or tested.

View File

@@ -0,0 +1,186 @@
# Tasks: enterprise-surrealdb
**Change ID**: enterprise-surrealdb
**Package**: `@suaveplan/surrealdb`
**Target path**: `universal/database/surrealdb`
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/retry`
---
## 1. Scaffold
- [x] Create directory `universal/database/surrealdb/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
- [x] Write `package.json` at version `0.1.0` with:
- `"type": "module"`, `"sideEffects": false`
- `exports` map: top-level `"."` barrel plus subpaths for `./connection/connection`, `./adapters/surreal-adapter`, `./schema/discovery`, `./init/init`, `./utils/version-detector`
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/db workspace:^`, `@suaveplan/retry workspace:^`, `surrealdb catalog:database`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
---
## 2. Types + errors
### 2a. Common types (`src/types/common.ts` + `src/types/common.md`)
- [x] Define all shared TS types: `SurrealVersion`, `MajorVersion`, `VersionInfo`, `ConnectionState`, `RecordId`, `RecordResult`, `QueryStatementResult`, `RawQueryResult`, `QueryResult`, `AuthCredentials`.
- [x] Write `src/types/common.md` (≥200 words, both simple + advanced examples, API reference).
### 2b. Error codes (`src/error-codes.ts`)
- [x] Import `CodeMetadata`, `ErrorCodes`, `SuaveplanError` from `@suaveplan/error`.
- [x] Define `SurrealErrorCode` union — eight codes: `SURREAL_CONNECT_FAILED`, `SURREAL_QUERY_FAILED`, `SURREAL_AUTH_FAILED`, `SURREAL_NOT_CONNECTED`, `SURREAL_TIMEOUT`, `SURREAL_VERSION_UNSUPPORTED`, `SURREAL_SCHEMA_LOAD_FAILED`, `SURREAL_INIT_FAILED`.
- [x] Define `SURREAL_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` for each code.
- [x] Export `registerSurrealErrorCodes()` — calls `ErrorCodes.registerMany(SURREAL_ERROR_METADATA)`. Idempotent.
- [x] Call `registerSurrealErrorCodes()` as an eager side-effect at module scope.
- [x] Export `SurrealError` extending `SuaveplanError` carrying `SurrealErrorCode` as its `.code` discriminant.
### 2c. Zod option schemas
- [x] Write `src/connection/connection/connection.types.ts` with `SurrealDBConfigSchema` and inferred `SurrealDBConfig`.
- [x] Write `src/init/init/init.types.ts` with `InitOptionsSchema`, `AdvancedInitOptionsSchema`, `SchemaFileSchema`.
- [x] Write `src/schema/discovery/discovery.types.ts` with `DiscoverOptionsSchema`.
---
## 3. Constants
- [x] Write `src/constants/index.ts`:
- `DEFAULT_SCHEMA_ORDER`: `readonly string[]` with sensible migration-first ordering.
- `DEFAULT_SURREAL_URL`, `DEFAULT_SURREAL_NAMESPACE`, `DEFAULT_SURREAL_DATABASE`.
- `SURREAL_SUPPORTED_MAJOR_VERSIONS: readonly MajorVersion[]` = `[1, 2, 3]`.
---
## 4. Version detector (`src/utils/version-detector.ts`)
- [x] Implement `detectVersion(db: Surreal): Promise<VersionInfo>`:
- Calls `db.version()`, parses the semver string with a regex.
- Returns typed `VersionInfo` for major versions 1–3.
- Throws `SurrealError` with code `SURREAL_VERSION_UNSUPPORTED` for unknown major versions.
- [x] Write `version-detector.test.ts`: known version strings for v1/v2/v3, prerelease string, unsupported major throws.
- [x] Write `version-detector.md` (≥200 words).
---
## 5. Connection (`src/connection/connection/`)
- [x] Implement `getDefaultConfig(): SurrealDBConfig` — reads `DB_URL`, `DB_NAMESPACE`, `DB_NAME`, `DB_USER`, `DB_PASS` from `process.env` where available; falls back to defaults.
- [x] Implement `createConnection(config?, options?)`:
- Merges partial config with defaults.
- Instantiates `new Surreal()`, calls `db.connect(url)`, then authenticates.
- Accepts optional `signal?: AbortSignal` — throws `SurrealError(SURREAL_TIMEOUT)` on abort before connect completes.
- Accepts optional `retry?: RetryStrategyOptions` — wraps the connect+auth sequence with `@suaveplan/retry`'s `withRetry`.
- OTel: span `surreal.connect`, counter `surreal.connect_total`.
- [x] Implement `connectWith`, `getSingleton`, `hasSingleton`, `closeSingleton` — delegate to `@suaveplan/db`'s singleton helpers where appropriate.
- [x] Write `connection.test.ts` (requires real SurrealDB; gate with `process.env.SURREALDB_URL` skip guard).
- [x] Write `connection.md` (≥200 words).
---
## 6. Adapter (`src/adapters/surreal-adapter/`)
- [x] Define `SurrealAdapter` interface in `surreal-adapter.types.ts` — mirrors the main-branch surface; adds `DatabaseAdapter` from `@suaveplan/db` as parent interface.
- [x] Implement `SurrealAdapter` class:
- Internal `_state: ConnectionState` with protected getter.
- `connect(config)` — calls `createConnection`, transitions state.
- `disconnect()` — calls `db.close()`, transitions to `disconnected`.
- `query<T>(sql, params?, options?)` — calls `db.query(sql, params)`, wraps errors as `SurrealError(SURREAL_QUERY_FAILED)`, records OTel span + histogram.
- `create`, `select`, `update`, `merge`, `delete` — delegate to driver, wrap errors.
- All methods accept optional `signal?: AbortSignal`.
- Fix: use `INSERT INTO` query instead of `db.create()` to avoid surrealdb.js v2 content-drop bug (see memory `feedback_surrealdb_create_content_drop`).
- [x] Write `surreal-adapter.test.ts` (integration, real SurrealDB; SURREALDB_URL skip guard).
- [x] Write `surreal-adapter.md` (≥200 words).
---
## 7. Schema discovery (`src/schema/discovery/`)
- [x] Implement `discoverSchemas(dir, options?)` — reads directory recursively for `.surql` files; returns `SchemaFile[]` sorted by `DEFAULT_SCHEMA_ORDER` then alphabetically.
- [x] Implement `filterSchemas(files, filter)` — apply caller-supplied predicate.
- [x] Implement `getSchemasToLoad(dir, filter?)` — `discoverSchemas` + optional `filterSchemas`.
- [x] Implement `readSchemaContent(file)` — reads file content; caches in `file.content`.
- [x] Write `discovery.test.ts` — in-memory fixture files (no real FS), happy path, empty dir, filter exclude/include.
- [x] Write `discovery.md` (≥200 words).
---
## 8. Init (`src/init/init/`)
- [x] Implement `initDatabase(db, options)`:
- Calls `getSchemasToLoad`, reads each file, executes via `db.query(content)`.
- Runs `options.hooks?.before` / `after` around each schema.
- Dry-run mode: skip execution, record in `result.skipped`.
- Returns `InitResult { applied, skipped, errors }`.
- OTel: span `surreal.init`, attribute `surreal.schema_count`.
- [x] Implement `initAndClose(options)` — opens a singleton, runs init, closes.
- [x] Implement `createInitializer(options)` — returns a closure `(db) => initDatabase(db, options)`.
- [x] Write `init.test.ts` (unit: mock schema files, real adapter not required; integration: SURREALDB_URL guard).
- [x] Write `init.md` (≥200 words).
---
## 9. Public barrel (`src/index.ts`)
- [x] Re-export all public symbols from connection, adapters, schema, init, utils, types, error-codes, constants.
- [x] Re-export `RecordId`, `StringRecordId`, `Surreal`, `Table`, `Patch`, `RecordIdValue` directly from `surrealdb` driver (not wrapped).
---
## 10. OTel instrumentation (cross-cutting)
- [x] Obtain tracer `surreal` and meter `surreal` once at module scope via lazy `trace.getTracer` / `metrics.getMeter`.
- [x] `createConnection` — span `surreal.connect` with attributes `surreal.url`, `surreal.namespace`, `surreal.database`; counter `surreal.connect_total`.
- [x] `SurrealAdapter.query` — span `surreal.query`; histogram `surreal.query_duration_ms`; counter `surreal.query_total` with attribute `surreal.table` where parseable.
- [x] `closeSingleton` / `disconnect` — span `surreal.disconnect`.
- [x] `initDatabase` — span `surreal.init`; attribute `surreal.schema_count`.
- [x] All spans record exception on error and end in `finally`.
---
## 11. Property-based tests (`tests/property.test.ts`)
Use `fast-check`. All test timeouts: `{ timeout: 30000 }`.
- [x] **`detectVersion` parser**: for arbitrary `major.minor.patch` strings (1–3), assert that `parseVersionString` returns the same major/minor/patch. Does not require live DB.
- [x] **`filterSchemas` complement**: generate N random schema files (2–10); assert include filter with predicate P plus exclude filter with predicate P covers the full set without overlap.
- [x] **`ConnectionState` state machine**: for a sequence of `connect` and `disconnect` calls on a mock adapter, assert the final state equals the result of applying transitions in order (always ends `connected` or `disconnected`).
---
## 12. Benchmarks (`src/__benchmarks__/surreal.bench.ts`)
Use `mitata`. Guard IO-bound benchmarks with `SKIP_DB_BENCHMARKS` env flag.
- [x] `detectVersion_parse_10k`: 10 000 pure semver parses.
- [x] `query_hot` (conditional on `!SKIP_DB_BENCHMARKS`): 1000 `SELECT 1` queries on a pre-connected adapter.
- [x] Document placeholder benchmark table in `README.md` under `## Benchmarks`.
---
## 13. Docs
- [x] `src/types/common.md`
- [x] `src/connection/connection/connection.md`
- [x] `src/adapters/surreal-adapter/surreal-adapter.md`
- [x] `src/schema/discovery/discovery.md`
- [x] `src/init/init/init.md`
- [x] `src/utils/version-detector.md`
- [x] Root `universal/database/surrealdb/README.md` — purpose, installation, quick-start, dep table, API symbol list, benchmarks table.
---
## 14. Verify and publish
- [x] Run `bunx turbo lint --filter=@suaveplan/surrealdb` — zero errors.
- [x] Run `bunx turbo typecheck --filter=@suaveplan/surrealdb` — zero errors.
- [x] Run `bunx turbo test --filter=@suaveplan/surrealdb` — all tests pass.
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/surrealdb` — 100% on all touched modules.
- [x] Run `bun run docs:check --filter=@suaveplan/surrealdb` — zero errors.
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
- [x] Confirm `"sideEffects": false` in `package.json`.
- [x] Run `bun scripts/publish.ts --package @suaveplan/surrealdb`.
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/surrealdb`.
- [x] Archive: move `openspec/changes/enterprise-surrealdb/` to `openspec/archive/2026-04-24-enterprise-surrealdb/` and fold spec delta into `openspec/specs/database/spec.md`.