280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
# 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.
|