Files
suaveplan/openspec/archive/2026-04-24-enterprise-surrealdb/proposal.md
2026-05-27 23:29:53 +01:00

280 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.