@venn-lang/db 0.1.0
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 +114 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/connect.ts +18 -0
- package/src/actions/exec.ts +17 -0
- package/src/actions/index.ts +17 -0
- package/src/actions/query.ts +17 -0
- package/src/actions/restore.ts +12 -0
- package/src/actions/seed.ts +24 -0
- package/src/actions/snapshot.ts +13 -0
- package/src/clients/fake-client.ts +28 -0
- package/src/clients/index.ts +2 -0
- package/src/clients/query-engine.ts +56 -0
- package/src/clients/real-client.ts +26 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +20 -0
- package/src/port/db-client.port.ts +16 -0
- package/src/port/db-client.types.ts +35 -0
- package/src/port/index.ts +9 -0
- package/src/types/index.ts +3 -0
- package/src/types/row.ts +7 -0
- package/src/types/type-defs.ts +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinicius Borges
|
|
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,114 @@
|
|
|
1
|
+
# @venn-lang/db
|
|
2
|
+
|
|
3
|
+
> The `db` namespace: seed tables, run statements and snapshot state, all through the `DbClient` port.
|
|
4
|
+
|
|
5
|
+
A flow that talks to a database needs the rows to be there before the first assertion and gone
|
|
6
|
+
after the last. This package gives six verbs for that, and puts every one of them behind a typed
|
|
7
|
+
port so the same flow runs against in-memory tables or a real connection without a line changing.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
The package is part of the stdlib the `venn` CLI loads, so nothing to install. Reach it from a
|
|
12
|
+
flow with a `use` line:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
use "venn/db"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The plugin declares the `net` capability. A host that does not offer it fails the load with a
|
|
19
|
+
readable diagnostic rather than a `TypeError` mid-run.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
module demo.orders
|
|
25
|
+
|
|
26
|
+
use "venn/assert"
|
|
27
|
+
use "venn/db"
|
|
28
|
+
|
|
29
|
+
flow "Orders survive a snapshot" {
|
|
30
|
+
step "seed the tables" {
|
|
31
|
+
const loaded = db.seed { users: [{ id: 1, name: "Ada" }, { id: 2, name: "Grace" }] }
|
|
32
|
+
expect loaded == 2
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
step "query them back" {
|
|
36
|
+
const rows = db.query "SELECT * FROM users"
|
|
37
|
+
expect rows.len == 2
|
|
38
|
+
|
|
39
|
+
const grace = db.query "SELECT * FROM users" { where: { id: 2 } }
|
|
40
|
+
expect grace[0].name == "Grace"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
step "a snapshot undoes a mutation" {
|
|
44
|
+
const before = db.snapshot
|
|
45
|
+
const cleared = db.exec "TRUNCATE users"
|
|
46
|
+
expect cleared == 2
|
|
47
|
+
|
|
48
|
+
db.restore before
|
|
49
|
+
const rows = db.query "SELECT * FROM users"
|
|
50
|
+
expect rows.len == 2
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Verbs
|
|
56
|
+
|
|
57
|
+
| Verb | Options | Gives back |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `db.connect url` | none | The URL it connected to, as a string. |
|
|
60
|
+
| `db.query sql` | `where` (a row of column to value) | `list<db.Row>`, the matching rows. |
|
|
61
|
+
| `db.exec sql` | `rows` (the rows an INSERT carries) | `number`, how many rows were affected. |
|
|
62
|
+
| `db.seed tables` | none | `number`, how many rows were loaded. |
|
|
63
|
+
| `db.snapshot` | none | `db.Tables`, a copy of the current state. |
|
|
64
|
+
| `db.restore snapshot` | none | nothing. |
|
|
65
|
+
|
|
66
|
+
The statement is always the positional argument; everything else is an option in the trailing
|
|
67
|
+
map. `db.seed` accepts the tables either positionally or written inline as options, so
|
|
68
|
+
`db.seed { users: [...] }` and `db.seed baseline` both work.
|
|
69
|
+
|
|
70
|
+
Two named types are published: `db.Row` (a map of column name to value) and `db.Tables` (rows
|
|
71
|
+
grouped by table name, which is what `db.seed` takes and `db.snapshot` gives back).
|
|
72
|
+
|
|
73
|
+
## The DbClient port
|
|
74
|
+
|
|
75
|
+
`venn.port.db-client`, contract version 1, requires `net`, six methods: `connect`, `query`,
|
|
76
|
+
`exec`, `seed`, `snapshot`, `restore`. Actions reach it with `ctx.port(DbClientPort)`, so nothing
|
|
77
|
+
in this package imports a driver.
|
|
78
|
+
|
|
79
|
+
Two implementations ship, and both answer the same conformance suite:
|
|
80
|
+
|
|
81
|
+
| Implementation | Behaviour |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `createFakeDbClient({ tables })` | In-memory tables. Deterministic, offline, seedable. This is what the stdlib binds by default. |
|
|
84
|
+
| `createRealDbClient()` | A stub in this build. Every method throws a `VennError` with code `VN8090` rather than failing quietly. |
|
|
85
|
+
|
|
86
|
+
The fake carries a deliberately small query engine: `query` reads the table named after `FROM`
|
|
87
|
+
and filters by equality on each key of `where`; `exec` appends `rows` for `INSERT INTO` and clears
|
|
88
|
+
the table for `TRUNCATE` or `DELETE FROM`, returning the affected count. `snapshot` does a
|
|
89
|
+
structural clone, so restoring never aliases live state.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
| Export | What it is |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `dbPlugin` (also the default export) | The `PluginDefinition` for the `db` namespace. |
|
|
96
|
+
| `DbClientPort` | The port descriptor. |
|
|
97
|
+
| `DbClient` | The interface an implementation satisfies. |
|
|
98
|
+
| `createFakeDbClient`, `createRealDbClient` | The two implementations. |
|
|
99
|
+
| `QueryArgs`, `ExecArgs` | The argument objects `query` and `exec` take. |
|
|
100
|
+
| `TableMap`, `SeedData`, `DbSnapshot` | Rows keyed by table name. The last two are aliases of the first, named for where they are used. |
|
|
101
|
+
| `Row`, `RowSchema` | The nominal row type and its Zod schema. |
|
|
102
|
+
| `dbTypeDefs` | The `db.Row` and `db.Tables` specs the checker and the editor read. |
|
|
103
|
+
|
|
104
|
+
## Adding an implementation
|
|
105
|
+
|
|
106
|
+
Write the file, add one line to `src/clients/index.ts`, and add one line to
|
|
107
|
+
`src/clients/db-client.test.ts` calling `dbClientConformance` with it. The suite is not rewritten;
|
|
108
|
+
if the new client passes it, the verbs above already work against it.
|
|
109
|
+
|
|
110
|
+
## See also
|
|
111
|
+
|
|
112
|
+
- [`@venn-lang/data`](../std-data) for the deterministic rows to seed with.
|
|
113
|
+
- [`@venn-lang/contracts`](../contracts) for `Port`, `Host` and capability negotiation.
|
|
114
|
+
- [`@venn-lang/stdlib`](../stdlib) for the list of plugins and the port bindings the CLI runs with.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Port } from "@venn-lang/contracts";
|
|
2
|
+
import { PluginDefinition, ZodType } from "@venn-lang/sdk";
|
|
3
|
+
import { TypeSpec } from "@venn-lang/types";
|
|
4
|
+
//#region src/types/row.d.ts
|
|
5
|
+
/** The nominal `Row` type: an opaque record of column values. */
|
|
6
|
+
type Row = Record<string, unknown>;
|
|
7
|
+
/** Zod schema for the nominal `Row` type the plugin contributes. */
|
|
8
|
+
declare const RowSchema: ZodType<Row>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/types/type-defs.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
|
|
13
|
+
*
|
|
14
|
+
* Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
|
|
15
|
+
* `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
|
|
16
|
+
*/
|
|
17
|
+
declare const dbTypeDefs: Readonly<Record<string, TypeSpec>>;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/port/db-client.types.d.ts
|
|
20
|
+
/** Arguments for {@link DbClient.query}: a SQL string plus an optional filter. */
|
|
21
|
+
interface QueryArgs {
|
|
22
|
+
sql: string;
|
|
23
|
+
where?: Row;
|
|
24
|
+
}
|
|
25
|
+
/** Arguments for {@link DbClient.exec}: a mutating statement plus optional rows. */
|
|
26
|
+
interface ExecArgs {
|
|
27
|
+
sql: string;
|
|
28
|
+
rows?: Row[];
|
|
29
|
+
}
|
|
30
|
+
/** Rows keyed by table name. The shape seeds and snapshots share. */
|
|
31
|
+
type TableMap = Record<string, Row[]>;
|
|
32
|
+
/** Rows to load, keyed by table name. */
|
|
33
|
+
type SeedData = TableMap;
|
|
34
|
+
/** A captured copy of every table. Detached from live state, so it can be restored later. */
|
|
35
|
+
type DbSnapshot = TableMap;
|
|
36
|
+
/**
|
|
37
|
+
* The contract a database is reached through. Actions call it via
|
|
38
|
+
* `ctx.port(DbClientPort)` and never touch a driver directly.
|
|
39
|
+
*/
|
|
40
|
+
interface DbClient {
|
|
41
|
+
connect(url: string): Promise<void>;
|
|
42
|
+
query(args: QueryArgs): Promise<Row[]>;
|
|
43
|
+
exec(args: ExecArgs): Promise<number>;
|
|
44
|
+
seed(data: SeedData): Promise<number>;
|
|
45
|
+
snapshot(): Promise<DbSnapshot>;
|
|
46
|
+
restore(snapshot: DbSnapshot): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/port/db-client.port.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* The port every `db` verb reaches the database through.
|
|
52
|
+
*
|
|
53
|
+
* Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
|
|
54
|
+
* the `net` capability, so a host without it is refused at load time rather
|
|
55
|
+
* than failing mid-run.
|
|
56
|
+
*/
|
|
57
|
+
declare const DbClientPort: Port<DbClient>;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/clients/fake-client.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* The in-memory `DbClient`: deterministic tables, no connection, no I/O.
|
|
62
|
+
*
|
|
63
|
+
* @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
|
|
64
|
+
*/
|
|
65
|
+
declare function createFakeDbClient(args?: {
|
|
66
|
+
tables?: SeedData;
|
|
67
|
+
}): DbClient;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/clients/real-client.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* The driver-backed `DbClient`. Not implemented in this build.
|
|
72
|
+
*
|
|
73
|
+
* @throws VennError `VN8090` from every method, so a host that binds it fails
|
|
74
|
+
* loudly instead of quietly answering with nothing.
|
|
75
|
+
*/
|
|
76
|
+
declare function createRealDbClient(): DbClient;
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/plugin.d.ts
|
|
79
|
+
/**
|
|
80
|
+
* The `db` plugin: connect, query, exec, seed, snapshot and restore.
|
|
81
|
+
*
|
|
82
|
+
* Every verb reaches the database through `DbClientPort`, so the host decides
|
|
83
|
+
* whether that is the fake or a real driver. Loading fails up front unless the
|
|
84
|
+
* host offers the `net` capability.
|
|
85
|
+
*/
|
|
86
|
+
declare const dbPlugin: PluginDefinition;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { type DbClient, DbClientPort, type DbSnapshot, type ExecArgs, type QueryArgs, type Row, RowSchema, type SeedData, type TableMap, createFakeDbClient, createRealDbClient, dbPlugin, dbPlugin as default, dbTypeDefs };
|
|
89
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/row.ts","../src/types/type-defs.ts","../src/port/db-client.types.ts","../src/port/db-client.port.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/plugin.ts"],"mappings":";;;;;KAGY,MAAM;;cAGL,WAAW,QAAQ;;;;;;;;;cCEnB,YAAY,SAAS,eAAe;;;;UCLhC;EACf;EACA,QAAQ;;;UAIO;EACf;EACA,OAAO;;;KAIG,WAAW,eAAe;;KAG1B,WAAW;;KAGX,aAAa;;;;;UAMR;EACf,QAAQ,cAAc;EACtB,MAAM,MAAM,YAAY,QAAQ;EAChC,KAAK,MAAM,WAAW;EACtB,KAAK,MAAM,WAAW;EACtB,YAAY,QAAQ;EACpB,QAAQ,UAAU,aAAa;;;;;;;;;;;cCvBpB,cAAc,KAAK;;;;;;;;iBCEhB,mBAAmB;EAAQ,SAAS;IAAkB;;;;;;;;;iBCHtD,sBAAsB;;;;;;;;;;cCEzB,UAAU"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { VennError } from "@venn-lang/contracts";
|
|
2
|
+
import { arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
|
|
3
|
+
import { t } from "@venn-lang/types";
|
|
4
|
+
//#region src/clients/query-engine.ts
|
|
5
|
+
/** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */
|
|
6
|
+
function cloneTables(tables) {
|
|
7
|
+
return structuredClone(tables);
|
|
8
|
+
}
|
|
9
|
+
/** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */
|
|
10
|
+
function selectRows(tables, query) {
|
|
11
|
+
const rows = tables[tableName(query.sql, /from\s+([a-z_]\w*)/i)] ?? [];
|
|
12
|
+
const where = query.where;
|
|
13
|
+
return (where ? rows.filter((row) => matchesWhere(row, where)) : rows).map((row) => ({ ...row }));
|
|
14
|
+
}
|
|
15
|
+
/** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */
|
|
16
|
+
function execStatement(tables, statement) {
|
|
17
|
+
const inserted = tableName(statement.sql, /insert\s+into\s+([a-z_]\w*)/i);
|
|
18
|
+
if (inserted) return insertRows(tables, inserted, statement.rows ?? []);
|
|
19
|
+
return truncate(tables, tableName(statement.sql, /(?:truncate|delete\s+from)\s+([a-z_]\w*)/i));
|
|
20
|
+
}
|
|
21
|
+
/** Append rows into named tables; returns how many rows were loaded. */
|
|
22
|
+
function seedTables(tables, data) {
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
25
|
+
const copies = rows.map((row) => ({ ...row }));
|
|
26
|
+
tables[name] = [...tables[name] ?? [], ...copies];
|
|
27
|
+
count += copies.length;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
function insertRows(tables, name, rows) {
|
|
32
|
+
const copies = rows.map((row) => ({ ...row }));
|
|
33
|
+
tables[name] = [...tables[name] ?? [], ...copies];
|
|
34
|
+
return copies.length;
|
|
35
|
+
}
|
|
36
|
+
function truncate(tables, name) {
|
|
37
|
+
const removed = tables[name]?.length ?? 0;
|
|
38
|
+
if (name) tables[name] = [];
|
|
39
|
+
return removed;
|
|
40
|
+
}
|
|
41
|
+
function matchesWhere(row, where) {
|
|
42
|
+
return Object.entries(where).every(([key, value]) => row[key] === value);
|
|
43
|
+
}
|
|
44
|
+
function tableName(sql, pattern) {
|
|
45
|
+
return pattern.exec(sql)?.[1] ?? "";
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/clients/fake-client.ts
|
|
49
|
+
/**
|
|
50
|
+
* The in-memory `DbClient`: deterministic tables, no connection, no I/O.
|
|
51
|
+
*
|
|
52
|
+
* @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
|
|
53
|
+
*/
|
|
54
|
+
function createFakeDbClient(args = {}) {
|
|
55
|
+
const state = { tables: cloneTables(args.tables ?? {}) };
|
|
56
|
+
return {
|
|
57
|
+
connect: () => Promise.resolve(),
|
|
58
|
+
query: (query) => Promise.resolve(selectRows(state.tables, query)),
|
|
59
|
+
exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),
|
|
60
|
+
seed: (data) => Promise.resolve(seedTables(state.tables, data)),
|
|
61
|
+
snapshot: () => Promise.resolve(cloneTables(state.tables)),
|
|
62
|
+
restore: (snapshot) => restore(state, snapshot)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function restore(state, snapshot) {
|
|
66
|
+
state.tables = cloneTables(snapshot);
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/clients/real-client.ts
|
|
71
|
+
/**
|
|
72
|
+
* The driver-backed `DbClient`. Not implemented in this build.
|
|
73
|
+
*
|
|
74
|
+
* @throws VennError `VN8090` from every method, so a host that binds it fails
|
|
75
|
+
* loudly instead of quietly answering with nothing.
|
|
76
|
+
*/
|
|
77
|
+
function createRealDbClient() {
|
|
78
|
+
return {
|
|
79
|
+
connect: () => notImplemented(),
|
|
80
|
+
query: () => notImplemented(),
|
|
81
|
+
exec: () => notImplemented(),
|
|
82
|
+
seed: () => notImplemented(),
|
|
83
|
+
snapshot: () => notImplemented(),
|
|
84
|
+
restore: () => notImplemented()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function notImplemented() {
|
|
88
|
+
throw new VennError({
|
|
89
|
+
code: "VN8090",
|
|
90
|
+
message: "Db real client not implemented in this build"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/port/db-client.port.ts
|
|
95
|
+
/**
|
|
96
|
+
* The port every `db` verb reaches the database through.
|
|
97
|
+
*
|
|
98
|
+
* Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
|
|
99
|
+
* the `net` capability, so a host without it is refused at load time rather
|
|
100
|
+
* than failing mid-run.
|
|
101
|
+
*/
|
|
102
|
+
const DbClientPort = {
|
|
103
|
+
id: "venn.port.db-client",
|
|
104
|
+
version: 1,
|
|
105
|
+
requires: ["net"],
|
|
106
|
+
methods: [
|
|
107
|
+
"connect",
|
|
108
|
+
"query",
|
|
109
|
+
"exec",
|
|
110
|
+
"seed",
|
|
111
|
+
"snapshot",
|
|
112
|
+
"restore"
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/actions/connect.ts
|
|
117
|
+
/** `db.connect(url)`: open the connection the other `db` verbs then use. */
|
|
118
|
+
const connectAction = defineAction({
|
|
119
|
+
name: "connect",
|
|
120
|
+
doc: "Connect to a database by URL.",
|
|
121
|
+
args: [arg("url", t.string, "Where the database is.")],
|
|
122
|
+
result: t.string,
|
|
123
|
+
run: async (ctx, input) => {
|
|
124
|
+
const url = String(input.args[0] ?? "");
|
|
125
|
+
await ctx.port(DbClientPort).connect(url);
|
|
126
|
+
return url;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/types/row.ts
|
|
131
|
+
/** Zod schema for the nominal `Row` type the plugin contributes. */
|
|
132
|
+
const RowSchema = z.record(z.string(), z.unknown());
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/types/type-defs.ts
|
|
135
|
+
/**
|
|
136
|
+
* The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
|
|
137
|
+
*
|
|
138
|
+
* Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
|
|
139
|
+
* `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
|
|
140
|
+
*/
|
|
141
|
+
const dbTypeDefs = {
|
|
142
|
+
/**
|
|
143
|
+
* One row. Which columns it has is the query's business, not the plugin's, so
|
|
144
|
+
* the type says only what is always true: names to values.
|
|
145
|
+
*/
|
|
146
|
+
Row: t.map(t.dynamic),
|
|
147
|
+
/** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */
|
|
148
|
+
Tables: t.map(t.list(t.ref("db.Row")))
|
|
149
|
+
};
|
|
150
|
+
/** `db.exec("TRUNCATE …", { rows })`: mutate the tables and answer with the affected count. */
|
|
151
|
+
const execAction = defineAction({
|
|
152
|
+
name: "exec",
|
|
153
|
+
doc: "Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.",
|
|
154
|
+
params: z.object({ rows: z.array(RowSchema).optional() }),
|
|
155
|
+
args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
|
|
156
|
+
result: t.number,
|
|
157
|
+
run: (ctx, input) => ctx.port(DbClientPort).exec({
|
|
158
|
+
sql: String(input.args[0] ?? ""),
|
|
159
|
+
rows: input.params.rows
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
/** `db.query("SELECT …", { where })`: run a SELECT and answer with the matching rows. */
|
|
163
|
+
const queryAction = defineAction({
|
|
164
|
+
name: "query",
|
|
165
|
+
doc: "Run a SELECT and return the matching rows.",
|
|
166
|
+
params: z.object({ where: RowSchema.optional() }),
|
|
167
|
+
args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
|
|
168
|
+
result: t.list(t.ref("db.Row")),
|
|
169
|
+
run: (ctx, input) => ctx.port(DbClientPort).query({
|
|
170
|
+
sql: String(input.args[0] ?? ""),
|
|
171
|
+
where: input.params.where
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/actions/restore.ts
|
|
176
|
+
/** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */
|
|
177
|
+
const restoreAction = defineAction({
|
|
178
|
+
name: "restore",
|
|
179
|
+
doc: "Restore a previously captured snapshot.",
|
|
180
|
+
args: [arg("snapshot", t.ref("db.Tables"), "What `db.snapshot` gave back.")],
|
|
181
|
+
result: t.void,
|
|
182
|
+
run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0])
|
|
183
|
+
});
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/actions/seed.ts
|
|
186
|
+
/** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */
|
|
187
|
+
const seedAction = defineAction({
|
|
188
|
+
name: "seed",
|
|
189
|
+
doc: "Load rows into the in-memory tables, grouped by table name.",
|
|
190
|
+
args: [arg("tables", t.ref("db.Tables"), "Rows to load, grouped by table name.")],
|
|
191
|
+
result: t.number,
|
|
192
|
+
run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input))
|
|
193
|
+
});
|
|
194
|
+
/**
|
|
195
|
+
* Read the tables from either call shape.
|
|
196
|
+
*
|
|
197
|
+
* `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`
|
|
198
|
+
* puts them in the options map instead, and no signature parameter names those.
|
|
199
|
+
*/
|
|
200
|
+
function readSeedData(input) {
|
|
201
|
+
const positional = input.args[0];
|
|
202
|
+
if (positional && typeof positional === "object") return positional;
|
|
203
|
+
return input.params ?? {};
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/plugin.ts
|
|
207
|
+
/**
|
|
208
|
+
* The `db` plugin: connect, query, exec, seed, snapshot and restore.
|
|
209
|
+
*
|
|
210
|
+
* Every verb reaches the database through `DbClientPort`, so the host decides
|
|
211
|
+
* whether that is the fake or a real driver. Loading fails up front unless the
|
|
212
|
+
* host offers the `net` capability.
|
|
213
|
+
*/
|
|
214
|
+
const dbPlugin = definePlugin({
|
|
215
|
+
name: "venn/db",
|
|
216
|
+
version: "0.0.0",
|
|
217
|
+
namespace: "db",
|
|
218
|
+
requires: ["net"],
|
|
219
|
+
actions: [
|
|
220
|
+
connectAction,
|
|
221
|
+
queryAction,
|
|
222
|
+
execAction,
|
|
223
|
+
seedAction,
|
|
224
|
+
defineAction({
|
|
225
|
+
name: "snapshot",
|
|
226
|
+
doc: "Capture the current in-memory state as a restorable snapshot.",
|
|
227
|
+
result: t.ref("db.Tables"),
|
|
228
|
+
run: (ctx) => ctx.port(DbClientPort).snapshot()
|
|
229
|
+
}),
|
|
230
|
+
restoreAction
|
|
231
|
+
],
|
|
232
|
+
types: { Row: RowSchema },
|
|
233
|
+
typeDefs: dbTypeDefs
|
|
234
|
+
});
|
|
235
|
+
//#endregion
|
|
236
|
+
export { DbClientPort, RowSchema, createFakeDbClient, createRealDbClient, dbPlugin, dbPlugin as default, dbTypeDefs };
|
|
237
|
+
|
|
238
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/clients/query-engine.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/port/db-client.port.ts","../src/actions/connect.ts","../src/types/row.ts","../src/types/type-defs.ts","../src/actions/exec.ts","../src/actions/query.ts","../src/actions/restore.ts","../src/actions/seed.ts","../src/actions/snapshot.ts","../src/actions/index.ts","../src/plugin.ts"],"sourcesContent":["import type { ExecArgs, QueryArgs, SeedData, TableMap } from \"../port/index.js\";\nimport type { Row } from \"../types/index.js\";\n\n// A deliberately small SQL reader for the fake: it recognises the table name and\n// little else. Enough to make a seeded test read like the real thing.\n\n/** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */\nexport function cloneTables(tables: TableMap): TableMap {\n return structuredClone(tables);\n}\n\n/** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */\nexport function selectRows(tables: TableMap, query: QueryArgs): Row[] {\n const rows = tables[tableName(query.sql, /from\\s+([a-z_]\\w*)/i)] ?? [];\n const where = query.where;\n const matched = where ? rows.filter((row) => matchesWhere(row, where)) : rows;\n return matched.map((row) => ({ ...row }));\n}\n\n/** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */\nexport function execStatement(tables: TableMap, statement: ExecArgs): number {\n const inserted = tableName(statement.sql, /insert\\s+into\\s+([a-z_]\\w*)/i);\n if (inserted) return insertRows(tables, inserted, statement.rows ?? []);\n return truncate(tables, tableName(statement.sql, /(?:truncate|delete\\s+from)\\s+([a-z_]\\w*)/i));\n}\n\n/** Append rows into named tables; returns how many rows were loaded. */\nexport function seedTables(tables: TableMap, data: SeedData): number {\n let count = 0;\n for (const [name, rows] of Object.entries(data)) {\n const copies = rows.map((row) => ({ ...row }));\n tables[name] = [...(tables[name] ?? []), ...copies];\n count += copies.length;\n }\n return count;\n}\n\nfunction insertRows(tables: TableMap, name: string, rows: Row[]): number {\n const copies = rows.map((row) => ({ ...row }));\n tables[name] = [...(tables[name] ?? []), ...copies];\n return copies.length;\n}\n\nfunction truncate(tables: TableMap, name: string): number {\n const removed = tables[name]?.length ?? 0;\n if (name) tables[name] = [];\n return removed;\n}\n\nfunction matchesWhere(row: Row, where: Row): boolean {\n return Object.entries(where).every(([key, value]) => row[key] === value);\n}\n\nfunction tableName(sql: string, pattern: RegExp): string {\n return pattern.exec(sql)?.[1] ?? \"\";\n}\n","import type { DbClient, DbSnapshot, SeedData, TableMap } from \"../port/index.js\";\nimport { cloneTables, execStatement, seedTables, selectRows } from \"./query-engine.js\";\n\ninterface DbState {\n tables: TableMap;\n}\n\n/**\n * The in-memory `DbClient`: deterministic tables, no connection, no I/O.\n *\n * @param args.tables Rows to start from. Copied, so the caller's object is never mutated.\n */\nexport function createFakeDbClient(args: { tables?: SeedData } = {}): DbClient {\n const state: DbState = { tables: cloneTables(args.tables ?? {}) };\n return {\n connect: () => Promise.resolve(),\n query: (query) => Promise.resolve(selectRows(state.tables, query)),\n exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),\n seed: (data) => Promise.resolve(seedTables(state.tables, data)),\n snapshot: () => Promise.resolve(cloneTables(state.tables)),\n restore: (snapshot) => restore(state, snapshot),\n };\n}\n\nfunction restore(state: DbState, snapshot: DbSnapshot): Promise<void> {\n state.tables = cloneTables(snapshot);\n return Promise.resolve();\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { DbClient } from \"../port/index.js\";\n\n/**\n * The driver-backed `DbClient`. Not implemented in this build.\n *\n * @throws VennError `VN8090` from every method, so a host that binds it fails\n * loudly instead of quietly answering with nothing.\n */\nexport function createRealDbClient(): DbClient {\n return {\n connect: () => notImplemented(),\n query: () => notImplemented(),\n exec: () => notImplemented(),\n seed: () => notImplemented(),\n snapshot: () => notImplemented(),\n restore: () => notImplemented(),\n };\n}\n\nfunction notImplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"Db real client not implemented in this build\",\n });\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { DbClient } from \"./db-client.types.js\";\n\n/**\n * The port every `db` verb reaches the database through.\n *\n * Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires\n * the `net` capability, so a host without it is refused at load time rather\n * than failing mid-run.\n */\nexport const DbClientPort: Port<DbClient> = {\n id: \"venn.port.db-client\",\n version: 1,\n requires: [\"net\"],\n methods: [\"connect\", \"query\", \"exec\", \"seed\", \"snapshot\", \"restore\"],\n};\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\n\n/** `db.connect(url)`: open the connection the other `db` verbs then use. */\nexport const connectAction: ActionDefinition = defineAction({\n name: \"connect\",\n doc: \"Connect to a database by URL.\",\n // It answers with the URL, not a handle: the connection lives behind the port,\n // and every other verb reaches it from there.\n args: [arg(\"url\", t.string, \"Where the database is.\")],\n result: t.string,\n run: async (ctx, input) => {\n const url = String(input.args[0] ?? \"\");\n await ctx.port(DbClientPort).connect(url);\n return url;\n },\n});\n","import { type ZodType, z } from \"@venn-lang/sdk\";\n\n/** The nominal `Row` type: an opaque record of column values. */\nexport type Row = Record<string, unknown>;\n\n/** Zod schema for the nominal `Row` type the plugin contributes. */\nexport const RowSchema: ZodType<Row> = z.record(z.string(), z.unknown());\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.\n *\n * Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which\n * `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.\n */\nexport const dbTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * One row. Which columns it has is the query's business, not the plugin's, so\n * the type says only what is always true: names to values.\n */\n Row: t.map(t.dynamic),\n /** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */\n Tables: t.map(t.list(t.ref(\"db.Row\"))),\n};\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\nimport { RowSchema } from \"../types/index.js\";\n\nconst execParams = z.object({ rows: z.array(RowSchema).optional() });\n\n/** `db.exec(\"TRUNCATE …\", { rows })`: mutate the tables and answer with the affected count. */\nexport const execAction: ActionDefinition = defineAction({\n name: \"exec\",\n doc: \"Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.\",\n params: execParams,\n args: [arg(\"sql\", t.string, \"The statement to run. Placeholders go in the options.\")],\n result: t.number,\n run: (ctx, input) =>\n ctx.port(DbClientPort).exec({ sql: String(input.args[0] ?? \"\"), rows: input.params.rows }),\n});\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\nimport { RowSchema } from \"../types/index.js\";\n\nconst queryParams = z.object({ where: RowSchema.optional() });\n\n/** `db.query(\"SELECT …\", { where })`: run a SELECT and answer with the matching rows. */\nexport const queryAction: ActionDefinition = defineAction({\n name: \"query\",\n doc: \"Run a SELECT and return the matching rows.\",\n params: queryParams,\n args: [arg(\"sql\", t.string, \"The statement to run. Placeholders go in the options.\")],\n result: t.list(t.ref(\"db.Row\")),\n run: (ctx, input) =>\n ctx.port(DbClientPort).query({ sql: String(input.args[0] ?? \"\"), where: input.params.where }),\n});\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort, type DbSnapshot } from \"../port/index.js\";\n\n/** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */\nexport const restoreAction: ActionDefinition = defineAction({\n name: \"restore\",\n doc: \"Restore a previously captured snapshot.\",\n args: [arg(\"snapshot\", t.ref(\"db.Tables\"), \"What `db.snapshot` gave back.\")],\n result: t.void,\n run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0] as DbSnapshot),\n});\n","import { type ActionDefinition, type ActionInput, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort, type SeedData } from \"../port/index.js\";\n\n/** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */\nexport const seedAction: ActionDefinition = defineAction({\n name: \"seed\",\n doc: \"Load rows into the in-memory tables, grouped by table name.\",\n args: [arg(\"tables\", t.ref(\"db.Tables\"), \"Rows to load, grouped by table name.\")],\n result: t.number,\n run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input)),\n});\n\n/**\n * Read the tables from either call shape.\n *\n * `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`\n * puts them in the options map instead, and no signature parameter names those.\n */\nfunction readSeedData(input: ActionInput<unknown>): SeedData {\n const positional = input.args[0];\n if (positional && typeof positional === \"object\") return positional as SeedData;\n return (input.params ?? {}) as SeedData;\n}\n","import { type ActionDefinition, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\n\n/** `db.snapshot()`: copy the current tables into a value `db.restore` accepts. */\nexport const snapshotAction: ActionDefinition = defineAction({\n name: \"snapshot\",\n doc: \"Capture the current in-memory state as a restorable snapshot.\",\n // `db.Tables`, not `DbSnapshot`: that alias lives in the port's TypeScript, and\n // a script can only name what the plugin publishes.\n result: t.ref(\"db.Tables\"),\n run: (ctx) => ctx.port(DbClientPort).snapshot(),\n});\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { connectAction } from \"./connect.js\";\nimport { execAction } from \"./exec.js\";\nimport { queryAction } from \"./query.js\";\nimport { restoreAction } from \"./restore.js\";\nimport { seedAction } from \"./seed.js\";\nimport { snapshotAction } from \"./snapshot.js\";\n\n/** The db namespace's verbs. Adding one is a single line here. */\nexport const dbActions: ActionDefinition[] = [\n connectAction,\n queryAction,\n execAction,\n seedAction,\n snapshotAction,\n restoreAction,\n];\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { dbActions } from \"./actions/index.js\";\nimport { dbTypeDefs, RowSchema } from \"./types/index.js\";\n\n/**\n * The `db` plugin: connect, query, exec, seed, snapshot and restore.\n *\n * Every verb reaches the database through `DbClientPort`, so the host decides\n * whether that is the fake or a real driver. Loading fails up front unless the\n * host offers the `net` capability.\n */\nexport const dbPlugin: PluginDefinition = definePlugin({\n name: \"venn/db\",\n version: \"0.0.0\",\n namespace: \"db\",\n requires: [\"net\"],\n actions: dbActions,\n types: { Row: RowSchema },\n typeDefs: dbTypeDefs,\n});\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@venn-lang/db",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The db namespace: seed tables, run statements and snapshot state, all through the DbClient port.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"venn",
|
|
7
|
+
"testing",
|
|
8
|
+
"e2e",
|
|
9
|
+
"db"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-db#readme",
|
|
12
|
+
"bugs": "https://github.com/venn-lang/venn/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/venn-lang/venn.git",
|
|
16
|
+
"directory": "packages/std-db"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Vinicius Borges",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"development": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"!src/**/*.test.ts",
|
|
34
|
+
"!src/**/*.suite.ts"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@venn-lang/contracts": "0.1.0",
|
|
41
|
+
"@venn-lang/sdk": "0.1.0",
|
|
42
|
+
"@venn-lang/types": "0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsdown": "^0.22.14",
|
|
46
|
+
"typescript": "^7.0.2",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort } from "../port/index.js";
|
|
4
|
+
|
|
5
|
+
/** `db.connect(url)`: open the connection the other `db` verbs then use. */
|
|
6
|
+
export const connectAction: ActionDefinition = defineAction({
|
|
7
|
+
name: "connect",
|
|
8
|
+
doc: "Connect to a database by URL.",
|
|
9
|
+
// It answers with the URL, not a handle: the connection lives behind the port,
|
|
10
|
+
// and every other verb reaches it from there.
|
|
11
|
+
args: [arg("url", t.string, "Where the database is.")],
|
|
12
|
+
result: t.string,
|
|
13
|
+
run: async (ctx, input) => {
|
|
14
|
+
const url = String(input.args[0] ?? "");
|
|
15
|
+
await ctx.port(DbClientPort).connect(url);
|
|
16
|
+
return url;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort } from "../port/index.js";
|
|
4
|
+
import { RowSchema } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
const execParams = z.object({ rows: z.array(RowSchema).optional() });
|
|
7
|
+
|
|
8
|
+
/** `db.exec("TRUNCATE …", { rows })`: mutate the tables and answer with the affected count. */
|
|
9
|
+
export const execAction: ActionDefinition = defineAction({
|
|
10
|
+
name: "exec",
|
|
11
|
+
doc: "Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.",
|
|
12
|
+
params: execParams,
|
|
13
|
+
args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
|
|
14
|
+
result: t.number,
|
|
15
|
+
run: (ctx, input) =>
|
|
16
|
+
ctx.port(DbClientPort).exec({ sql: String(input.args[0] ?? ""), rows: input.params.rows }),
|
|
17
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ActionDefinition } from "@venn-lang/sdk";
|
|
2
|
+
import { connectAction } from "./connect.js";
|
|
3
|
+
import { execAction } from "./exec.js";
|
|
4
|
+
import { queryAction } from "./query.js";
|
|
5
|
+
import { restoreAction } from "./restore.js";
|
|
6
|
+
import { seedAction } from "./seed.js";
|
|
7
|
+
import { snapshotAction } from "./snapshot.js";
|
|
8
|
+
|
|
9
|
+
/** The db namespace's verbs. Adding one is a single line here. */
|
|
10
|
+
export const dbActions: ActionDefinition[] = [
|
|
11
|
+
connectAction,
|
|
12
|
+
queryAction,
|
|
13
|
+
execAction,
|
|
14
|
+
seedAction,
|
|
15
|
+
snapshotAction,
|
|
16
|
+
restoreAction,
|
|
17
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort } from "../port/index.js";
|
|
4
|
+
import { RowSchema } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
const queryParams = z.object({ where: RowSchema.optional() });
|
|
7
|
+
|
|
8
|
+
/** `db.query("SELECT …", { where })`: run a SELECT and answer with the matching rows. */
|
|
9
|
+
export const queryAction: ActionDefinition = defineAction({
|
|
10
|
+
name: "query",
|
|
11
|
+
doc: "Run a SELECT and return the matching rows.",
|
|
12
|
+
params: queryParams,
|
|
13
|
+
args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
|
|
14
|
+
result: t.list(t.ref("db.Row")),
|
|
15
|
+
run: (ctx, input) =>
|
|
16
|
+
ctx.port(DbClientPort).query({ sql: String(input.args[0] ?? ""), where: input.params.where }),
|
|
17
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort, type DbSnapshot } from "../port/index.js";
|
|
4
|
+
|
|
5
|
+
/** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */
|
|
6
|
+
export const restoreAction: ActionDefinition = defineAction({
|
|
7
|
+
name: "restore",
|
|
8
|
+
doc: "Restore a previously captured snapshot.",
|
|
9
|
+
args: [arg("snapshot", t.ref("db.Tables"), "What `db.snapshot` gave back.")],
|
|
10
|
+
result: t.void,
|
|
11
|
+
run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0] as DbSnapshot),
|
|
12
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ActionDefinition, type ActionInput, arg, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort, type SeedData } from "../port/index.js";
|
|
4
|
+
|
|
5
|
+
/** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */
|
|
6
|
+
export const seedAction: ActionDefinition = defineAction({
|
|
7
|
+
name: "seed",
|
|
8
|
+
doc: "Load rows into the in-memory tables, grouped by table name.",
|
|
9
|
+
args: [arg("tables", t.ref("db.Tables"), "Rows to load, grouped by table name.")],
|
|
10
|
+
result: t.number,
|
|
11
|
+
run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input)),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the tables from either call shape.
|
|
16
|
+
*
|
|
17
|
+
* `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`
|
|
18
|
+
* puts them in the options map instead, and no signature parameter names those.
|
|
19
|
+
*/
|
|
20
|
+
function readSeedData(input: ActionInput<unknown>): SeedData {
|
|
21
|
+
const positional = input.args[0];
|
|
22
|
+
if (positional && typeof positional === "object") return positional as SeedData;
|
|
23
|
+
return (input.params ?? {}) as SeedData;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ActionDefinition, defineAction } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { DbClientPort } from "../port/index.js";
|
|
4
|
+
|
|
5
|
+
/** `db.snapshot()`: copy the current tables into a value `db.restore` accepts. */
|
|
6
|
+
export const snapshotAction: ActionDefinition = defineAction({
|
|
7
|
+
name: "snapshot",
|
|
8
|
+
doc: "Capture the current in-memory state as a restorable snapshot.",
|
|
9
|
+
// `db.Tables`, not `DbSnapshot`: that alias lives in the port's TypeScript, and
|
|
10
|
+
// a script can only name what the plugin publishes.
|
|
11
|
+
result: t.ref("db.Tables"),
|
|
12
|
+
run: (ctx) => ctx.port(DbClientPort).snapshot(),
|
|
13
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DbClient, DbSnapshot, SeedData, TableMap } from "../port/index.js";
|
|
2
|
+
import { cloneTables, execStatement, seedTables, selectRows } from "./query-engine.js";
|
|
3
|
+
|
|
4
|
+
interface DbState {
|
|
5
|
+
tables: TableMap;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The in-memory `DbClient`: deterministic tables, no connection, no I/O.
|
|
10
|
+
*
|
|
11
|
+
* @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
|
|
12
|
+
*/
|
|
13
|
+
export function createFakeDbClient(args: { tables?: SeedData } = {}): DbClient {
|
|
14
|
+
const state: DbState = { tables: cloneTables(args.tables ?? {}) };
|
|
15
|
+
return {
|
|
16
|
+
connect: () => Promise.resolve(),
|
|
17
|
+
query: (query) => Promise.resolve(selectRows(state.tables, query)),
|
|
18
|
+
exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),
|
|
19
|
+
seed: (data) => Promise.resolve(seedTables(state.tables, data)),
|
|
20
|
+
snapshot: () => Promise.resolve(cloneTables(state.tables)),
|
|
21
|
+
restore: (snapshot) => restore(state, snapshot),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function restore(state: DbState, snapshot: DbSnapshot): Promise<void> {
|
|
26
|
+
state.tables = cloneTables(snapshot);
|
|
27
|
+
return Promise.resolve();
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ExecArgs, QueryArgs, SeedData, TableMap } from "../port/index.js";
|
|
2
|
+
import type { Row } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
// A deliberately small SQL reader for the fake: it recognises the table name and
|
|
5
|
+
// little else. Enough to make a seeded test read like the real thing.
|
|
6
|
+
|
|
7
|
+
/** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */
|
|
8
|
+
export function cloneTables(tables: TableMap): TableMap {
|
|
9
|
+
return structuredClone(tables);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */
|
|
13
|
+
export function selectRows(tables: TableMap, query: QueryArgs): Row[] {
|
|
14
|
+
const rows = tables[tableName(query.sql, /from\s+([a-z_]\w*)/i)] ?? [];
|
|
15
|
+
const where = query.where;
|
|
16
|
+
const matched = where ? rows.filter((row) => matchesWhere(row, where)) : rows;
|
|
17
|
+
return matched.map((row) => ({ ...row }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */
|
|
21
|
+
export function execStatement(tables: TableMap, statement: ExecArgs): number {
|
|
22
|
+
const inserted = tableName(statement.sql, /insert\s+into\s+([a-z_]\w*)/i);
|
|
23
|
+
if (inserted) return insertRows(tables, inserted, statement.rows ?? []);
|
|
24
|
+
return truncate(tables, tableName(statement.sql, /(?:truncate|delete\s+from)\s+([a-z_]\w*)/i));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Append rows into named tables; returns how many rows were loaded. */
|
|
28
|
+
export function seedTables(tables: TableMap, data: SeedData): number {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
31
|
+
const copies = rows.map((row) => ({ ...row }));
|
|
32
|
+
tables[name] = [...(tables[name] ?? []), ...copies];
|
|
33
|
+
count += copies.length;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function insertRows(tables: TableMap, name: string, rows: Row[]): number {
|
|
39
|
+
const copies = rows.map((row) => ({ ...row }));
|
|
40
|
+
tables[name] = [...(tables[name] ?? []), ...copies];
|
|
41
|
+
return copies.length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function truncate(tables: TableMap, name: string): number {
|
|
45
|
+
const removed = tables[name]?.length ?? 0;
|
|
46
|
+
if (name) tables[name] = [];
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchesWhere(row: Row, where: Row): boolean {
|
|
51
|
+
return Object.entries(where).every(([key, value]) => row[key] === value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tableName(sql: string, pattern: RegExp): string {
|
|
55
|
+
return pattern.exec(sql)?.[1] ?? "";
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { VennError } from "@venn-lang/contracts";
|
|
2
|
+
import type { DbClient } from "../port/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The driver-backed `DbClient`. Not implemented in this build.
|
|
6
|
+
*
|
|
7
|
+
* @throws VennError `VN8090` from every method, so a host that binds it fails
|
|
8
|
+
* loudly instead of quietly answering with nothing.
|
|
9
|
+
*/
|
|
10
|
+
export function createRealDbClient(): DbClient {
|
|
11
|
+
return {
|
|
12
|
+
connect: () => notImplemented(),
|
|
13
|
+
query: () => notImplemented(),
|
|
14
|
+
exec: () => notImplemented(),
|
|
15
|
+
seed: () => notImplemented(),
|
|
16
|
+
snapshot: () => notImplemented(),
|
|
17
|
+
restore: () => notImplemented(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function notImplemented(): never {
|
|
22
|
+
throw new VennError({
|
|
23
|
+
code: "VN8090",
|
|
24
|
+
message: "Db real client not implemented in this build",
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @venn-lang/db: table verbs and a nominal Row type, riding the DbClient port so the
|
|
2
|
+
// database is injected at the host boundary rather than baked into the plugin.
|
|
3
|
+
|
|
4
|
+
export * from "./clients/index.js";
|
|
5
|
+
export { dbPlugin, dbPlugin as default } from "./plugin.js";
|
|
6
|
+
export * from "./port/index.js";
|
|
7
|
+
export * from "./types/index.js";
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
|
|
2
|
+
import { dbActions } from "./actions/index.js";
|
|
3
|
+
import { dbTypeDefs, RowSchema } from "./types/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `db` plugin: connect, query, exec, seed, snapshot and restore.
|
|
7
|
+
*
|
|
8
|
+
* Every verb reaches the database through `DbClientPort`, so the host decides
|
|
9
|
+
* whether that is the fake or a real driver. Loading fails up front unless the
|
|
10
|
+
* host offers the `net` capability.
|
|
11
|
+
*/
|
|
12
|
+
export const dbPlugin: PluginDefinition = definePlugin({
|
|
13
|
+
name: "venn/db",
|
|
14
|
+
version: "0.0.0",
|
|
15
|
+
namespace: "db",
|
|
16
|
+
requires: ["net"],
|
|
17
|
+
actions: dbActions,
|
|
18
|
+
types: { Row: RowSchema },
|
|
19
|
+
typeDefs: dbTypeDefs,
|
|
20
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Port } from "@venn-lang/contracts";
|
|
2
|
+
import type { DbClient } from "./db-client.types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The port every `db` verb reaches the database through.
|
|
6
|
+
*
|
|
7
|
+
* Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
|
|
8
|
+
* the `net` capability, so a host without it is refused at load time rather
|
|
9
|
+
* than failing mid-run.
|
|
10
|
+
*/
|
|
11
|
+
export const DbClientPort: Port<DbClient> = {
|
|
12
|
+
id: "venn.port.db-client",
|
|
13
|
+
version: 1,
|
|
14
|
+
requires: ["net"],
|
|
15
|
+
methods: ["connect", "query", "exec", "seed", "snapshot", "restore"],
|
|
16
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Row } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
/** Arguments for {@link DbClient.query}: a SQL string plus an optional filter. */
|
|
4
|
+
export interface QueryArgs {
|
|
5
|
+
sql: string;
|
|
6
|
+
where?: Row;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Arguments for {@link DbClient.exec}: a mutating statement plus optional rows. */
|
|
10
|
+
export interface ExecArgs {
|
|
11
|
+
sql: string;
|
|
12
|
+
rows?: Row[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Rows keyed by table name. The shape seeds and snapshots share. */
|
|
16
|
+
export type TableMap = Record<string, Row[]>;
|
|
17
|
+
|
|
18
|
+
/** Rows to load, keyed by table name. */
|
|
19
|
+
export type SeedData = TableMap;
|
|
20
|
+
|
|
21
|
+
/** A captured copy of every table. Detached from live state, so it can be restored later. */
|
|
22
|
+
export type DbSnapshot = TableMap;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The contract a database is reached through. Actions call it via
|
|
26
|
+
* `ctx.port(DbClientPort)` and never touch a driver directly.
|
|
27
|
+
*/
|
|
28
|
+
export interface DbClient {
|
|
29
|
+
connect(url: string): Promise<void>;
|
|
30
|
+
query(args: QueryArgs): Promise<Row[]>;
|
|
31
|
+
exec(args: ExecArgs): Promise<number>;
|
|
32
|
+
seed(data: SeedData): Promise<number>;
|
|
33
|
+
snapshot(): Promise<DbSnapshot>;
|
|
34
|
+
restore(snapshot: DbSnapshot): Promise<void>;
|
|
35
|
+
}
|
package/src/types/row.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ZodType, z } from "@venn-lang/sdk";
|
|
2
|
+
|
|
3
|
+
/** The nominal `Row` type: an opaque record of column values. */
|
|
4
|
+
export type Row = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/** Zod schema for the nominal `Row` type the plugin contributes. */
|
|
7
|
+
export const RowSchema: ZodType<Row> = z.record(z.string(), z.unknown());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type TypeSpec, t } from "@venn-lang/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
|
|
5
|
+
*
|
|
6
|
+
* Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
|
|
7
|
+
* `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
|
|
8
|
+
*/
|
|
9
|
+
export const dbTypeDefs: Readonly<Record<string, TypeSpec>> = {
|
|
10
|
+
/**
|
|
11
|
+
* One row. Which columns it has is the query's business, not the plugin's, so
|
|
12
|
+
* the type says only what is always true: names to values.
|
|
13
|
+
*/
|
|
14
|
+
Row: t.map(t.dynamic),
|
|
15
|
+
/** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */
|
|
16
|
+
Tables: t.map(t.list(t.ref("db.Row"))),
|
|
17
|
+
};
|