@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 +21 -0
- package/README.md +36 -0
- package/dist/src/core/Clause.d.ts +30 -0
- package/dist/src/core/Cursor.d.ts +21 -0
- package/dist/src/core/Database.d.ts +80 -0
- package/dist/src/core/Query.d.ts +47 -0
- package/dist/src/core/Table.d.ts +69 -0
- package/dist/src/core/constants.d.ts +22 -0
- package/dist/src/core/drivers/MemoryDriver.d.ts +94 -0
- package/dist/src/core/errors.d.ts +38 -0
- package/dist/src/core/factories.d.ts +43 -0
- package/dist/src/core/helpers.d.ts +383 -0
- package/dist/src/core/index.d.ts +11 -0
- package/dist/src/core/index.js +3364 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/types.d.ts +739 -0
- package/dist/src/server/compilers.d.ts +169 -0
- package/dist/src/server/drivers/JSONDriver.d.ts +106 -0
- package/dist/src/server/factories.d.ts +31 -0
- package/dist/src/server/helpers.d.ts +222 -0
- package/dist/src/server/index.cjs +1014 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.ts +5 -0
- package/dist/src/server/types.d.ts +36 -0
- package/package.json +84 -0
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;
|