toiljs 0.0.58 → 0.0.60
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/CHANGELOG.md +19 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +309 -116
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/catalog.d.ts +1 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +64 -0
- package/build/devserver/db/database.js +662 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/types.d.ts +58 -0
- package/build/devserver/db/types.js +20 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +9 -24
- package/build/devserver/index.js +4 -165
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +1 -1
- package/build/devserver/{host.js → runtime/host.js} +6 -6
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +1 -1
- package/build/devserver/{module.js → runtime/module.js} +8 -1
- package/build/devserver/server.d.ts +17 -0
- package/build/devserver/server.js +164 -0
- package/docs/time.md +2 -2
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/package.json +5 -2
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +38 -1
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/devserver/db/catalog.ts +100 -0
- package/src/devserver/db/database.ts +1169 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/types.ts +76 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +19 -287
- package/src/devserver/{host.ts → runtime/host.ts} +6 -6
- package/src/devserver/{module.ts → runtime/module.ts} +13 -1
- package/src/devserver/server.ts +292 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +114 -9
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/example-guestbook.test.ts +43 -1
- package/test/pqauth-e2e.test.ts +1 -1
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
package/src/cli/create.ts
CHANGED
|
@@ -40,6 +40,38 @@ import { isPackageManager, isValidName, resolveProjectDir } from './validate.js'
|
|
|
40
40
|
|
|
41
41
|
export type Template = 'app' | 'minimal';
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The `server/migrations/` README, scaffolded by `create` and ensured by `doctor`/`update`. ToilDB
|
|
45
|
+
* migrations are enforced by convention: a `@migrate` function MUST live in a `*.migration.ts` file
|
|
46
|
+
* under a `migrations/` folder (the toilscript compiler raises a compile error otherwise), and the
|
|
47
|
+
* build auto-discovers every such file. Keeping the convention documented next to the folder means a
|
|
48
|
+
* fresh project ships with a place to evolve `@data` value types from day one.
|
|
49
|
+
*/
|
|
50
|
+
export const MIGRATIONS_README =
|
|
51
|
+
'# migrations/\n\n' +
|
|
52
|
+
'ToilDB schema migrations. One file per evolving `@data` value type, named `<Type>.migration.ts`\n' +
|
|
53
|
+
'(e.g. `User.migration.ts`).\n\n' +
|
|
54
|
+
'Each file keeps the OLD `@data` shapes (e.g. `UserV1`) alongside the `@migrate` transform(s) that\n' +
|
|
55
|
+
'carry old records forward, and imports the CURRENT value type from the app. The build\n' +
|
|
56
|
+
'auto-discovers every `*.migration.ts` under the project.\n\n' +
|
|
57
|
+
'Do NOT put `@migrate` anywhere else: a `@migrate` outside a `*.migration.ts` file in a\n' +
|
|
58
|
+
'`migrations/` folder is a compile error.\n\n' +
|
|
59
|
+
'## Example\n\n' +
|
|
60
|
+
'`User.migration.ts`:\n\n' +
|
|
61
|
+
'```ts\n' +
|
|
62
|
+
"import { User } from '../models/User';\n\n" +
|
|
63
|
+
'// The previous on-disk shape of a User record.\n' +
|
|
64
|
+
'@data\n' +
|
|
65
|
+
'export class UserV1 {\n' +
|
|
66
|
+
" name: string = '';\n" +
|
|
67
|
+
'}\n\n' +
|
|
68
|
+
'// Carry a UserV1 forward into the current User. Runs once per record on read.\n' +
|
|
69
|
+
'@migrate\n' +
|
|
70
|
+
'export function up(old: UserV1, into: User): void {\n' +
|
|
71
|
+
' into.name = old.name;\n' +
|
|
72
|
+
'}\n' +
|
|
73
|
+
'```\n';
|
|
74
|
+
|
|
43
75
|
/** Human label for each preprocessor in the styling picker. */
|
|
44
76
|
const PREPROCESSOR_LABEL: Record<Preprocessor, string> = {
|
|
45
77
|
css: 'Plain CSS',
|
|
@@ -114,7 +146,7 @@ function scaffold(
|
|
|
114
146
|
'@types/react-dom': '^19.2.3',
|
|
115
147
|
eslint: '^10.2.0',
|
|
116
148
|
prettier: '^3.8.1',
|
|
117
|
-
toilscript: '^0.1.
|
|
149
|
+
toilscript: '^0.1.37',
|
|
118
150
|
typescript: '^6.0.3',
|
|
119
151
|
};
|
|
120
152
|
for (const dep of requiredPackages(features).sort()) {
|
|
@@ -259,6 +291,10 @@ function scaffold(
|
|
|
259
291
|
' npm run build',
|
|
260
292
|
'',
|
|
261
293
|
].join('\n'),
|
|
294
|
+
// ToilDB migrations live under server/migrations/ (folder + `*.migration.ts` is enforced by
|
|
295
|
+
// the compiler; the build auto-discovers them). Ship the folder, documented, with no live
|
|
296
|
+
// *.migration.ts: a @migrate referencing types a fresh project lacks would break the build.
|
|
297
|
+
'server/migrations/README.md': MIGRATIONS_README,
|
|
262
298
|
};
|
|
263
299
|
|
|
264
300
|
// The `app` template's client UI + server are copied from examples/basic at runtime; `minimal`
|
|
@@ -377,6 +413,7 @@ function minimalServer(): Record<string, string> {
|
|
|
377
413
|
'| `main.ts` | The entry point: wires the handler and imports the surface modules. |\n' +
|
|
378
414
|
'| `core/` | The request handler and shared app logic (state, helpers). |\n' +
|
|
379
415
|
'| `models/` | `@data` classes, the typed wire model shared by HTTP and RPC. One type per file. |\n' +
|
|
416
|
+
'| `migrations/` | ToilDB schema migrations: a `<Type>.migration.ts` per evolving `@data` value type, holding the old shapes + the `@migrate` transform. |\n' +
|
|
380
417
|
'| `routes/` | `@rest` controllers (HTTP). One controller per file, named after its class. |\n' +
|
|
381
418
|
'| `services/` | `@service` classes and free `@remote` functions (typed RPC). |\n' +
|
|
382
419
|
'| `scheduled/` | Reserved for scheduled tasks (not shipped yet). |\n\n' +
|
package/src/cli/db.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toiljs db <action>` — manage the dev server's on-disk ToilDB data.
|
|
3
|
+
*
|
|
4
|
+
* Under `toiljs dev`, the in-process ToilDB emulator persists every family
|
|
5
|
+
* (records / views / unique / events / membership / counter / capacity) plus each
|
|
6
|
+
* row's schema_version to `<root>/.toil/devdata.json` (so data + migrations survive
|
|
7
|
+
* restarts). These subcommands let you inspect, reset, snapshot, and restore that
|
|
8
|
+
* store from outside a running server — handy for fixtures, sharing repro data, or
|
|
9
|
+
* wiping a corrupt dev state. The snapshot is the exact JSON the dev DB writes, so
|
|
10
|
+
* an exported file imports cleanly (and a hand-crafted one seeds the dev DB).
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { accent, bold, danger, dim, success, warn } from './ui.js';
|
|
16
|
+
|
|
17
|
+
export interface DbOptions {
|
|
18
|
+
/** Project root (where `.toil/` lives); defaults to the current directory. */
|
|
19
|
+
root?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The top-level families a valid snapshot carries (mirrors DbSnapshot). */
|
|
23
|
+
const FAMILIES = ['store', 'views', 'members', 'counters', 'events', 'eventDedup', 'capacity'] as const;
|
|
24
|
+
|
|
25
|
+
function devdataPath(opts: DbOptions): string {
|
|
26
|
+
return path.join(path.resolve(opts.root ?? process.cwd()), '.toil', 'devdata.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function out(line: string): void {
|
|
30
|
+
process.stdout.write(line + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-family key counts for a snapshot file, or null if unreadable. */
|
|
34
|
+
function familyCounts(file: string): Record<string, number> | null {
|
|
35
|
+
let snap: Record<string, unknown>;
|
|
36
|
+
try {
|
|
37
|
+
snap = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const counts: Record<string, number> = {};
|
|
42
|
+
for (const f of FAMILIES) {
|
|
43
|
+
const fam = snap[f];
|
|
44
|
+
counts[f] = fam && typeof fam === 'object' ? Object.keys(fam).length : 0;
|
|
45
|
+
}
|
|
46
|
+
return counts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isSnapshot(v: unknown): boolean {
|
|
50
|
+
return typeof v === 'object' && v !== null && FAMILIES.every((f) => f in (v as object));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Run `toiljs db <action> [fileArg]`. */
|
|
54
|
+
export function runDb(action: string | undefined, fileArg: string | undefined, opts: DbOptions): void {
|
|
55
|
+
const file = devdataPath(opts);
|
|
56
|
+
const rel = path.relative(process.cwd(), file) || file;
|
|
57
|
+
|
|
58
|
+
switch (action) {
|
|
59
|
+
case 'reset':
|
|
60
|
+
case 'purge': {
|
|
61
|
+
if (!fs.existsSync(file)) {
|
|
62
|
+
out(dim(` dev database already empty (${rel} not found)`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fs.rmSync(file);
|
|
66
|
+
out(success(' ✓ ') + 'dev database reset ' + dim(`(${rel} deleted)`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'export': {
|
|
71
|
+
if (!fs.existsSync(file)) {
|
|
72
|
+
out(warn(' ! ') + `nothing to export (${rel} not found)`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let pretty: string;
|
|
77
|
+
try {
|
|
78
|
+
pretty = JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2) + '\n';
|
|
79
|
+
} catch {
|
|
80
|
+
out(danger(' ✗ ') + `the dev database at ${rel} is unreadable`);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (fileArg === undefined) {
|
|
85
|
+
process.stdout.write(pretty); // no banner/prefix -> pipe-friendly
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(path.resolve(fileArg), pretty);
|
|
89
|
+
out(success(' ✓ ') + 'exported dev database to ' + dim(fileArg));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'import': {
|
|
94
|
+
if (fileArg === undefined) {
|
|
95
|
+
out(danger(' ✗ ') + 'usage: ' + dim('toiljs db import <file>'));
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let parsed: unknown;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(fs.readFileSync(path.resolve(fileArg), 'utf8'));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
out(danger(' ✗ ') + `cannot read/parse ${fileArg}: ${(e as Error).message}`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!isSnapshot(parsed)) {
|
|
108
|
+
out(danger(' ✗ ') + `${fileArg} is not a toiljs dev database snapshot`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
113
|
+
fs.writeFileSync(file, JSON.stringify(parsed)); // compact: the dev DB's on-disk form
|
|
114
|
+
out(success(' ✓ ') + `imported dev database from ${dim(fileArg)} ` + dim(`(${rel})`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'path':
|
|
119
|
+
out(file); // bare path, scriptable
|
|
120
|
+
return;
|
|
121
|
+
|
|
122
|
+
case 'status':
|
|
123
|
+
case 'info': {
|
|
124
|
+
out(bold(' dev database') + dim(` ${rel}`));
|
|
125
|
+
if (!fs.existsSync(file)) {
|
|
126
|
+
out(dim(' (empty — no data written yet; run the dev server to populate it)'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const counts = familyCounts(file);
|
|
130
|
+
if (counts === null) {
|
|
131
|
+
out(danger(' ✗ unreadable snapshot'));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
out(dim(` ${(fs.statSync(file).size / 1024).toFixed(1)} KiB`));
|
|
136
|
+
const nonEmpty = FAMILIES.filter((f) => counts[f] > 0);
|
|
137
|
+
if (nonEmpty.length === 0) out(dim(' (no rows)'));
|
|
138
|
+
else for (const f of nonEmpty) out(' ' + accent(f.padEnd(12)) + String(counts[f]));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
out(
|
|
144
|
+
[
|
|
145
|
+
bold('Usage') + ' ' + dim('toiljs db') + ' <action>',
|
|
146
|
+
'',
|
|
147
|
+
bold('Actions'),
|
|
148
|
+
' ' + accent('status'.padEnd(16)) + dim('show the dev DB path + per-family row counts'),
|
|
149
|
+
' ' + accent('reset'.padEnd(16)) + dim('delete all dev data (alias: purge)'),
|
|
150
|
+
' ' + accent('export [file]'.padEnd(16)) + dim('write a snapshot to <file> (or stdout)'),
|
|
151
|
+
' ' + accent('import <file>'.padEnd(16)) + dim('replace the dev DB with a snapshot'),
|
|
152
|
+
' ' + accent('path'.padEnd(16)) + dim('print the devdata.json path'),
|
|
153
|
+
].join('\n'),
|
|
154
|
+
);
|
|
155
|
+
if (action !== undefined) process.exitCode = 1; // unknown action
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/cli/diagnostics.ts
CHANGED
|
@@ -521,6 +521,25 @@ export function checkServerTsPlugin(present: boolean): Check {
|
|
|
521
521
|
};
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Whether the server has a `migrations/` folder. ToilDB `@migrate` functions MUST live in a
|
|
526
|
+
* `*.migration.ts` file under `migrations/` (the toilscript compiler enforces folder + extension as
|
|
527
|
+
* a compile error), and the build auto-discovers them, so a server project should keep the folder
|
|
528
|
+
* ready. Older projects predate the convention, so this WARNS (does not fail) and points at the
|
|
529
|
+
* one-command fix (`toiljs update` creates it).
|
|
530
|
+
*/
|
|
531
|
+
export function checkMigrationsDir(exists: boolean): Check {
|
|
532
|
+
return exists
|
|
533
|
+
? { id: 'migrations-dir', label: 'server/migrations/ directory', status: 'pass' }
|
|
534
|
+
: {
|
|
535
|
+
id: 'migrations-dir',
|
|
536
|
+
label: 'server/migrations/ directory',
|
|
537
|
+
status: 'warn',
|
|
538
|
+
detail: 'no server/migrations/ folder; ToilDB @migrate functions must live in a *.migration.ts file under it',
|
|
539
|
+
fix: 'Create server/migrations/ (one <Type>.migration.ts per evolving @data value type), or run `toiljs update` to add it.',
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
524
543
|
// --- Security -------------------------------------------------------------------------------------
|
|
525
544
|
|
|
526
545
|
/** Whether the project uses the auth primitive, and whether its session secret is configured. */
|
package/src/cli/doctor.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
checkDir,
|
|
28
28
|
checkDuplicatePatterns,
|
|
29
29
|
type CheckGroup,
|
|
30
|
+
checkMigrationsDir,
|
|
30
31
|
checkMountSlots,
|
|
31
32
|
checkNode,
|
|
32
33
|
checkPackageManager,
|
|
@@ -279,6 +280,24 @@ function serverTsconfigPath(root: string, toilconfig: Record<string, unknown> |
|
|
|
279
280
|
return null;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Whether a `migrations/` folder exists under any server dir (the dir of a toilconfig entry,
|
|
285
|
+
* conventionally `server/`). ToilDB `@migrate` functions live in `*.migration.ts` files there, and
|
|
286
|
+
* the build auto-discovers them; a missing folder is a warn, not a fail (older projects predate it).
|
|
287
|
+
*/
|
|
288
|
+
function serverMigrationsExist(root: string, toilconfig: Record<string, unknown> | null): boolean {
|
|
289
|
+
const dirs = new Set<string>();
|
|
290
|
+
const entries = Array.isArray(toilconfig?.entries)
|
|
291
|
+
? (toilconfig.entries as unknown[]).filter((e): e is string => typeof e === 'string')
|
|
292
|
+
: [];
|
|
293
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
294
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
295
|
+
for (const dir of dirs) {
|
|
296
|
+
if (fs.existsSync(path.join(dir, 'migrations'))) return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
282
301
|
/** Whether a parsed tsconfig's `compilerOptions.plugins` references the toilscript LS plugin. */
|
|
283
302
|
function tsconfigHasToilPlugin(tsconfig: Record<string, unknown> | null): boolean {
|
|
284
303
|
const plugins = asRecord(tsconfig?.compilerOptions)?.plugins;
|
|
@@ -840,6 +859,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<void> {
|
|
|
840
859
|
checkRestDispatch(restFacts),
|
|
841
860
|
checkPrettierPlugin(prettierPresent),
|
|
842
861
|
checkServerTsPlugin(serverTsPluginPresent),
|
|
862
|
+
checkMigrationsDir(serverMigrationsExist(root, toilconfig)),
|
|
843
863
|
]
|
|
844
864
|
: [checkToilconfig(false)],
|
|
845
865
|
},
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { build, dev, start } from 'toiljs/compiler';
|
|
|
8
8
|
|
|
9
9
|
import { runConfigure } from './configure.js';
|
|
10
10
|
import { runCreate, type Template } from './create.js';
|
|
11
|
+
import { runDb } from './db.js';
|
|
11
12
|
import { runDoctor } from './doctor.js';
|
|
12
13
|
import { notifyIfOutdated } from './notify.js';
|
|
13
14
|
import { runUpdate } from './update.js';
|
|
@@ -132,6 +133,7 @@ function printHelp(): void {
|
|
|
132
133
|
cmd('start', 'self-host the built app (hyper-express / uWS)'),
|
|
133
134
|
cmd('doctor', 'diagnose project setup and dependencies'),
|
|
134
135
|
cmd('update', 'check for and apply dependency updates'),
|
|
136
|
+
cmd('db <action>', 'manage dev DB data (status | reset | export | import)'),
|
|
135
137
|
'',
|
|
136
138
|
bold('Options'),
|
|
137
139
|
cmd('--root <dir>', 'project root (default: current directory)'),
|
|
@@ -255,6 +257,14 @@ async function main(): Promise<void> {
|
|
|
255
257
|
});
|
|
256
258
|
break;
|
|
257
259
|
|
|
260
|
+
case 'db': {
|
|
261
|
+
// `toiljs db <action> [file]` — no banner so `path`/`export` pipe cleanly.
|
|
262
|
+
const action = rest[0];
|
|
263
|
+
const fileArg = rest[1] !== undefined && !rest[1].startsWith('-') ? rest[1] : undefined;
|
|
264
|
+
runDb(action, fileArg, { root: flags.root });
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
258
268
|
case 'help':
|
|
259
269
|
case '--help':
|
|
260
270
|
case '-h':
|
package/src/cli/update.ts
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
import { cancel, intro, isCancel, multiselect, note, outro, spinner } from '@clack/prompts';
|
|
11
11
|
|
|
12
|
+
import { MIGRATIONS_README } from './create.js';
|
|
12
13
|
import { capture, run } from './proc.js';
|
|
13
14
|
import { buildRows, type Bump, parseNcuJson, type UpdateRow } from './updates.js';
|
|
14
15
|
import { accent, danger, dim, success, warn } from './ui.js';
|
|
@@ -49,6 +50,53 @@ function readDependencies(pkgPath: string): Record<string, string> {
|
|
|
49
50
|
return { ...merge(pkg.dependencies), ...merge(pkg.devDependencies) };
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The server dir(s) of a project: the directories of the toilconfig.json `entries`, conventionally
|
|
55
|
+
* `server/`. Falls back to `<root>/server` when there is no toilconfig (or it has no string entries),
|
|
56
|
+
* matching how `doctor` locates the server tree.
|
|
57
|
+
*/
|
|
58
|
+
function serverDirs(root: string): string[] {
|
|
59
|
+
const dirs = new Set<string>();
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(
|
|
62
|
+
fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8'),
|
|
63
|
+
);
|
|
64
|
+
const entries =
|
|
65
|
+
typeof parsed === 'object' &&
|
|
66
|
+
parsed !== null &&
|
|
67
|
+
Array.isArray((parsed as { entries?: unknown }).entries)
|
|
68
|
+
? (parsed as { entries: unknown[] }).entries.filter(
|
|
69
|
+
(e): e is string => typeof e === 'string',
|
|
70
|
+
)
|
|
71
|
+
: [];
|
|
72
|
+
for (const e of entries) dirs.add(path.dirname(path.resolve(root, e)));
|
|
73
|
+
} catch {
|
|
74
|
+
// no/unreadable toilconfig.json: fall back to the conventional server/ below
|
|
75
|
+
}
|
|
76
|
+
if (dirs.size === 0) dirs.add(path.join(root, 'server'));
|
|
77
|
+
return [...dirs];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensures the ToilDB `migrations/` folder exists under each server dir that exists. `@migrate`
|
|
82
|
+
* functions MUST live in a `*.migration.ts` file under `migrations/` (folder + extension is a
|
|
83
|
+
* compile error otherwise) and the build auto-discovers them, so an up-to-date project keeps the
|
|
84
|
+
* folder ready. Idempotent: only writes the folder + README where the server dir exists and the
|
|
85
|
+
* folder is absent. Returns the project-relative paths created (for the note), empty if nothing.
|
|
86
|
+
*/
|
|
87
|
+
function ensureMigrationsDirs(root: string): string[] {
|
|
88
|
+
const created: string[] = [];
|
|
89
|
+
for (const dir of serverDirs(root)) {
|
|
90
|
+
if (!fs.existsSync(dir)) continue;
|
|
91
|
+
const migrations = path.join(dir, 'migrations');
|
|
92
|
+
if (fs.existsSync(migrations)) continue;
|
|
93
|
+
fs.mkdirSync(migrations, { recursive: true });
|
|
94
|
+
fs.writeFileSync(path.join(migrations, 'README.md'), MIGRATIONS_README);
|
|
95
|
+
created.push(path.relative(root, migrations) || 'migrations');
|
|
96
|
+
}
|
|
97
|
+
return created;
|
|
98
|
+
}
|
|
99
|
+
|
|
52
100
|
function bumpColor(bump: Bump, text: string): string {
|
|
53
101
|
if (bump === 'major') return danger(text);
|
|
54
102
|
if (bump === 'minor') return warn(text);
|
|
@@ -85,6 +133,16 @@ export async function runUpdate(opts: UpdateOptions): Promise<void> {
|
|
|
85
133
|
|
|
86
134
|
intro(accent('toiljs update'));
|
|
87
135
|
|
|
136
|
+
// Bring the project's structure up to date: ensure the ToilDB migrations folder exists (it is
|
|
137
|
+
// newer than some projects, and the compiler requires @migrate functions to live under it).
|
|
138
|
+
const createdMigrations = ensureMigrationsDirs(root);
|
|
139
|
+
if (createdMigrations.length > 0) {
|
|
140
|
+
note(
|
|
141
|
+
createdMigrations.map((p) => `${dim('+')} ${p}/`).join('\n'),
|
|
142
|
+
'Created ToilDB migrations folder',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
88
146
|
const s = spinner();
|
|
89
147
|
s.start('Checking the registry for updates');
|
|
90
148
|
const res = await capture('npx', ncuArgs(['--jsonUpgraded']), root);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a compiled server wasm's `toildb.catalog` custom section into a map of
|
|
3
|
+
* `"<Db>/<collection>"` (the resolve key the guest passes to
|
|
4
|
+
* `data.resolve_collection`) -> the collection's CURRENT `schema_version`.
|
|
5
|
+
*
|
|
6
|
+
* The dev DB uses this to STAMP each write with the value type's current schema
|
|
7
|
+
* version. When the developer evolves a `@data` type and rebuilds, the catalog
|
|
8
|
+
* version changes; data already on disk keeps its OLD stamp, so a read surfaces
|
|
9
|
+
* that old version and the guest's woven `decodeInto` runs the `@migrate` - the
|
|
10
|
+
* dev-side equivalent of the edge binding the cap's schema_version into the row.
|
|
11
|
+
*
|
|
12
|
+
* Wire format mirrors `toildb::catalog` / the backend `db_catalog` decoder and the
|
|
13
|
+
* compiler's `buildToilDbCatalog` emitter (all little-endian).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Read a LEB128 from `buf` at `pos`; throws on overrun (the section is a
|
|
17
|
+
* tenant-built, possibly mid-rebuild wasm, so it must never over-read). */
|
|
18
|
+
import { DataReader } from 'toiljs/io';
|
|
19
|
+
|
|
20
|
+
function leb(buf: Buffer, pos: number): [number, number] {
|
|
21
|
+
let result = 0;
|
|
22
|
+
let shift = 0;
|
|
23
|
+
let p = pos;
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (p >= buf.length) throw new RangeError('leb128 past end of buffer');
|
|
26
|
+
const b = buf[p++];
|
|
27
|
+
result |= (b & 0x7f) << shift;
|
|
28
|
+
if ((b & 0x80) === 0) break;
|
|
29
|
+
shift += 7;
|
|
30
|
+
if (shift > 35) throw new RangeError('leb128 too long');
|
|
31
|
+
}
|
|
32
|
+
return [result >>> 0, p];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The bytes of the named wasm custom section, or null if absent. Bounds-checked
|
|
36
|
+
* so a truncated/garbage module can never read past the buffer. */
|
|
37
|
+
function customSection(wasm: Buffer, want: string): Buffer | null {
|
|
38
|
+
let pos = 8; // skip the 8-byte magic + version header
|
|
39
|
+
while (pos < wasm.length) {
|
|
40
|
+
const id = wasm[pos++];
|
|
41
|
+
let size: number;
|
|
42
|
+
[size, pos] = leb(wasm, pos);
|
|
43
|
+
const end = pos + size;
|
|
44
|
+
if (end > wasm.length || end < pos) return null; // truncated section table
|
|
45
|
+
if (id === 0) {
|
|
46
|
+
let nameLen: number;
|
|
47
|
+
let namePos: number;
|
|
48
|
+
[nameLen, namePos] = leb(wasm, pos);
|
|
49
|
+
if (namePos + nameLen <= end && wasm.toString('latin1', namePos, namePos + nameLen) === want)
|
|
50
|
+
return wasm.subarray(namePos + nameLen, end);
|
|
51
|
+
}
|
|
52
|
+
pos = end;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `"<Db>/<collection>"` -> current `schema_version` for every collection. Decoded
|
|
58
|
+
* with the shared bounds-checked {@link DataReader} (it returns 0/empty + flips
|
|
59
|
+
* `.ok` past the end), so a truncated/garbage section - e.g. a mid-rebuild wasm -
|
|
60
|
+
* yields only the collections that decoded cleanly and never over-reads. Mirrors
|
|
61
|
+
* the compiler's `buildToilDbCatalog` emitter + the backend `db_catalog` decoder. */
|
|
62
|
+
export function parseCatalog(wasm: Buffer): Map<string, number> {
|
|
63
|
+
const out = new Map<string, number>();
|
|
64
|
+
let sec: Buffer | null;
|
|
65
|
+
try {
|
|
66
|
+
sec = customSection(wasm, 'toildb.catalog');
|
|
67
|
+
} catch {
|
|
68
|
+
return out; // garbage section table (mid-rebuild) -> no catalog
|
|
69
|
+
}
|
|
70
|
+
if (sec === null) return out;
|
|
71
|
+
|
|
72
|
+
const r = new DataReader(sec);
|
|
73
|
+
r.readU16(); // catalog format version
|
|
74
|
+
const ndb = r.readU16();
|
|
75
|
+
for (let d = 0; d < ndb && r.ok; d++) {
|
|
76
|
+
const db = r.readString();
|
|
77
|
+
const nc = r.readU16();
|
|
78
|
+
for (let c = 0; c < nc && r.ok; c++) {
|
|
79
|
+
const name = r.readString();
|
|
80
|
+
r.readU8(); // family
|
|
81
|
+
r.readString(); // keyType
|
|
82
|
+
r.readString(); // valueType
|
|
83
|
+
r.readU32(); // valueDataId
|
|
84
|
+
const schemaVersion = r.readU32();
|
|
85
|
+
r.readU32(); // generation
|
|
86
|
+
r.readU8(); // replication (emitter order: replication then placement)
|
|
87
|
+
r.readU8(); // placement
|
|
88
|
+
const nFields = r.readU16();
|
|
89
|
+
for (let f = 0; f < nFields; f++) {
|
|
90
|
+
r.readString(); // field name
|
|
91
|
+
r.readString(); // field type
|
|
92
|
+
r.readU8(); // isArray
|
|
93
|
+
}
|
|
94
|
+
const nMig = r.readU16();
|
|
95
|
+
for (let m = 0; m < nMig; m++) r.readU32(); // migratableFrom versions
|
|
96
|
+
if (r.ok) out.set(db + '/' + name, schemaVersion >>> 0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|