@yydb/yydb 0.0.1 → 0.0.2
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/README.md +77 -17
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.js +27 -0
- package/dist/cli/generate-cmd.d.ts +3 -0
- package/dist/cli/generate-cmd.js +25 -0
- package/dist/cli/info-cmd.d.ts +3 -0
- package/dist/cli/info-cmd.js +10 -0
- package/dist/cli/init-cmd.d.ts +3 -0
- package/dist/cli/init-cmd.js +18 -0
- package/dist/cli/options.d.ts +4 -0
- package/dist/cli/options.js +14 -0
- package/dist/cli/serve-cmd.d.ts +3 -0
- package/dist/cli/serve-cmd.js +14 -0
- package/dist/cli/version-cmd.d.ts +3 -0
- package/dist/cli/version-cmd.js +8 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/database.d.ts +20 -2
- package/dist/database.js +70 -28
- package/dist/generator/emit-typescript.d.ts +3 -0
- package/dist/generator/emit-typescript.js +26 -0
- package/dist/generator/fixtures/setting.generated.d.ts +13 -0
- package/dist/generator/fixtures/setting.generated.js +3 -0
- package/dist/generator/index.d.ts +6 -0
- package/dist/generator/index.js +10 -0
- package/dist/generator/ir.d.ts +20 -0
- package/dist/generator/ir.js +12 -0
- package/dist/generator/parse-vos.d.ts +6 -0
- package/dist/generator/parse-vos.js +149 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/micro.d.ts +63 -0
- package/dist/micro.js +160 -0
- package/dist/native.d.ts +9 -0
- package/dist/native.js +66 -0
- package/dist/resolve-bin.d.ts +9 -4
- package/dist/resolve-bin.js +31 -47
- package/dist/udf-types.d.ts +26 -0
- package/dist/udf-types.js +16 -0
- package/package.json +25 -13
package/README.md
CHANGED
|
@@ -1,30 +1,90 @@
|
|
|
1
1
|
# `@yydb/yydb`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**All-in-one YYDB for Node.js: CLI, embedded engine, and local `serve`.**
|
|
4
|
+
|
|
5
|
+
Open a `.yydb` file with one dependency — `@yydb/yydb` spawns a private loopback server and talks to it over the YY
|
|
6
|
+
wire protocol. For a **lightweight client only** (browser, renderer, or an app that already runs `yydb serve`), use
|
|
7
|
+
[`@yydb/yydb-client`](../yydb-client).
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@yydb/yydb)
|
|
10
|
+
[](https://www.npmjs.com/package/@yydb/yydb)
|
|
11
|
+
[](https://github.com/yy-database/yydb/actions/workflows/check-rust.yml)
|
|
12
|
+
[](https://github.com/yy-database/yydb/actions/workflows/check-typescript.yml)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @yydb/yydb
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Node.js 18 or newer is supported. The matching `yydb-napi` binding is installed as an optional platform dependency and
|
|
21
|
+
is
|
|
22
|
+
started on a private loopback endpoint when `Database.open()` runs.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
4
25
|
|
|
5
26
|
```ts
|
|
6
|
-
import {
|
|
27
|
+
import {Database} from "@yydb/yydb";
|
|
28
|
+
|
|
29
|
+
const db = await Database.open("./data/app.yydb");
|
|
30
|
+
|
|
31
|
+
await db.ensureSchema(
|
|
32
|
+
1,
|
|
33
|
+
`table Setting {
|
|
34
|
+
@@id: uuid,
|
|
35
|
+
key: utf8,
|
|
36
|
+
value: utf8,
|
|
37
|
+
}`,
|
|
38
|
+
);
|
|
7
39
|
|
|
8
|
-
const db = await Database.open("app.yydb");
|
|
9
|
-
await db.ensureSchema(1, "table Setting { @@id: uuid, key: utf8, value: utf8 }");
|
|
10
40
|
await db.put("theme", "dark");
|
|
41
|
+
const value = await db.get("theme");
|
|
42
|
+
console.log(value && new TextDecoder().decode(value));
|
|
43
|
+
|
|
11
44
|
await db.close();
|
|
12
45
|
```
|
|
13
46
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
The database file is created if it does not exist. The VOS schema and its version are stored with the data, so reopening
|
|
48
|
+
the file does not depend on a separate schema registry.
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const db = await Database.open(path, options ?);
|
|
54
|
+
|
|
55
|
+
await db.ensureSchema(version, vosDocument);
|
|
56
|
+
await db.getSchema();
|
|
57
|
+
await db.put(key, stringOrBytes);
|
|
58
|
+
await db.get(key);
|
|
59
|
+
await db.info();
|
|
60
|
+
await db.serverVersion();
|
|
61
|
+
await db.close();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`OpenOptions` supports `cli` (an explicit CLI script path), `port` (a preferred loopback port), and `serveArgs` (extra
|
|
65
|
+
arguments forwarded to `yydb serve`). `binary` is a deprecated alias for `cli`.
|
|
66
|
+
|
|
67
|
+
## Use a different package when
|
|
68
|
+
|
|
69
|
+
| Need | Package |
|
|
70
|
+
|----------------------------------------|-------------------------------------------|
|
|
71
|
+
| Node all-in-one (CLI + engine + serve) | **`@yydb/yydb`** (this package) |
|
|
72
|
+
| Lightweight wire client only | [`@yydb/yydb-client`](../yydb-client) |
|
|
73
|
+
| In-process Rust embed | [`yydb`](https://crates.io/crates/yydb) |
|
|
74
|
+
| Rust wire server crate (used by N-API) | [`yydb-server`](../../crates/yydb-server) |
|
|
75
|
+
| Rust remote client | [`yydb-client`](../../crates/yydb-client) |
|
|
76
|
+
|
|
77
|
+
For Electron, the main process can use `@yydb/yydb`; renderer code can use
|
|
78
|
+
`@yydb/yydb-client` against the main process endpoint.
|
|
18
79
|
|
|
19
|
-
|
|
20
|
-
| --- | --- |
|
|
21
|
-
| `@yydb/yydb` | High-level `Database.open` (this package) |
|
|
22
|
-
| `@yydb/yydb-client` | Low-level YY wire (also used by WebUI) |
|
|
23
|
-
| `@yydb/yydb-<os>-<arch>` | Prebuilt `yydb` binary (publish pipeline) |
|
|
80
|
+
## Security
|
|
24
81
|
|
|
25
|
-
|
|
82
|
+
The managed engine listens on `127.0.0.1` and has no authentication layer. Keep the endpoint private and use the
|
|
83
|
+
operating system's file permissions for the `.yydb` file.
|
|
26
84
|
|
|
27
|
-
|
|
28
|
-
the engine (e.g. Electron main used `@yydb/yydb`, renderer talks WS).
|
|
85
|
+
## Links
|
|
29
86
|
|
|
30
|
-
|
|
87
|
+
- [YYDB overview](https://github.com/yy-database/yydb.rs)
|
|
88
|
+
- [User guides](https://github.com/yy-database/yydb.rs/tree/dev/projects/packages/homepage/documentation/zh-hans)
|
|
89
|
+
- [API documentation](https://docs.rs/yydb)
|
|
90
|
+
- [License](https://github.com/yy-database/yydb.rs/blob/dev/License.md)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCli(argv: string[]): Promise<number>;
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createCli } from '@vmz/commander';
|
|
4
|
+
import { registerGenerateCommand } from './generate-cmd.js';
|
|
5
|
+
import { registerInfoCommand } from './info-cmd.js';
|
|
6
|
+
import { registerInitCommand } from './init-cmd.js';
|
|
7
|
+
import { registerServeCommand } from './serve-cmd.js';
|
|
8
|
+
import { registerVersionCommand } from './version-cmd.js';
|
|
9
|
+
function buildYydbCli() {
|
|
10
|
+
const localesRoot = join(dirname(fileURLToPath(import.meta.url)), '../../locales');
|
|
11
|
+
const cli = createCli('yydb').locales(localesRoot).intro('cli.intro');
|
|
12
|
+
registerVersionCommand(cli);
|
|
13
|
+
registerInitCommand(cli);
|
|
14
|
+
registerInfoCommand(cli);
|
|
15
|
+
registerServeCommand(cli);
|
|
16
|
+
registerGenerateCommand(cli);
|
|
17
|
+
return cli;
|
|
18
|
+
}
|
|
19
|
+
export async function runCli(argv) {
|
|
20
|
+
try {
|
|
21
|
+
return await buildYydbCli().parse(argv);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { generateTypeScript } from '../generator/index.js';
|
|
4
|
+
import { requiredPositional, str } from './options.js';
|
|
5
|
+
export function registerGenerateCommand(cli) {
|
|
6
|
+
cli.command('generate', 'cli.cmd.generate')
|
|
7
|
+
.option('-o, --output <path>', 'cli.opt.output')
|
|
8
|
+
.option('--schema-version <n>', 'cli.opt.schema-version')
|
|
9
|
+
.action((options) => cmdGenerate(options));
|
|
10
|
+
}
|
|
11
|
+
export function cmdGenerate(options) {
|
|
12
|
+
const input = requiredPositional(options, 'generate');
|
|
13
|
+
const output = str(options, 'output');
|
|
14
|
+
if (!output) {
|
|
15
|
+
throw new Error('generate requires --output <path>');
|
|
16
|
+
}
|
|
17
|
+
const schemaVersion = Number.parseInt(str(options, 'schema-version') ?? '1', 10);
|
|
18
|
+
const source = fs.readFileSync(input, 'utf8');
|
|
19
|
+
const ts = generateTypeScript(source, schemaVersion);
|
|
20
|
+
const outPath = path.resolve(output);
|
|
21
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(outPath, ts);
|
|
23
|
+
console.log(`wrote ${outPath}`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadNative } from '../native.js';
|
|
2
|
+
import { requiredPositional } from './options.js';
|
|
3
|
+
export function registerInfoCommand(cli) {
|
|
4
|
+
cli.command('info', 'cli.cmd.info').action((options) => cmdInfo(options));
|
|
5
|
+
}
|
|
6
|
+
export function cmdInfo(options) {
|
|
7
|
+
const dbPath = requiredPositional(options, 'info');
|
|
8
|
+
console.log(loadNative().infoText(dbPath));
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadNative } from '../native.js';
|
|
2
|
+
import { requiredPositional, str } from './options.js';
|
|
3
|
+
export function registerInitCommand(cli) {
|
|
4
|
+
cli.command('init', 'cli.cmd.init')
|
|
5
|
+
.option('--schema-version <n>', 'cli.opt.schema-version')
|
|
6
|
+
.option('--schema-file <path>', 'cli.opt.schema-file')
|
|
7
|
+
.action((options) => cmdInit(options));
|
|
8
|
+
}
|
|
9
|
+
export function cmdInit(options) {
|
|
10
|
+
const dbPath = requiredPositional(options, 'init');
|
|
11
|
+
const schemaVersion = Number.parseInt(str(options, 'schema-version') ?? '1', 10);
|
|
12
|
+
const schemaFile = str(options, 'schema-file');
|
|
13
|
+
const native = loadNative();
|
|
14
|
+
const schemaDocument = schemaFile ? native.readSchemaFile(schemaFile) : null;
|
|
15
|
+
native.initDb(dbPath, schemaVersion, schemaDocument);
|
|
16
|
+
console.log(`initialized ${dbPath}`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ParsedOptions } from '@vmz/commander';
|
|
2
|
+
export declare function str(options: ParsedOptions, key: string): string | undefined;
|
|
3
|
+
export declare function flag(options: ParsedOptions, key: string): boolean;
|
|
4
|
+
export declare function requiredPositional(options: ParsedOptions, label: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function str(options, key) {
|
|
2
|
+
const value = options[key];
|
|
3
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
export function flag(options, key) {
|
|
6
|
+
return options[key] === true;
|
|
7
|
+
}
|
|
8
|
+
export function requiredPositional(options, label) {
|
|
9
|
+
const value = options._[0];
|
|
10
|
+
if (!value) {
|
|
11
|
+
throw new Error(`${label} requires a path argument`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { loadNative } from '../native.js';
|
|
2
|
+
import { flag, requiredPositional, str } from './options.js';
|
|
3
|
+
export function registerServeCommand(cli) {
|
|
4
|
+
cli.command('serve', 'cli.cmd.serve')
|
|
5
|
+
.option('--bind <host:port>', 'cli.opt.bind')
|
|
6
|
+
.option('--insecure-bind', 'cli.opt.insecure-bind')
|
|
7
|
+
.action((options) => cmdServe(options));
|
|
8
|
+
}
|
|
9
|
+
export function cmdServe(options) {
|
|
10
|
+
const dbPath = requiredPositional(options, 'serve');
|
|
11
|
+
const bind = str(options, 'bind') ?? '127.0.0.1:7700';
|
|
12
|
+
loadNative().serve(dbPath, bind, flag(options, 'insecure-bind'));
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
package/dist/database.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { defineMicro as buildMicro, type DefinedMicro, type MicroHandle } from './micro.js';
|
|
2
|
+
import type { UdfTypeDescriptor } from './udf-types.js';
|
|
3
|
+
export type { SchemaVersion } from '@yydb/yydb-client';
|
|
2
4
|
export interface OpenOptions {
|
|
3
|
-
/** Override
|
|
5
|
+
/** Override CLI script path (tests / special installs). */
|
|
6
|
+
cli?: string;
|
|
7
|
+
/** @deprecated Use `cli`. */
|
|
4
8
|
binary?: string;
|
|
5
9
|
/** Prefer a specific loopback port; default = ephemeral free port. */
|
|
6
10
|
port?: number;
|
|
@@ -16,6 +20,7 @@ export declare class Database {
|
|
|
16
20
|
readonly endpoint: string;
|
|
17
21
|
private client;
|
|
18
22
|
private child;
|
|
23
|
+
private readonly microRegistry;
|
|
19
24
|
private constructor();
|
|
20
25
|
/**
|
|
21
26
|
* Open (create) a `.yydb` file. Spawns the engine on `127.0.0.1` automatically.
|
|
@@ -27,6 +32,19 @@ export declare class Database {
|
|
|
27
32
|
ensureSchema(version: number, document: string): Promise<void>;
|
|
28
33
|
get(key: string): Promise<Uint8Array | null>;
|
|
29
34
|
put(key: string, value: Uint8Array | string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Build a typed session-local micro definition. Does not register it with
|
|
37
|
+
* the engine until [`registerMicro`](./database.ts) is called.
|
|
38
|
+
*/
|
|
39
|
+
defineMicro<TArgs extends readonly UdfTypeDescriptor[], TReturn extends UdfTypeDescriptor>(definition: Parameters<typeof buildMicro<TArgs, TReturn>>[0]): DefinedMicro<TArgs, TReturn>;
|
|
40
|
+
/**
|
|
41
|
+
* Install a [`defineMicro`](./database.ts) definition into the current
|
|
42
|
+
* session registry and sync metadata to the private engine. Host micros are
|
|
43
|
+
* session-local and never written to `.yydb`.
|
|
44
|
+
*/
|
|
45
|
+
registerMicro<TArgs extends readonly UdfTypeDescriptor[], TReturn extends UdfTypeDescriptor>(definition: DefinedMicro<TArgs, TReturn>): Promise<MicroHandle>;
|
|
46
|
+
/** Invoke a registered scalar UDF on the private engine. */
|
|
47
|
+
callScalar(name: string, version: number, args: readonly (string | number | boolean | null)[]): Promise<string | number | boolean | null>;
|
|
30
48
|
/** Stop the private engine process. */
|
|
31
49
|
close(): Promise<void>;
|
|
32
50
|
}
|
package/dist/database.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import fs from
|
|
3
|
-
import net from
|
|
4
|
-
import path from
|
|
5
|
-
import { connect } from
|
|
6
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { connect } from '@yydb/yydb-client/node';
|
|
6
|
+
import { defineMicro as buildMicro, MicroSessionRegistry } from './micro.js';
|
|
7
|
+
import { resolveYydbCli } from './resolve-bin.js';
|
|
8
|
+
function microFingerprint(name, version) {
|
|
9
|
+
const bytes = new Uint8Array(32);
|
|
10
|
+
const seed = `${name}@${version}`;
|
|
11
|
+
for (let index = 0; index < bytes.byteLength; index += 1) {
|
|
12
|
+
bytes[index] = seed.charCodeAt(index % seed.length) & 0xff;
|
|
13
|
+
}
|
|
14
|
+
return bytes;
|
|
15
|
+
}
|
|
7
16
|
async function freeLoopbackPort() {
|
|
8
17
|
return await new Promise((resolve, reject) => {
|
|
9
18
|
const server = net.createServer();
|
|
10
|
-
server.listen(0,
|
|
19
|
+
server.listen(0, '127.0.0.1', () => {
|
|
11
20
|
const address = server.address();
|
|
12
|
-
if (!address || typeof address ===
|
|
21
|
+
if (!address || typeof address === 'string') {
|
|
13
22
|
server.close();
|
|
14
|
-
reject(new Error(
|
|
23
|
+
reject(new Error('could not allocate loopback port'));
|
|
15
24
|
return;
|
|
16
25
|
}
|
|
17
26
|
const { port } = address;
|
|
@@ -22,7 +31,7 @@ async function freeLoopbackPort() {
|
|
|
22
31
|
resolve(port);
|
|
23
32
|
});
|
|
24
33
|
});
|
|
25
|
-
server.on(
|
|
34
|
+
server.on('error', reject);
|
|
26
35
|
});
|
|
27
36
|
}
|
|
28
37
|
async function waitForTcp(host, port, timeoutMs) {
|
|
@@ -34,7 +43,7 @@ async function waitForTcp(host, port, timeoutMs) {
|
|
|
34
43
|
socket.end();
|
|
35
44
|
resolve();
|
|
36
45
|
});
|
|
37
|
-
socket.on(
|
|
46
|
+
socket.on('error', reject);
|
|
38
47
|
});
|
|
39
48
|
return;
|
|
40
49
|
}
|
|
@@ -53,11 +62,14 @@ export class Database {
|
|
|
53
62
|
endpoint;
|
|
54
63
|
client;
|
|
55
64
|
child;
|
|
65
|
+
microRegistry;
|
|
56
66
|
constructor(dbPath, endpoint, client, child) {
|
|
57
67
|
this.path = dbPath;
|
|
58
68
|
this.endpoint = endpoint;
|
|
59
69
|
this.client = client;
|
|
60
70
|
this.child = child;
|
|
71
|
+
this.microRegistry = new MicroSessionRegistry();
|
|
72
|
+
this.client.setMicroHostHandler((payload) => this.microRegistry.invokeFromWire(payload));
|
|
61
73
|
}
|
|
62
74
|
/**
|
|
63
75
|
* Open (create) a `.yydb` file. Spawns the engine on `127.0.0.1` automatically.
|
|
@@ -65,37 +77,37 @@ export class Database {
|
|
|
65
77
|
static async open(dbPath, options = {}) {
|
|
66
78
|
const resolved = path.resolve(dbPath);
|
|
67
79
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
68
|
-
const
|
|
80
|
+
const cli = options.cli ?? options.binary ?? resolveYydbCli();
|
|
69
81
|
const port = options.port ?? (await freeLoopbackPort());
|
|
70
82
|
const bind = `127.0.0.1:${port}`;
|
|
71
|
-
const args = [
|
|
72
|
-
const child = spawn(
|
|
73
|
-
stdio:
|
|
83
|
+
const args = [cli, 'serve', resolved, '--bind', bind, ...(options.serveArgs ?? [])];
|
|
84
|
+
const child = spawn(process.execPath, args, {
|
|
85
|
+
stdio: 'pipe',
|
|
74
86
|
windowsHide: true,
|
|
75
87
|
});
|
|
76
|
-
let stderr =
|
|
77
|
-
child.stderr?.setEncoding(
|
|
78
|
-
child.stderr?.on(
|
|
88
|
+
let stderr = '';
|
|
89
|
+
child.stderr?.setEncoding('utf8');
|
|
90
|
+
child.stderr?.on('data', (chunk) => {
|
|
79
91
|
stderr += chunk;
|
|
80
92
|
});
|
|
81
93
|
const exitPromise = new Promise((_, reject) => {
|
|
82
|
-
child.once(
|
|
94
|
+
child.once('exit', (code, signal) => {
|
|
83
95
|
reject(new Error(`yydb engine exited early (code=${code}, signal=${signal}): ${stderr.trim()}`));
|
|
84
96
|
});
|
|
85
|
-
child.once(
|
|
97
|
+
child.once('error', reject);
|
|
86
98
|
});
|
|
87
99
|
try {
|
|
88
|
-
await Promise.race([waitForTcp(
|
|
100
|
+
await Promise.race([waitForTcp('127.0.0.1', port, 8000), exitPromise]);
|
|
89
101
|
const client = await connect(bind);
|
|
90
|
-
child.removeAllListeners(
|
|
91
|
-
child.removeAllListeners(
|
|
92
|
-
child.on(
|
|
102
|
+
child.removeAllListeners('exit');
|
|
103
|
+
child.removeAllListeners('error');
|
|
104
|
+
child.on('exit', () => {
|
|
93
105
|
/* closed via Database.close or crash */
|
|
94
106
|
});
|
|
95
107
|
return new Database(resolved, bind, client, child);
|
|
96
108
|
}
|
|
97
109
|
catch (error) {
|
|
98
|
-
child.kill(
|
|
110
|
+
child.kill('SIGTERM');
|
|
99
111
|
throw error;
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -117,6 +129,36 @@ export class Database {
|
|
|
117
129
|
async put(key, value) {
|
|
118
130
|
await this.client.put(key, value);
|
|
119
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Build a typed session-local micro definition. Does not register it with
|
|
134
|
+
* the engine until [`registerMicro`](./database.ts) is called.
|
|
135
|
+
*/
|
|
136
|
+
defineMicro(definition) {
|
|
137
|
+
return buildMicro(definition);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Install a [`defineMicro`](./database.ts) definition into the current
|
|
141
|
+
* session registry and sync metadata to the private engine. Host micros are
|
|
142
|
+
* session-local and never written to `.yydb`.
|
|
143
|
+
*/
|
|
144
|
+
async registerMicro(definition) {
|
|
145
|
+
const handle = this.microRegistry.register(definition);
|
|
146
|
+
await this.client.registerMicro({
|
|
147
|
+
hostId: handle.hostId,
|
|
148
|
+
handleVersion: handle.implementationVersion,
|
|
149
|
+
udfVersion: definition.version,
|
|
150
|
+
name: definition.name,
|
|
151
|
+
functionId: handle.functionId,
|
|
152
|
+
args: definition.args.map((arg) => arg.kind),
|
|
153
|
+
returns: definition.returns.kind,
|
|
154
|
+
fingerprint: microFingerprint(definition.name, definition.version),
|
|
155
|
+
});
|
|
156
|
+
return handle;
|
|
157
|
+
}
|
|
158
|
+
/** Invoke a registered scalar UDF on the private engine. */
|
|
159
|
+
async callScalar(name, version, args) {
|
|
160
|
+
return this.client.callScalar(name, version, args);
|
|
161
|
+
}
|
|
120
162
|
/** Stop the private engine process. */
|
|
121
163
|
async close() {
|
|
122
164
|
this.client.close();
|
|
@@ -126,11 +168,11 @@ export class Database {
|
|
|
126
168
|
return;
|
|
127
169
|
}
|
|
128
170
|
await new Promise((resolve) => {
|
|
129
|
-
child.once(
|
|
130
|
-
child.kill(
|
|
171
|
+
child.once('exit', () => resolve());
|
|
172
|
+
child.kill('SIGTERM');
|
|
131
173
|
setTimeout(() => {
|
|
132
174
|
if (child.exitCode === null && !child.killed) {
|
|
133
|
-
child.kill(
|
|
175
|
+
child.kill('SIGKILL');
|
|
134
176
|
}
|
|
135
177
|
resolve();
|
|
136
178
|
}, 2000);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { scalarToTs } from './ir.js';
|
|
2
|
+
const BANNER = '// @generated by `yydb generate`, do not edit!';
|
|
3
|
+
/** Generate `schema.generated.ts` source from IR. */
|
|
4
|
+
export function emitTypeScript(ir) {
|
|
5
|
+
const lines = [BANNER];
|
|
6
|
+
lines.push(`export const APP_SCHEMA_VERSION = ${ir.schemaVersion};`);
|
|
7
|
+
lines.push(`export const APP_SCHEMA = ${JSON.stringify(ir.vosSource)};`);
|
|
8
|
+
for (const table of ir.tables) {
|
|
9
|
+
lines.push(emitInterfaceRow(table));
|
|
10
|
+
}
|
|
11
|
+
lines.push(emitInterfaceAppDb(ir));
|
|
12
|
+
lines.push(`export type AppDbTableName = keyof AppDb["tables"];`);
|
|
13
|
+
return `${lines.join('\n')}\n`;
|
|
14
|
+
}
|
|
15
|
+
function emitInterfaceRow(table) {
|
|
16
|
+
const lines = [`export interface ${table.name}Row {`];
|
|
17
|
+
for (const field of table.fields) {
|
|
18
|
+
lines.push(` ${field.name}: ${scalarToTs(field.ty)};`);
|
|
19
|
+
}
|
|
20
|
+
lines.push('}');
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
function emitInterfaceAppDb(ir) {
|
|
24
|
+
const tableLines = ir.tables.map((table) => ` ${table.name}: ${table.name}Row;`);
|
|
25
|
+
return ['export interface AppDb {', ' tables: {', ...tableLines, ' };', '}'].join('\n');
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const APP_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const APP_SCHEMA = "table Setting {\n @@id: uuid,\n key: utf8,\n value: utf8,\n}\n";
|
|
3
|
+
export interface SettingRow {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AppDb {
|
|
9
|
+
tables: {
|
|
10
|
+
Setting: SettingRow;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export type AppDbTableName = keyof AppDb['tables'];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { SchemaField, SchemaTable, ScalarType, TsSchemaIr } from './ir.js';
|
|
2
|
+
export { emitTypeScript } from './emit-typescript.js';
|
|
3
|
+
export { parseVosSubset, VosParseError } from './parse-vos.js';
|
|
4
|
+
/** Generate TypeScript from a VOS document. */
|
|
5
|
+
export declare function generateTypeScript(vosSource: string, schemaVersion: number): string;
|
|
6
|
+
export { generateTypeScript as generateTypescript };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { emitTypeScript } from './emit-typescript.js';
|
|
2
|
+
import { parseVosSubset } from './parse-vos.js';
|
|
3
|
+
export { emitTypeScript } from './emit-typescript.js';
|
|
4
|
+
export { parseVosSubset, VosParseError } from './parse-vos.js';
|
|
5
|
+
/** Generate TypeScript from a VOS document. */
|
|
6
|
+
export function generateTypeScript(vosSource, schemaVersion) {
|
|
7
|
+
const ir = parseVosSubset(vosSource, schemaVersion);
|
|
8
|
+
return emitTypeScript(ir);
|
|
9
|
+
}
|
|
10
|
+
export { generateTypeScript as generateTypescript };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Scalar types supported by the MVP emitter (expand with `vos-parser`). */
|
|
2
|
+
export type ScalarType = 'uuid' | 'utf8' | 'bool' | 'i64' | 'f64';
|
|
3
|
+
export interface SchemaField {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly ty: ScalarType;
|
|
6
|
+
/** True when this field is declared via `@@id`. */
|
|
7
|
+
readonly isId: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SchemaTable {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly fields: readonly SchemaField[];
|
|
12
|
+
}
|
|
13
|
+
/** One schema document ready for TypeScript emission. */
|
|
14
|
+
export interface TsSchemaIr {
|
|
15
|
+
/** Original VOS source (database truth embedded into generated module). */
|
|
16
|
+
readonly vosSource: string;
|
|
17
|
+
readonly schemaVersion: number;
|
|
18
|
+
readonly tables: readonly SchemaTable[];
|
|
19
|
+
}
|
|
20
|
+
export declare function scalarToTs(ty: ScalarType): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TsSchemaIr } from './ir.js';
|
|
2
|
+
export declare class VosParseError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Interim VOS subset parser for `yydb generate` (MVP). */
|
|
6
|
+
export declare function parseVosSubset(source: string, schemaVersion: number): TsSchemaIr;
|