@orkestrel/database 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orkestrel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @orkestrel/database
2
+
3
+ A typed database abstraction for the `@orkestrel` line — a single
4
+ environment-agnostic core engine (`Database`, `Table`, `Query`, `Cursor`,
5
+ `Clause`) over pluggable storage drivers at the seams. Built to sit beside
6
+ `@orkestrel/contract` (validation) and `@orkestrel/emitter` (observable
7
+ lifecycle), reusing both as it takes shape.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @orkestrel/database
13
+ ```
14
+
15
+ ## Requirements
16
+
17
+ - Node.js >= 24
18
+ - Core is ESM; the `./server` subpath ships a CommonJS build
19
+
20
+ ## Status
21
+
22
+ Pre-release (`0.0.1`): the core engine and the memory and JSON file drivers
23
+ are implemented and tested, but the public API is still unstable and may
24
+ change without notice. See [guides/src/database.md](./guides/src/database.md)
25
+ for the full documented surface.
26
+
27
+ ## Package
28
+
29
+ Published as two environment-scoped entry points per the `exports` field in
30
+ `package.json`: a shared core (with the in-memory driver) and `./server` (the
31
+ JSON file driver). IndexedDB and SQLite drivers are planned once their
32
+ backing packages exist.
33
+
34
+ ## License
35
+
36
+ MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
@@ -0,0 +1,30 @@
1
+ import type { FieldPath } from '@orkestrel/contract';
2
+ import type { Condition, Connector, QueryInterface, ClauseInterface } from './types.js';
3
+ /**
4
+ * A pending condition opened by a query's `where` / `and` / `or`.
5
+ *
6
+ * @remarks
7
+ * Holds the column, the connector that will join this condition to the ones
8
+ * before it, and a recorder the owning query supplies. Each operator builds the
9
+ * {@link Condition}, hands it to the recorder, and returns the query — so the
10
+ * fluent chain flows straight back into the builder without exposing a mutator.
11
+ */
12
+ export declare class Clause<T = Record<string, unknown>> implements ClauseInterface<T> {
13
+ #private;
14
+ constructor(record: (condition: Condition) => QueryInterface<T>, column: FieldPath, connector: Connector);
15
+ equals(value: unknown): QueryInterface<T>;
16
+ not(value: unknown): QueryInterface<T>;
17
+ above(value: unknown): QueryInterface<T>;
18
+ below(value: unknown): QueryInterface<T>;
19
+ from(value: unknown): QueryInterface<T>;
20
+ to(value: unknown): QueryInterface<T>;
21
+ between(lower: unknown, upper: unknown): QueryInterface<T>;
22
+ like(pattern: string): QueryInterface<T>;
23
+ glob(pattern: string): QueryInterface<T>;
24
+ starts(prefix: string): QueryInterface<T>;
25
+ ends(suffix: string): QueryInterface<T>;
26
+ any(values: readonly unknown[]): QueryInterface<T>;
27
+ none(values: readonly unknown[]): QueryInterface<T>;
28
+ absent(): QueryInterface<T>;
29
+ present(): QueryInterface<T>;
30
+ }
@@ -0,0 +1,21 @@
1
+ import type { CursorInterface, Key, TableInterface } from './types.js';
2
+ /**
3
+ * A forward row cursor for bulk in-place mutation.
4
+ *
5
+ * @remarks
6
+ * Iterates a snapshot of the table's keys captured when the cursor was opened,
7
+ * reading each row lazily through the owning table — so a mutation made during
8
+ * iteration cannot corrupt the walk, and a key removed mid-iteration is simply
9
+ * skipped. `update` and `remove` act on the row at the current position.
10
+ */
11
+ export declare class Cursor<T = Record<string, unknown>> implements CursorInterface<T> {
12
+ #private;
13
+ constructor(table: TableInterface<T>, keys: readonly Key[]);
14
+ get value(): T | undefined;
15
+ get index(): number;
16
+ get done(): boolean;
17
+ next(): Promise<void>;
18
+ update(changes: Partial<T>): Promise<void>;
19
+ remove(): Promise<void>;
20
+ close(): void;
21
+ }
@@ -0,0 +1,80 @@
1
+ import type { EmitterInterface } from '@orkestrel/emitter';
2
+ import type { DatabaseEventMap, DatabaseInterface, DatabaseOptions, DatabaseStatus, Migration, ReadOptions, RowOf, TableExport, TableInterface, TableKeys, TableSchema, TablesShape } from './types.js';
3
+ /**
4
+ * A database — the ergonomic entry point over a {@link DriverInterface}.
5
+ *
6
+ * @remarks
7
+ * Owns the driver and a `tables` shape map, connecting the driver lazily on first
8
+ * use so a freshly created database is immediately usable. `table(name)` returns
9
+ * a table typed by that table's shape `Infer`. `import` registers more tables and
10
+ * returns a database re-typed with them over the **same** driver and storage;
11
+ * `export` emits a portable {@link TableExport} per table. `transaction` snapshots
12
+ * the driver, runs the scope, and rolls every table back if it throws — an
13
+ * optimistic model that works uniformly across backends rather than reconciling
14
+ * SQL's and IndexedDB's incompatible native transactions.
15
+ *
16
+ * @remarks
17
+ * - **Versioned (optional).** When {@link DatabaseOptions.version} is set and the driver
18
+ * implements both {@link DriverInterface.meta} and {@link DriverInterface.stamp},
19
+ * `open()` reconciles the driver's persisted {@link DriverMeta} against the declared
20
+ * version INSIDE the same lazy-connect chain, AFTER the `open` event fires — see
21
+ * {@link DatabaseOptions.version} for the full reconciliation contract.
22
+ * - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
23
+ * connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
24
+ * `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
25
+ * event is emitted directly, strictly AFTER the relevant transition: `commit` only after
26
+ * the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
27
+ * OBSERVES the propagated error — it never swallows it (the original throw propagates
28
+ * exactly as before). The emitter isolates a listener throw and routes it to its `error`
29
+ * handler (the `error` option), so observation can never reorder, throw into, or corrupt
30
+ * the snapshot / commit / rollback flow.
31
+ */
32
+ export declare class Database<T extends TablesShape = TablesShape> implements DatabaseInterface<T> {
33
+ #private;
34
+ constructor(options: DatabaseOptions<T>);
35
+ get emitter(): EmitterInterface<DatabaseEventMap>;
36
+ get name(): string;
37
+ get status(): DatabaseStatus;
38
+ table<K extends keyof T & string>(name: K): TableInterface<RowOf<T[K]>>;
39
+ import<U extends TablesShape>(tables: U, keys?: TableKeys): DatabaseInterface<U>;
40
+ export(): Readonly<Record<string, TableExport>>;
41
+ open(): Promise<void>;
42
+ close(): Promise<void>;
43
+ /**
44
+ * Run `scope` transactionally: commit its writes on success, roll every table
45
+ * back if it throws.
46
+ *
47
+ * @remarks
48
+ * When the driver implements the optional native {@link DriverInterface.transaction}
49
+ * hook, that native `commit` / `rollback` handle drives the transaction; otherwise
50
+ * the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
51
+ * emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
52
+ * `options.signal` is checked ONCE at entry, before connecting or starting any
53
+ * transactional work — an already-aborted signal throws `ABORTED` and neither the
54
+ * native hook nor the snapshot floor is invoked. Nesting is unguarded and
55
+ * unsupported exactly as before: this is a single-writer model, not reentrant.
56
+ * On the native path, a `scope` throw rolls back via the native handle; a
57
+ * native `commit` failure propagates as-is with no rollback attempt — the
58
+ * engine owns transaction state after a failed COMMIT.
59
+ *
60
+ * @param scope - The transactional work to run
61
+ * @param options - `{ signal }` to abort before the transaction starts
62
+ * @returns The scope's resolved value
63
+ * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
64
+ */
65
+ transaction<R>(scope: () => Promise<R>, options?: ReadOptions): Promise<R>;
66
+ /**
67
+ * Diff `deployed` against this database's declared schema and apply the
68
+ * resulting plan through the driver's optional `migrate` hook.
69
+ *
70
+ * @param deployed - The schema currently deployed, as {@link TableSchema}s
71
+ * @param options - `{ signal }` to abort before the migration starts
72
+ * @returns The applied {@link Migration} plan
73
+ * @throws A `MIGRATION` {@link DatabaseError} when the driver does not
74
+ * implement `migrate`, or when a step references an unknown table
75
+ * (propagated from the driver)
76
+ * @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
77
+ * already fired at entry
78
+ */
79
+ migrate(deployed: readonly TableSchema[], options?: ReadOptions): Promise<Migration>;
80
+ }
@@ -0,0 +1,47 @@
1
+ import type { FieldPath } from '@orkestrel/contract';
2
+ import type { AggregateFunction, ClauseInterface, QueryInterface, ReadOptions, TableInterface } from './types.js';
3
+ /**
4
+ * A fluent query builder bound to one table.
5
+ *
6
+ * @remarks
7
+ * Accumulates conditions, ordering, JS filters, and a page; each builder method
8
+ * mutates and returns the same instance, so a chain reads as one statement. The
9
+ * portable parts (conditions, order, page) compile into a {@link Criteria} the
10
+ * table resolves; a `filter` predicate is applied in memory after the read and
11
+ * before paging, so it composes with the rest without a backend ever seeing a
12
+ * JS callback.
13
+ */
14
+ export declare class Query<T = Record<string, unknown>> implements QueryInterface<T> {
15
+ #private;
16
+ constructor(table: TableInterface<T>);
17
+ where(column: FieldPath): ClauseInterface<T>;
18
+ and(column: FieldPath): ClauseInterface<T>;
19
+ or(column: FieldPath): ClauseInterface<T>;
20
+ filter(predicate: (row: T) => boolean): QueryInterface<T>;
21
+ ascending(column: FieldPath): QueryInterface<T>;
22
+ descending(column: FieldPath): QueryInterface<T>;
23
+ limit(count: number): QueryInterface<T>;
24
+ offset(count: number): QueryInterface<T>;
25
+ all(): Promise<readonly T[]>;
26
+ first(): Promise<T | undefined>;
27
+ count(): Promise<number>;
28
+ /**
29
+ * Lazy per-row evaluation of this query's conditions / filters / offset /
30
+ * limit.
31
+ *
32
+ * @remarks
33
+ * `order` and its comparators are IGNORED (streaming yields unsorted, as rows
34
+ * are evaluated one at a time). Same abort semantics as
35
+ * `TableInterface.scan`: the signal (if any) is checked before each yield,
36
+ * and breaking out early closes the underlying source.
37
+ *
38
+ * @param options - `signal` to cancel the iteration; checked before each yield
39
+ * @returns An async iterable of matching rows
40
+ */
41
+ stream(options?: ReadOptions): AsyncGenerator<T>;
42
+ aggregate(operation: AggregateFunction, column: FieldPath): Promise<number | undefined>;
43
+ sum(column: FieldPath): Promise<number | undefined>;
44
+ average(column: FieldPath): Promise<number | undefined>;
45
+ minimum(column: FieldPath): Promise<number | undefined>;
46
+ maximum(column: FieldPath): Promise<number | undefined>;
47
+ }
@@ -0,0 +1,69 @@
1
+ import type { ContractInterface, FieldPath } from '@orkestrel/contract';
2
+ import type { EmitterErrorHandler, EmitterHooks, EmitterInterface } from '@orkestrel/emitter';
3
+ import type { AggregateFunction, Criteria, CursorInterface, DriverInterface, Key, KeyFunction, QueryInterface, ReadOptions, Row, TableEventMap, TableInterface } from './types.js';
4
+ /**
5
+ * A table — typed keyed CRUD plus fluent query and cursor access over a driver.
6
+ *
7
+ * @remarks
8
+ * The table's contract is the load-bearing piece: writes go through `parse`
9
+ * (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
10
+ * reads come back through the contract guard (narrowing a stored {@link Row} to
11
+ * the table's type — no assertion, AGENTS §1), and `contract` is exposed for
12
+ * introspection and seeding. The driver only stores and scans; all querying is
13
+ * the shared core engine in `helpers.ts`.
14
+ *
15
+ * @remarks
16
+ * - **Observable (§13).** The owned {@link emitter} ({@link TableEventMap}) carries the
17
+ * per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
18
+ * fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
19
+ * database-level lifecycle. Events carry the affected KEY only (no value payload, to
20
+ * keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
21
+ * directly, strictly AFTER the driver write / delete / clear completes; the emitter
22
+ * isolates a listener throw and routes it to its `error` handler (the `error` option),
23
+ * so a buggy observer can never corrupt a write or perturb a transaction.
24
+ */
25
+ export declare class Table<T = Row> implements TableInterface<T> {
26
+ #private;
27
+ constructor(ready: () => Promise<void>, driver: DriverInterface, name: string, key: string, contract: ContractInterface<T>, generate?: KeyFunction, on?: EmitterHooks<TableEventMap>, error?: EmitterErrorHandler);
28
+ get emitter(): EmitterInterface<TableEventMap>;
29
+ get name(): string;
30
+ get primary(): string;
31
+ get contract(): ContractInterface<T>;
32
+ get(key: Key): Promise<T | undefined>;
33
+ get(keys: readonly Key[]): Promise<readonly (T | undefined)[]>;
34
+ resolve(key: Key): Promise<T>;
35
+ resolve(keys: readonly Key[]): Promise<readonly T[]>;
36
+ has(key: Key): Promise<boolean>;
37
+ has(keys: readonly Key[]): Promise<readonly boolean[]>;
38
+ keys(): Promise<readonly Key[]>;
39
+ records(criteria?: Criteria, options?: ReadOptions): Promise<readonly T[]>;
40
+ count(criteria?: Criteria, options?: ReadOptions): Promise<number>;
41
+ aggregate(operation: AggregateFunction, column: FieldPath, criteria?: Criteria, options?: ReadOptions): Promise<number | undefined>;
42
+ /**
43
+ * Stream the table's rows matching `criteria`, applying offset/limit paging.
44
+ *
45
+ * @remarks
46
+ * `criteria.limit` counts rows that pass BOTH the criteria conditions AND the
47
+ * table's contract guard (a stored row that fails the guard is skipped and
48
+ * does not count toward `limit`) — this can differ from {@link records}'s
49
+ * `limit`, which a driver's optional native `records` hook applies BEFORE
50
+ * the contract guard runs, when storage holds rows that no longer conform
51
+ * to the table's contract.
52
+ *
53
+ * @param criteria - Optional conditions plus offset/limit paging
54
+ * @param options - `{ signal }` to abort mid-stream
55
+ * @returns An async generator of matching, guard-conforming rows
56
+ */
57
+ scan(criteria?: Criteria, options?: ReadOptions): AsyncGenerator<T>;
58
+ set(row: T, options?: ReadOptions): Promise<Key>;
59
+ set(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
60
+ add(row: T, options?: ReadOptions): Promise<Key>;
61
+ add(rows: readonly T[], options?: ReadOptions): Promise<readonly Key[]>;
62
+ update(key: Key, changes: Partial<T>, options?: ReadOptions): Promise<boolean>;
63
+ update(keys: readonly Key[], changes: Partial<T>, options?: ReadOptions): Promise<readonly boolean[]>;
64
+ remove(key: Key, options?: ReadOptions): Promise<boolean>;
65
+ remove(keys: readonly Key[], options?: ReadOptions): Promise<readonly boolean[]>;
66
+ clear(): Promise<void>;
67
+ query(): QueryInterface<T>;
68
+ cursor(): Promise<CursorInterface<T>>;
69
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The primary-key column assumed when {@link TableKeys} does not name one.
3
+ *
4
+ * @remarks
5
+ * `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
6
+ * lean on, so a table that omits `key` keys its rows by `id`.
7
+ */
8
+ export declare const DEFAULT_PRIMARY = "id";
9
+ /**
10
+ * The longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
11
+ *
12
+ * @remarks
13
+ * A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
14
+ * criteria over the wire, so `likeMatch` / `globMatch` run attacker-controlled
15
+ * patterns. The matcher is the LINEAR greedy two-pointer wildcard match — never a
16
+ * backtracking regex (`.*`-segments-separated-by-literals against a long input is the
17
+ * catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
18
+ * pattern). Capping the pattern length bounds that pattern factor, leaving a match
19
+ * linear in the value length whatever the pattern. A longer pattern throws a
20
+ * `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
21
+ */
22
+ export declare const MAX_PATTERN_LENGTH = 1024;
@@ -0,0 +1,94 @@
1
+ import type { Criteria, DriverInterface, DriverMeta, Key, Migration, Row, TableSchema } from '../types.js';
2
+ /**
3
+ * The reference {@link DriverInterface} — nested maps, no I/O.
4
+ *
5
+ * @remarks
6
+ * The in-between made concrete: it runs identically in a browser or on a server,
7
+ * so it is the storage behind tests, ephemeral caches, and any code that wants
8
+ * the database API without a persistent backend. Rows are copied in and out so a
9
+ * caller can never mutate stored state by reference (AGENTS §11), and `snapshot`
10
+ * clones every table to give transactions an exact rollback point. `scan` and
11
+ * `keys` yield in key order — sorted by the core {@link compareValues} total
12
+ * order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
13
+ * reads) backends honor, so an unordered read agrees across every backend rather
14
+ * than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
15
+ * implements the same nine methods over real storage.
16
+ */
17
+ export declare class MemoryDriver implements DriverInterface {
18
+ #private;
19
+ open(schema: readonly TableSchema[]): Promise<void>;
20
+ close(): Promise<void>;
21
+ read(table: string, key: Key): Promise<Row | undefined>;
22
+ write(table: string, key: Key, row: Row): Promise<void>;
23
+ delete(table: string, key: Key): Promise<boolean>;
24
+ keys(table: string): Promise<readonly Key[]>;
25
+ scan(table: string): AsyncIterable<Row>;
26
+ /**
27
+ * Natively filtered lazy iteration — the {@link DriverInterface.stream} hook.
28
+ *
29
+ * @remarks
30
+ * Iterates the table's keys in the same key order `scan` and `keys` yield
31
+ * (sorted by {@link compareValues}), testing each row against
32
+ * `criteria.conditions` (via {@link matchesCriteria}) before counting it
33
+ * toward `offset` / `limit`. Both are applied lazily as matches are found —
34
+ * `offset` matches are skipped without being yielded, and iteration stops the
35
+ * instant `limit` yields have been produced, so a large table is never fully
36
+ * walked for a small page. `criteria.order` is IGNORED (the same contract as
37
+ * `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
38
+ * order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
39
+ * §11), and an unknown table mirrors `scan`'s empty-yield behavior.
40
+ *
41
+ * @param table - The table to stream
42
+ * @param criteria - The filter / offset / limit to apply lazily
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * for await (const row of driver.stream('users', { conditions, limit: 10 })) {
47
+ * // one matched row at a time, in key order
48
+ * }
49
+ * ```
50
+ */
51
+ stream(table: string, criteria: Criteria): AsyncIterable<Row>;
52
+ clear(table: string): Promise<void>;
53
+ /**
54
+ * Capture the current state and return a thunk that rolls back to it.
55
+ *
56
+ * @remarks
57
+ * `tables` omitted clones and restores the WHOLE store, byte-identical to the
58
+ * prior whole-store behavior. `tables` provided clones ONLY the named tables,
59
+ * and the returned thunk restores ONLY those — every other table keeps
60
+ * whatever it was mutated to after the snapshot was taken.
61
+ *
62
+ * @param tables - The table names to scope the snapshot to; omitted captures every table
63
+ * @returns A thunk that restores the captured tables
64
+ */
65
+ snapshot(tables?: readonly string[]): Promise<() => Promise<void>>;
66
+ /**
67
+ * Return the persisted {@link DriverMeta}, or `undefined` when the store has
68
+ * never been stamped.
69
+ *
70
+ * @remarks
71
+ * In-process only — the metadata lives in this instance's memory, exactly
72
+ * like the rest of this driver's storage. A driver-conformance-valid
73
+ * implementation of the optional `meta` / `stamp` pair.
74
+ *
75
+ * @returns The last-stamped {@link DriverMeta}, or `undefined`
76
+ */
77
+ meta(): Promise<DriverMeta | undefined>;
78
+ /**
79
+ * Persist `meta` verbatim for a later `meta()` to return.
80
+ *
81
+ * @param meta - The {@link DriverMeta} to persist
82
+ */
83
+ stamp(meta: DriverMeta): Promise<void>;
84
+ /**
85
+ * Apply a {@link Migration} plan's steps against the in-memory store.
86
+ *
87
+ * @remarks
88
+ * A multi-step plan applies its steps sequentially and is NOT atomic — a
89
+ * failure partway through a plan leaves the earlier steps already applied.
90
+ *
91
+ * @param plan - The migration plan to apply
92
+ */
93
+ migrate(plan: Migration): Promise<void>;
94
+ }
@@ -0,0 +1,38 @@
1
+ import type { DatabaseErrorCode } from './types.js';
2
+ /**
3
+ * An error thrown by the database layer.
4
+ *
5
+ * @remarks
6
+ * Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
7
+ * offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
8
+ * `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
9
+ * row that fails its table's contract (`VALIDATION`), a cancelled operation whose
10
+ * {@link ReadOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
11
+ * `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
12
+ * driver that violates a {@link DriverInterface} invariant, thrown by the
13
+ * `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
14
+ * fault surfaced by a driver seam — e.g. a filesystem failure while
15
+ * persisting (`DRIVER`) — as opposed to expected domain conditions, which
16
+ * keep their specific codes.
17
+ */
18
+ export declare class DatabaseError extends Error {
19
+ readonly code: DatabaseErrorCode;
20
+ readonly context?: Readonly<Record<string, unknown>>;
21
+ constructor(code: DatabaseErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
22
+ }
23
+ /**
24
+ * Narrow an unknown caught value to a {@link DatabaseError}.
25
+ *
26
+ * @param value - The value to test (typically a `catch` binding)
27
+ * @returns `true` when `value` is a {@link DatabaseError}
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * try {
32
+ * await users.add(row)
33
+ * } catch (error) {
34
+ * if (isDatabaseError(error) && error.code === 'CONFLICT') await users.set(row)
35
+ * }
36
+ * ```
37
+ */
38
+ export declare function isDatabaseError(value: unknown): value is DatabaseError;
@@ -0,0 +1,43 @@
1
+ import type { DatabaseInterface, DatabaseOptions, DriverInterface, TablesShape } from './types.js';
2
+ /**
3
+ * Create a database over a driver and a declared `tables` schema.
4
+ *
5
+ * @remarks
6
+ * `tables` maps each name to its columns (a `column → shape` map); the database
7
+ * wraps each in an `objectShape`, so you never write `objectShape` at the table
8
+ * level. The `const` type parameter captures the literal names and columns, so
9
+ * `db.table('users')` is checked against the schema and typed by `Infer` of its
10
+ * columns — no annotations. Name a non-`id` primary-key column per table via the
11
+ * optional `keys` map.
12
+ *
13
+ * @param options - The driver, the `tables` column map, optional `keys`, and an
14
+ * optional `name`
15
+ * @returns A typed {@link DatabaseInterface}
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
20
+ * import { integerShape, stringShape } from '@orkestrel/contract'
21
+ *
22
+ * const db = createDatabase({
23
+ * driver: createMemoryDriver(),
24
+ * tables: {
25
+ * users: { id: stringShape(), age: integerShape() },
26
+ * posts: { slug: stringShape(), title: stringShape() },
27
+ * },
28
+ * keys: { posts: 'slug' },
29
+ * })
30
+ * await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
31
+ * ```
32
+ */
33
+ export declare function createDatabase<const T extends TablesShape>(options: DatabaseOptions<T>): DatabaseInterface<T>;
34
+ /**
35
+ * Create the in-memory reference {@link DriverInterface}.
36
+ *
37
+ * @remarks
38
+ * Backed by nested maps with no I/O — the same driver runs in a browser or on a
39
+ * server, making it the natural choice for tests and ephemeral storage.
40
+ *
41
+ * @returns A fresh in-memory driver
42
+ */
43
+ export declare function createMemoryDriver(): DriverInterface;