turbine-orm 0.36.1 → 0.37.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/README.md +3 -1
- package/dist/cjs/cli/index.js +53 -25
- package/dist/cjs/cli/studio-demo.js +310 -0
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +243 -70
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.js +53 -25
- package/dist/cli/studio-demo.d.ts +43 -0
- package/dist/cli/studio-demo.js +306 -0
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +31 -3
- package/dist/cli/studio.js +242 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -614,7 +614,7 @@ Commands:
|
|
|
614
614
|
seed Run seed file
|
|
615
615
|
status Show database schema summary
|
|
616
616
|
doctor Check relations for missing FK indexes (--fix emits migration)
|
|
617
|
-
studio Launch local Studio web UI (read-only; --write
|
|
617
|
+
studio Launch local Studio web UI (read-only; --write for writes, --demo for a sample DB)
|
|
618
618
|
mcp Start read-only MCP server over JSON-RPC stdio
|
|
619
619
|
observe Launch local metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
620
620
|
|
|
@@ -688,6 +688,8 @@ DATABASE_URL=postgres://user:pass@localhost:5432/mydb npx turbine studio
|
|
|
688
688
|
npx turbine studio --port 5173 --host 127.0.0.1 --no-open
|
|
689
689
|
```
|
|
690
690
|
|
|
691
|
+
**Try it without a database.** `npx turbine-orm@latest studio --demo` boots Studio against a seeded, in-memory sample database (users, posts, comments, orgs) with no `DATABASE_URL` and no extra dependency, backed by Turbine's own SQLite engine over `node:sqlite` (Node 22.5+). An in-UI switcher flips the three modes live (Read-only / Show PII / Write), so you can feel PII redaction and the write flow back to back. Writes genuinely apply to the in-memory store but nothing is ever saved: every launch starts fresh.
|
|
692
|
+
|
|
691
693
|
**Features**
|
|
692
694
|
|
|
693
695
|
- **Query / Data / Schema tabs.** Compose queries visually, browse rows, and inspect tables and relations.
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* turbine seed — Run seed file
|
|
16
16
|
* turbine status — Show schema summary
|
|
17
17
|
* turbine doctor — Check relations for missing FK indexes (--fix emits migration)
|
|
18
|
-
* turbine studio
|
|
18
|
+
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
19
19
|
* turbine mcp — Start read-only MCP server over JSON-RPC stdio
|
|
20
20
|
* turbine observe — Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
21
21
|
*
|
|
@@ -190,6 +190,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
190
190
|
case '--show-pii':
|
|
191
191
|
result.showPii = true;
|
|
192
192
|
break;
|
|
193
|
+
case '--demo':
|
|
194
|
+
result.demo = true;
|
|
195
|
+
break;
|
|
193
196
|
default:
|
|
194
197
|
if (!arg.startsWith('-')) {
|
|
195
198
|
result.positional.push(arg);
|
|
@@ -1503,7 +1506,10 @@ function isLoopbackHost(host) {
|
|
|
1503
1506
|
// ---------------------------------------------------------------------------
|
|
1504
1507
|
async function cmdStudio(args, config) {
|
|
1505
1508
|
(0, ui_js_1.banner)();
|
|
1506
|
-
const
|
|
1509
|
+
const demo = args.demo === true;
|
|
1510
|
+
// Demo mode is self-contained (seeded in-memory database), so it never needs
|
|
1511
|
+
// a DATABASE_URL. The placeholder is only used for display.
|
|
1512
|
+
const url = demo ? 'demo://in-memory' : requireUrl(config);
|
|
1507
1513
|
const port = args.port ?? 4983;
|
|
1508
1514
|
const host = args.host ?? '127.0.0.1';
|
|
1509
1515
|
const openBrowser = !args.noOpen;
|
|
@@ -1526,7 +1532,7 @@ async function cmdStudio(args, config) {
|
|
|
1526
1532
|
console.log((0, ui_js_1.warn)(`Studio is binding to ${(0, ui_js_1.yellow)(host)} — this is NOT loopback. ` +
|
|
1527
1533
|
`Anyone on your network who can reach this port + guess the session token can read your database.`));
|
|
1528
1534
|
}
|
|
1529
|
-
const spinner = new ui_js_1.Spinner('Introspecting database').start();
|
|
1535
|
+
const spinner = new ui_js_1.Spinner(demo ? 'Seeding demo dataset' : 'Introspecting database').start();
|
|
1530
1536
|
let studio;
|
|
1531
1537
|
try {
|
|
1532
1538
|
studio = await (0, studio_js_1.startStudio)({
|
|
@@ -1537,39 +1543,60 @@ async function cmdStudio(args, config) {
|
|
|
1537
1543
|
openBrowser,
|
|
1538
1544
|
include: config.include.length ? config.include : undefined,
|
|
1539
1545
|
exclude: config.exclude.length ? config.exclude : undefined,
|
|
1546
|
+
// Demo boots read-only + PII redacted; the flags are ignored in demo mode
|
|
1547
|
+
// (the in-UI switcher controls modes live).
|
|
1540
1548
|
write: args.write === true,
|
|
1541
1549
|
showPii: args.showPii === true,
|
|
1550
|
+
demo,
|
|
1542
1551
|
});
|
|
1543
|
-
spinner.succeed(
|
|
1552
|
+
spinner.succeed(demo ? 'Demo Studio is running' : 'Studio is running');
|
|
1544
1553
|
}
|
|
1545
1554
|
catch (err) {
|
|
1546
1555
|
spinner.fail(`Failed to start Studio: ${err instanceof Error ? err.message : String(err)}`);
|
|
1547
1556
|
process.exit(1);
|
|
1548
1557
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1558
|
+
if (demo) {
|
|
1559
|
+
(0, ui_js_1.newline)();
|
|
1560
|
+
console.log((0, ui_js_1.box)([
|
|
1561
|
+
`${(0, ui_js_1.bold)('Turbine Studio')} ${(0, ui_js_1.dim)('DEMO MODE (seeded in-memory sample database)')}`,
|
|
1562
|
+
'',
|
|
1563
|
+
` ${(0, ui_js_1.cyan)('URL:')} ${(0, ui_js_1.bold)(studio.url)}`,
|
|
1564
|
+
` ${(0, ui_js_1.cyan)('Data:')} seeded sample dataset (users, posts, comments, orgs)`,
|
|
1565
|
+
` ${(0, ui_js_1.cyan)('Modes:')} switch Read-only / Show PII / Write live from inside the UI`,
|
|
1566
|
+
'',
|
|
1567
|
+
(0, ui_js_1.dim)('Nothing you do here is saved anywhere. The database lives only in'),
|
|
1568
|
+
(0, ui_js_1.dim)('memory: every launch starts fresh and restarts reset all edits.'),
|
|
1569
|
+
(0, ui_js_1.dim)('Open the URL above (it carries a one-time session token).'),
|
|
1570
|
+
(0, ui_js_1.dim)('Press Ctrl+C to stop.'),
|
|
1571
|
+
].join('\n'), { title: (0, ui_js_1.bold)((0, ui_js_1.cyan)('Studio · demo')), padding: 1 }));
|
|
1551
1572
|
(0, ui_js_1.newline)();
|
|
1552
|
-
console.log((0, ui_js_1.warn)('WRITE MODE is ON. Studio can update, insert, and delete single rows in ' +
|
|
1553
|
-
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`));
|
|
1554
1573
|
}
|
|
1555
|
-
|
|
1574
|
+
else {
|
|
1575
|
+
// Loud startup warnings for the opt-in modes that widen Studio's surface.
|
|
1576
|
+
if (args.write) {
|
|
1577
|
+
(0, ui_js_1.newline)();
|
|
1578
|
+
console.log((0, ui_js_1.warn)('WRITE MODE is ON. Studio can update, insert, and delete single rows in ' +
|
|
1579
|
+
`${(0, ui_js_1.redactUrl)(url)}. Every change is committed directly to your database.`));
|
|
1580
|
+
}
|
|
1581
|
+
if (args.showPii) {
|
|
1582
|
+
(0, ui_js_1.newline)();
|
|
1583
|
+
console.log((0, ui_js_1.warn)('--show-pii is ON. PII-tagged column values are shown UNREDACTED in Studio.'));
|
|
1584
|
+
}
|
|
1585
|
+
(0, ui_js_1.newline)();
|
|
1586
|
+
console.log((0, ui_js_1.box)([
|
|
1587
|
+
`${(0, ui_js_1.bold)('Turbine Studio')} ${(0, ui_js_1.dim)(args.write ? 'local UI (WRITE MODE)' : 'local read-only UI')}`,
|
|
1588
|
+
'',
|
|
1589
|
+
` ${(0, ui_js_1.cyan)('URL:')} ${(0, ui_js_1.bold)(studio.url)}`,
|
|
1590
|
+
` ${(0, ui_js_1.cyan)('Schema:')} ${config.schema}`,
|
|
1591
|
+
` ${(0, ui_js_1.cyan)('DB:')} ${(0, ui_js_1.redactUrl)(url)}`,
|
|
1592
|
+
` ${(0, ui_js_1.cyan)('Mode:')} ${args.write ? (0, ui_js_1.red)('read-write (single-row)') : 'read-only'}`,
|
|
1593
|
+
'',
|
|
1594
|
+
(0, ui_js_1.dim)('Open the URL above in your browser. It includes a one-time session'),
|
|
1595
|
+
(0, ui_js_1.dim)('token that gets set as an HttpOnly cookie on first load.'),
|
|
1596
|
+
(0, ui_js_1.dim)('Press Ctrl+C to stop.'),
|
|
1597
|
+
].join('\n'), { title: (0, ui_js_1.bold)((0, ui_js_1.cyan)('Studio')), padding: 1 }));
|
|
1556
1598
|
(0, ui_js_1.newline)();
|
|
1557
|
-
console.log((0, ui_js_1.warn)('--show-pii is ON. PII-tagged column values are shown UNREDACTED in Studio.'));
|
|
1558
1599
|
}
|
|
1559
|
-
(0, ui_js_1.newline)();
|
|
1560
|
-
console.log((0, ui_js_1.box)([
|
|
1561
|
-
`${(0, ui_js_1.bold)('Turbine Studio')} ${(0, ui_js_1.dim)(args.write ? 'local UI (WRITE MODE)' : 'local read-only UI')}`,
|
|
1562
|
-
'',
|
|
1563
|
-
` ${(0, ui_js_1.cyan)('URL:')} ${(0, ui_js_1.bold)(studio.url)}`,
|
|
1564
|
-
` ${(0, ui_js_1.cyan)('Schema:')} ${config.schema}`,
|
|
1565
|
-
` ${(0, ui_js_1.cyan)('DB:')} ${(0, ui_js_1.redactUrl)(url)}`,
|
|
1566
|
-
` ${(0, ui_js_1.cyan)('Mode:')} ${args.write ? (0, ui_js_1.red)('read-write (single-row)') : 'read-only'}`,
|
|
1567
|
-
'',
|
|
1568
|
-
(0, ui_js_1.dim)('Open the URL above in your browser. It includes a one-time session'),
|
|
1569
|
-
(0, ui_js_1.dim)('token that gets set as an HttpOnly cookie on first load.'),
|
|
1570
|
-
(0, ui_js_1.dim)('Press Ctrl+C to stop.'),
|
|
1571
|
-
].join('\n'), { title: (0, ui_js_1.bold)((0, ui_js_1.cyan)('Studio')), padding: 1 }));
|
|
1572
|
-
(0, ui_js_1.newline)();
|
|
1573
1600
|
// Wait forever until SIGINT/SIGTERM, then dispose cleanly.
|
|
1574
1601
|
await new Promise((resolve) => {
|
|
1575
1602
|
const shutdown = async () => {
|
|
@@ -1850,7 +1877,7 @@ function showHelp() {
|
|
|
1850
1877
|
console.log(` ${(0, ui_js_1.cyan)('seed')} Run seed file`);
|
|
1851
1878
|
console.log(` ${(0, ui_js_1.cyan)('status')} ${(0, ui_js_1.dim)('| info')} Show schema summary`);
|
|
1852
1879
|
console.log(` ${(0, ui_js_1.cyan)('doctor')} Check relations for missing FK indexes ${(0, ui_js_1.dim)('(--fix emits migration)')}`);
|
|
1853
|
-
console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI ${(0, ui_js_1.dim)('(--write
|
|
1880
|
+
console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI ${(0, ui_js_1.dim)('(--write for writes, --demo for a sample DB)')}`);
|
|
1854
1881
|
console.log(` ${(0, ui_js_1.cyan)('mcp')} Start read-only MCP server over stdio`);
|
|
1855
1882
|
console.log(` ${(0, ui_js_1.cyan)('observe')} Launch metrics dashboard ${(0, ui_js_1.dim)('(requires TURBINE_OBSERVE_URL)')}`);
|
|
1856
1883
|
(0, ui_js_1.newline)();
|
|
@@ -1876,6 +1903,7 @@ function showHelp() {
|
|
|
1876
1903
|
console.log(` ${(0, ui_js_1.cyan)('--allow-remote')} Allow non-loopback --host ${(0, ui_js_1.dim)('(refused without this flag)')}`);
|
|
1877
1904
|
console.log(` ${(0, ui_js_1.cyan)('--write')} Studio: enable single-row update/insert/delete ${(0, ui_js_1.dim)('(read-only by default)')}`);
|
|
1878
1905
|
console.log(` ${(0, ui_js_1.cyan)('--show-pii')} Studio: show PII-tagged values unredacted ${(0, ui_js_1.dim)('(redacted by default)')}`);
|
|
1906
|
+
console.log(` ${(0, ui_js_1.cyan)('--demo')} Studio: launch with a seeded in-memory sample database ${(0, ui_js_1.dim)('(no DATABASE_URL needed; nothing is saved)')}`);
|
|
1879
1907
|
(0, ui_js_1.newline)();
|
|
1880
1908
|
console.log(` ${(0, ui_js_1.bold)('Config file:')}`);
|
|
1881
1909
|
console.log(` ${(0, ui_js_1.dim)('Create')} ${(0, ui_js_1.cyan)('turbine.config.ts')} ${(0, ui_js_1.dim)('with')} ${(0, ui_js_1.cyan)('npx turbine init')}`);
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm CLI: Studio demo mode (`turbine studio --demo`)
|
|
4
|
+
*
|
|
5
|
+
* Boots Studio with NO database and NO DATABASE_URL: a baked-in, seeded sample
|
|
6
|
+
* dataset served from an in-memory engine. It is the "feel the product in 10
|
|
7
|
+
* seconds" experience: read mode, PII redaction, and the single-row write flow,
|
|
8
|
+
* all safely fake.
|
|
9
|
+
*
|
|
10
|
+
* The store is backed by Turbine's OWN SQLite engine over `node:sqlite`'s
|
|
11
|
+
* `:memory:` database (a built-in on Node >= 22.5, zero new dependency). Because
|
|
12
|
+
* `:memory:` is per-handle, the store dies with the process and every launch
|
|
13
|
+
* starts pristine: writes genuinely apply (edits stick, a refresh shows them)
|
|
14
|
+
* but nothing is ever persisted anywhere.
|
|
15
|
+
*
|
|
16
|
+
* This module lives under `src/cli/` (coverage-excluded, never imported by
|
|
17
|
+
* library code) and reuses `SqlitePool` + `sqliteDialect` from `../sqlite.js`;
|
|
18
|
+
* it never writes its own SQL evaluator.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.DEMO_SCHEMA = void 0;
|
|
22
|
+
exports.createDemoContext = createDemoContext;
|
|
23
|
+
const node_module_1 = require("node:module");
|
|
24
|
+
const sqlite_js_1 = require("../sqlite.js");
|
|
25
|
+
/**
|
|
26
|
+
* Load `node:sqlite`'s `DatabaseSync` constructor, throwing a clear,
|
|
27
|
+
* demo-specific message on older Node. Kept lazy (called only when a demo
|
|
28
|
+
* context is actually created) so `import`ing this module never crashes the CLI
|
|
29
|
+
* on Node < 22.5.
|
|
30
|
+
*/
|
|
31
|
+
function loadDatabaseSync() {
|
|
32
|
+
let ctor;
|
|
33
|
+
try {
|
|
34
|
+
const req = (0, node_module_1.createRequire)(process.cwd());
|
|
35
|
+
ctor = req('node:sqlite').DatabaseSync;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
ctor = undefined;
|
|
39
|
+
}
|
|
40
|
+
if (typeof ctor !== 'function') {
|
|
41
|
+
throw new Error('studio --demo needs Node 22.5+ (uses the built-in node:sqlite engine)');
|
|
42
|
+
}
|
|
43
|
+
return ctor;
|
|
44
|
+
}
|
|
45
|
+
/** Map a demo column's Postgres-flavored type to a TypeScript type string. */
|
|
46
|
+
function demoTsType(pgType, nullable) {
|
|
47
|
+
let base;
|
|
48
|
+
if (/int|serial/i.test(pgType))
|
|
49
|
+
base = 'number';
|
|
50
|
+
else if (/bool/i.test(pgType))
|
|
51
|
+
base = 'boolean';
|
|
52
|
+
else if (/timestamp|date/i.test(pgType))
|
|
53
|
+
base = 'Date';
|
|
54
|
+
else
|
|
55
|
+
base = 'string';
|
|
56
|
+
return nullable ? `${base} | null` : base;
|
|
57
|
+
}
|
|
58
|
+
function demoColumn(spec) {
|
|
59
|
+
const nullable = spec.nullable === true;
|
|
60
|
+
return {
|
|
61
|
+
name: spec.name,
|
|
62
|
+
field: spec.field,
|
|
63
|
+
dialectType: spec.pgType,
|
|
64
|
+
pgType: spec.pgType,
|
|
65
|
+
tsType: demoTsType(spec.pgType, nullable),
|
|
66
|
+
nullable,
|
|
67
|
+
hasDefault: spec.hasDefault ?? spec.isGenerated ?? false,
|
|
68
|
+
isGenerated: spec.isGenerated === true,
|
|
69
|
+
pii: spec.pii === true,
|
|
70
|
+
isArray: false,
|
|
71
|
+
pgArrayType: 'text[]',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function demoTable(name, columnSpecs, relations = {}) {
|
|
75
|
+
const columns = columnSpecs.map(demoColumn);
|
|
76
|
+
const columnMap = {};
|
|
77
|
+
const reverseColumnMap = {};
|
|
78
|
+
const dateColumns = new Set();
|
|
79
|
+
const pgTypes = {};
|
|
80
|
+
const allColumns = [];
|
|
81
|
+
for (const col of columns) {
|
|
82
|
+
columnMap[col.field] = col.name;
|
|
83
|
+
reverseColumnMap[col.name] = col.field;
|
|
84
|
+
pgTypes[col.name] = col.pgType;
|
|
85
|
+
allColumns.push(col.name);
|
|
86
|
+
if (/timestamp|date/i.test(col.pgType))
|
|
87
|
+
dateColumns.add(col.name);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
name,
|
|
91
|
+
columns,
|
|
92
|
+
columnMap,
|
|
93
|
+
reverseColumnMap,
|
|
94
|
+
dateColumns,
|
|
95
|
+
dialectTypes: pgTypes,
|
|
96
|
+
pgTypes,
|
|
97
|
+
allColumns,
|
|
98
|
+
primaryKey: ['id'],
|
|
99
|
+
uniqueColumns: [['id']],
|
|
100
|
+
relations,
|
|
101
|
+
indexes: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The seeded sample schema. Four tables with realistic relations; `email` and
|
|
106
|
+
* `phone` are tagged `pii` so Studio's redaction path is exercised out of the
|
|
107
|
+
* box.
|
|
108
|
+
*/
|
|
109
|
+
exports.DEMO_SCHEMA = {
|
|
110
|
+
tables: {
|
|
111
|
+
users: demoTable('users', [
|
|
112
|
+
{ name: 'id', field: 'id', pgType: 'int4', isGenerated: true },
|
|
113
|
+
{ name: 'name', field: 'name', pgType: 'text' },
|
|
114
|
+
{ name: 'email', field: 'email', pgType: 'text', pii: true },
|
|
115
|
+
{ name: 'phone', field: 'phone', pgType: 'text', nullable: true, pii: true },
|
|
116
|
+
{ name: 'role', field: 'role', pgType: 'text' },
|
|
117
|
+
{ name: 'created_at', field: 'createdAt', pgType: 'timestamptz' },
|
|
118
|
+
], {
|
|
119
|
+
posts: {
|
|
120
|
+
type: 'hasMany',
|
|
121
|
+
name: 'posts',
|
|
122
|
+
from: 'users',
|
|
123
|
+
to: 'posts',
|
|
124
|
+
foreignKey: 'user_id',
|
|
125
|
+
referenceKey: 'id',
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
posts: demoTable('posts', [
|
|
129
|
+
{ name: 'id', field: 'id', pgType: 'int4', isGenerated: true },
|
|
130
|
+
{ name: 'user_id', field: 'userId', pgType: 'int4' },
|
|
131
|
+
{ name: 'title', field: 'title', pgType: 'text' },
|
|
132
|
+
{ name: 'body', field: 'body', pgType: 'text' },
|
|
133
|
+
{ name: 'published', field: 'published', pgType: 'bool' },
|
|
134
|
+
{ name: 'created_at', field: 'createdAt', pgType: 'timestamptz' },
|
|
135
|
+
], {
|
|
136
|
+
comments: {
|
|
137
|
+
type: 'hasMany',
|
|
138
|
+
name: 'comments',
|
|
139
|
+
from: 'posts',
|
|
140
|
+
to: 'comments',
|
|
141
|
+
foreignKey: 'post_id',
|
|
142
|
+
referenceKey: 'id',
|
|
143
|
+
},
|
|
144
|
+
author: {
|
|
145
|
+
type: 'belongsTo',
|
|
146
|
+
name: 'author',
|
|
147
|
+
from: 'posts',
|
|
148
|
+
to: 'users',
|
|
149
|
+
foreignKey: 'user_id',
|
|
150
|
+
referenceKey: 'id',
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
comments: demoTable('comments', [
|
|
154
|
+
{ name: 'id', field: 'id', pgType: 'int4', isGenerated: true },
|
|
155
|
+
{ name: 'post_id', field: 'postId', pgType: 'int4' },
|
|
156
|
+
{ name: 'user_id', field: 'userId', pgType: 'int4' },
|
|
157
|
+
{ name: 'body', field: 'body', pgType: 'text' },
|
|
158
|
+
{ name: 'created_at', field: 'createdAt', pgType: 'timestamptz' },
|
|
159
|
+
], {
|
|
160
|
+
post: {
|
|
161
|
+
type: 'belongsTo',
|
|
162
|
+
name: 'post',
|
|
163
|
+
from: 'comments',
|
|
164
|
+
to: 'posts',
|
|
165
|
+
foreignKey: 'post_id',
|
|
166
|
+
referenceKey: 'id',
|
|
167
|
+
},
|
|
168
|
+
user: {
|
|
169
|
+
type: 'belongsTo',
|
|
170
|
+
name: 'user',
|
|
171
|
+
from: 'comments',
|
|
172
|
+
to: 'users',
|
|
173
|
+
foreignKey: 'user_id',
|
|
174
|
+
referenceKey: 'id',
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
orgs: demoTable('orgs', [
|
|
178
|
+
{ name: 'id', field: 'id', pgType: 'int4', isGenerated: true },
|
|
179
|
+
{ name: 'name', field: 'name', pgType: 'text' },
|
|
180
|
+
{ name: 'plan', field: 'plan', pgType: 'text' },
|
|
181
|
+
]),
|
|
182
|
+
},
|
|
183
|
+
enums: {},
|
|
184
|
+
};
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Deterministic seed data (hardcoded, no randomness)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/** A fixed base instant so every launch produces byte-identical timestamps. */
|
|
189
|
+
const SEED_EPOCH = Date.UTC(2024, 0, 1, 9, 0, 0);
|
|
190
|
+
const DAY_MS = 86_400_000;
|
|
191
|
+
/** ISO timestamp `n` days after the seed epoch (deterministic, no `Date.now()`). */
|
|
192
|
+
function seedTime(dayOffset) {
|
|
193
|
+
return new Date(SEED_EPOCH + dayOffset * DAY_MS).toISOString();
|
|
194
|
+
}
|
|
195
|
+
const USERS = [
|
|
196
|
+
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.com', phone: '+1-202-555-0101', role: 'admin' },
|
|
197
|
+
{ id: 2, name: 'Grace Hopper', email: 'grace@example.com', phone: '+1-202-555-0102', role: 'admin' },
|
|
198
|
+
{ id: 3, name: 'Alan Turing', email: 'alan@example.com', phone: '+1-202-555-0103', role: 'member' },
|
|
199
|
+
{ id: 4, name: 'Katherine Johnson', email: 'katherine@example.com', phone: '+1-202-555-0104', role: 'member' },
|
|
200
|
+
{ id: 5, name: 'Linus Torvalds', email: 'linus@example.com', phone: '+1-202-555-0105', role: 'member' },
|
|
201
|
+
{ id: 6, name: 'Margaret Hamilton', email: 'margaret@example.com', phone: null, role: 'member' },
|
|
202
|
+
{ id: 7, name: 'Dennis Ritchie', email: 'dennis@example.com', phone: '+1-202-555-0107', role: 'member' },
|
|
203
|
+
{ id: 8, name: 'Barbara Liskov', email: 'barbara@example.com', phone: '+1-202-555-0108', role: 'viewer' },
|
|
204
|
+
];
|
|
205
|
+
const ORGS = [
|
|
206
|
+
{ id: 1, name: 'Analytical Engines', plan: 'pro' },
|
|
207
|
+
{ id: 2, name: 'Compiler Collective', plan: 'team' },
|
|
208
|
+
{ id: 3, name: 'Kernel Works', plan: 'free' },
|
|
209
|
+
];
|
|
210
|
+
const POST_TOPICS = [
|
|
211
|
+
'Notes on the Analytical Engine',
|
|
212
|
+
'Debugging the first moth',
|
|
213
|
+
'On computable numbers',
|
|
214
|
+
'Orbital mechanics by hand',
|
|
215
|
+
'Why monolithic kernels win',
|
|
216
|
+
'The Apollo guidance software',
|
|
217
|
+
'A tour of the C language',
|
|
218
|
+
'Abstraction and specification',
|
|
219
|
+
'Loop invariants in practice',
|
|
220
|
+
'Sequential vs parallel search',
|
|
221
|
+
];
|
|
222
|
+
/** 20 deterministic posts spread across the 8 users. */
|
|
223
|
+
const POSTS = Array.from({ length: 20 }, (_, i) => {
|
|
224
|
+
const id = i + 1;
|
|
225
|
+
const userId = (i % USERS.length) + 1;
|
|
226
|
+
return {
|
|
227
|
+
id,
|
|
228
|
+
userId,
|
|
229
|
+
title: `${POST_TOPICS[i % POST_TOPICS.length]} (part ${Math.floor(i / POST_TOPICS.length) + 1})`,
|
|
230
|
+
body: `A short sample body for post ${id}, written by user ${userId}. Everything here is fake demo data.`,
|
|
231
|
+
published: id % 4 !== 0,
|
|
232
|
+
createdAt: seedTime(id),
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
/** 40 deterministic comments (two per post), authored round-robin. */
|
|
236
|
+
const COMMENTS = Array.from({ length: 40 }, (_, i) => {
|
|
237
|
+
const id = i + 1;
|
|
238
|
+
const postId = (i % POSTS.length) + 1;
|
|
239
|
+
const userId = ((i * 3) % USERS.length) + 1;
|
|
240
|
+
return {
|
|
241
|
+
id,
|
|
242
|
+
postId,
|
|
243
|
+
userId,
|
|
244
|
+
body: `Comment ${id} on post ${postId}. Nicely done. This is seeded demo content.`,
|
|
245
|
+
createdAt: seedTime(20 + id),
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// DDL + seeding
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
/** SQLite DDL for the demo tables. Types map cleanly onto SQLite affinities. */
|
|
252
|
+
const DEMO_DDL = `
|
|
253
|
+
CREATE TABLE orgs (
|
|
254
|
+
id INTEGER PRIMARY KEY,
|
|
255
|
+
name TEXT NOT NULL,
|
|
256
|
+
plan TEXT NOT NULL
|
|
257
|
+
);
|
|
258
|
+
CREATE TABLE users (
|
|
259
|
+
id INTEGER PRIMARY KEY,
|
|
260
|
+
name TEXT NOT NULL,
|
|
261
|
+
email TEXT NOT NULL,
|
|
262
|
+
phone TEXT,
|
|
263
|
+
role TEXT NOT NULL,
|
|
264
|
+
created_at TEXT NOT NULL
|
|
265
|
+
);
|
|
266
|
+
CREATE TABLE posts (
|
|
267
|
+
id INTEGER PRIMARY KEY,
|
|
268
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
269
|
+
title TEXT NOT NULL,
|
|
270
|
+
body TEXT NOT NULL,
|
|
271
|
+
published INTEGER NOT NULL,
|
|
272
|
+
created_at TEXT NOT NULL
|
|
273
|
+
);
|
|
274
|
+
CREATE TABLE comments (
|
|
275
|
+
id INTEGER PRIMARY KEY,
|
|
276
|
+
post_id INTEGER NOT NULL REFERENCES posts(id),
|
|
277
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
278
|
+
body TEXT NOT NULL,
|
|
279
|
+
created_at TEXT NOT NULL
|
|
280
|
+
);
|
|
281
|
+
`;
|
|
282
|
+
function seedDemoData(db) {
|
|
283
|
+
const insOrg = db.prepare('INSERT INTO orgs (id, name, plan) VALUES (?, ?, ?)');
|
|
284
|
+
for (const o of ORGS)
|
|
285
|
+
insOrg.run(o.id, o.name, o.plan);
|
|
286
|
+
const insUser = db.prepare('INSERT INTO users (id, name, email, phone, role, created_at) VALUES (?, ?, ?, ?, ?, ?)');
|
|
287
|
+
for (const u of USERS)
|
|
288
|
+
insUser.run(u.id, u.name, u.email, u.phone, u.role, seedTime(u.id));
|
|
289
|
+
const insPost = db.prepare('INSERT INTO posts (id, user_id, title, body, published, created_at) VALUES (?, ?, ?, ?, ?, ?)');
|
|
290
|
+
for (const p of POSTS)
|
|
291
|
+
insPost.run(p.id, p.userId, p.title, p.body, p.published ? 1 : 0, p.createdAt);
|
|
292
|
+
const insComment = db.prepare('INSERT INTO comments (id, post_id, user_id, body, created_at) VALUES (?, ?, ?, ?, ?)');
|
|
293
|
+
for (const c of COMMENTS)
|
|
294
|
+
insComment.run(c.id, c.postId, c.userId, c.body, c.createdAt);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Open a fresh, seeded in-memory demo store and return the pool + metadata +
|
|
298
|
+
* dialect Studio needs. Each call yields an independent, pristine database
|
|
299
|
+
* (`:memory:` is per-handle), so demo launches never share state.
|
|
300
|
+
*
|
|
301
|
+
* @throws Error on Node < 22.5 (no built-in `node:sqlite`).
|
|
302
|
+
*/
|
|
303
|
+
function createDemoContext() {
|
|
304
|
+
const DatabaseSyncCtor = loadDatabaseSync();
|
|
305
|
+
const db = new DatabaseSyncCtor(':memory:');
|
|
306
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
307
|
+
db.exec(DEMO_DDL);
|
|
308
|
+
seedDemoData(db);
|
|
309
|
+
return { pool: new sqlite_js_1.SqlitePool(db), metadata: exports.DEMO_SCHEMA, dialect: sqlite_js_1.sqliteDialect };
|
|
310
|
+
}
|