turbine-orm 0.36.0 → 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 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 opts in to single-row writes)
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.
@@ -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 Launch local read-only web UI
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 url = requireUrl(config);
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(`Studio is running`);
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
- // Loud startup warnings for the opt-in modes that widen Studio's surface.
1550
- if (args.write) {
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
- if (args.showPii) {
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 opts in to single-row writes)')}`);
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
+ }