@stardeck-customer-apps/data-store-sdk 0.4.0 → 0.6.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.
@@ -0,0 +1,662 @@
1
+ import {
2
+ readDataStoreManifest,
3
+ resolveDataStore
4
+ } from "./chunk-FEKROS4B.mjs";
5
+
6
+ // src/cli/generate-types.ts
7
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
8
+ import { resolve } from "path";
9
+ import { parseEnv } from "util";
10
+ import { Pool } from "@neondatabase/serverless";
11
+
12
+ // src/cli/module-type-slices.ts
13
+ import { existsSync, readdirSync, readFileSync } from "fs";
14
+ import { join } from "path";
15
+ function toPascalCase(name) {
16
+ return name.split(/[_-]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
17
+ }
18
+ function parseModuleManifestSlice(content, sourceLabel, warn) {
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(content);
22
+ } catch {
23
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
24
+ return null;
25
+ }
26
+ if (typeof parsed !== "object" || parsed === null) {
27
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
28
+ return null;
29
+ }
30
+ const record = parsed;
31
+ const name = record.name;
32
+ const tables = record.tables;
33
+ if (typeof name !== "string" || name.length === 0) {
34
+ warn(`Skipping module manifest at ${sourceLabel}: missing or invalid "name"`);
35
+ return null;
36
+ }
37
+ if (!Array.isArray(tables) || tables.length === 0) {
38
+ return null;
39
+ }
40
+ const tableNames = [];
41
+ for (const entry of tables) {
42
+ if (typeof entry !== "string" || entry.length === 0) {
43
+ warn(`Skipping module manifest at ${sourceLabel}: invalid "tables" entry`);
44
+ return null;
45
+ }
46
+ tableNames.push(entry);
47
+ }
48
+ return { name, tables: tableNames };
49
+ }
50
+ function discoverModuleManifests(modulesDir, warn) {
51
+ if (!existsSync(modulesDir)) {
52
+ return [];
53
+ }
54
+ let entries;
55
+ try {
56
+ entries = readdirSync(modulesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
57
+ } catch {
58
+ warn(`Could not read modules directory: ${modulesDir}`);
59
+ return [];
60
+ }
61
+ const manifests = [];
62
+ for (const dirName of entries) {
63
+ const manifestPath = join(modulesDir, dirName, "module.json");
64
+ if (!existsSync(manifestPath)) {
65
+ continue;
66
+ }
67
+ let content;
68
+ try {
69
+ content = readFileSync(manifestPath, "utf-8");
70
+ } catch {
71
+ warn(`Skipping unreadable module manifest at ${manifestPath}`);
72
+ continue;
73
+ }
74
+ const slice = parseModuleManifestSlice(content, manifestPath, warn);
75
+ if (slice) {
76
+ manifests.push(slice);
77
+ }
78
+ }
79
+ return manifests.sort((a, b) => a.name.localeCompare(b.name));
80
+ }
81
+ function shouldLoadModuleManifests(noModules) {
82
+ return !noModules;
83
+ }
84
+ function emitModuleDbSlices(schemaTableNames, manifests, warn) {
85
+ const lines = [];
86
+ const moduleMapEntries = [];
87
+ for (const manifest of manifests) {
88
+ const presentTables = manifest.tables.filter((t) => schemaTableNames.has(t));
89
+ const absentTables = manifest.tables.filter((t) => !schemaTableNames.has(t));
90
+ if (absentTables.length > 0) {
91
+ warn(`Module "${manifest.name}": table(s) not in database: ${absentTables.join(", ")}`);
92
+ }
93
+ if (presentTables.length === 0) {
94
+ continue;
95
+ }
96
+ const interfaceName = `${toPascalCase(manifest.name)}ModuleDB`;
97
+ lines.push(`export interface ${interfaceName} {`);
98
+ for (const tableName of presentTables) {
99
+ const tableTypeName = `${toPascalCase(tableName)}Table`;
100
+ lines.push(` ${tableName}: ${tableTypeName};`);
101
+ }
102
+ lines.push("}");
103
+ lines.push("");
104
+ moduleMapEntries.push({ name: manifest.name, interfaceName });
105
+ }
106
+ if (moduleMapEntries.length === 0) {
107
+ return "";
108
+ }
109
+ lines.push("export interface ModuleDBs {");
110
+ for (const { name, interfaceName } of moduleMapEntries) {
111
+ lines.push(` "${name}": ${interfaceName};`);
112
+ }
113
+ lines.push("}");
114
+ lines.push("");
115
+ return lines.join("\n");
116
+ }
117
+
118
+ // src/cli/schema-ddl.ts
119
+ function quoteIdent(name) {
120
+ return `"${name.replace(/"/g, '""')}"`;
121
+ }
122
+ var TRACKED_EXTENSIONS = /* @__PURE__ */ new Set(["pgcrypto", "pg_trgm", "uuid-ossp", "citext"]);
123
+ function resolveSqlType(row) {
124
+ const dataType = row.data_type;
125
+ if (dataType === "ARRAY") {
126
+ return `${row.udt_name.replace(/^_/, "")}[]`;
127
+ }
128
+ if (dataType === "USER-DEFINED") {
129
+ return quoteIdent(row.udt_name);
130
+ }
131
+ if (dataType === "character varying") {
132
+ return row.character_maximum_length ? `varchar(${row.character_maximum_length})` : "varchar";
133
+ }
134
+ if (dataType === "character") {
135
+ return row.character_maximum_length ? `char(${row.character_maximum_length})` : "char";
136
+ }
137
+ if (dataType === "numeric") {
138
+ return row.numeric_precision !== null && row.numeric_scale !== null ? `numeric(${row.numeric_precision},${row.numeric_scale})` : "numeric";
139
+ }
140
+ if (dataType === "timestamp with time zone") return "timestamptz";
141
+ if (dataType === "timestamp without time zone") return "timestamp";
142
+ if (dataType === "time with time zone") return "timetz";
143
+ if (dataType === "time without time zone") return "time";
144
+ return dataType;
145
+ }
146
+ async function introspectSchemaSnapshot(executor) {
147
+ const [extensions, enums, columns, constraints, fks, checks, indexes] = await Promise.all([
148
+ executor.query(`SELECT extname FROM pg_extension ORDER BY extname`),
149
+ executor.query(
150
+ `SELECT t.typname AS name, e.enumlabel AS value
151
+ FROM pg_type t
152
+ JOIN pg_enum e ON e.enumtypid = t.oid
153
+ JOIN pg_namespace n ON n.oid = t.typnamespace
154
+ WHERE n.nspname = 'public'
155
+ ORDER BY t.typname, e.enumsortorder`
156
+ ),
157
+ executor.query(
158
+ `SELECT c.table_name, c.column_name, c.data_type, c.udt_name, c.is_nullable,
159
+ c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale,
160
+ c.is_generated, c.generation_expression
161
+ FROM information_schema.columns c
162
+ JOIN information_schema.tables t
163
+ ON c.table_name = t.table_name AND c.table_schema = t.table_schema
164
+ WHERE c.table_schema = 'public'
165
+ AND t.table_type = 'BASE TABLE'
166
+ AND t.table_name NOT LIKE '\\_deleted\\_%'
167
+ ORDER BY c.table_name, c.ordinal_position`
168
+ ),
169
+ executor.query(
170
+ `SELECT tc.table_name, tc.constraint_name, tc.constraint_type, ku.column_name
171
+ FROM information_schema.table_constraints tc
172
+ JOIN information_schema.key_column_usage ku
173
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
174
+ WHERE tc.table_schema = 'public'
175
+ AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE')
176
+ ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
177
+ ),
178
+ executor.query(
179
+ // Read column pairings from pg_catalog: information_schema's
180
+ // constraint_column_usage is uncorrelated with key_column_usage, so
181
+ // joining them for a composite FK produces a cross product. Unnesting
182
+ // conkey/confkey together WITH ORDINALITY keeps referencing↔referenced
183
+ // columns paired and ordered.
184
+ `SELECT con.conname AS constraint_name,
185
+ rel.relname AS table_name,
186
+ att.attname AS column_name,
187
+ frel.relname AS referenced_table,
188
+ fatt.attname AS referenced_column,
189
+ CASE con.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
190
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS delete_rule,
191
+ CASE con.confupdtype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
192
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS update_rule
193
+ FROM pg_constraint con
194
+ JOIN pg_class rel ON rel.oid = con.conrelid
195
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
196
+ JOIN pg_class frel ON frel.oid = con.confrelid
197
+ JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(conkey, confkey, ord) ON true
198
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = cols.conkey
199
+ JOIN pg_attribute fatt ON fatt.attrelid = con.confrelid AND fatt.attnum = cols.confkey
200
+ WHERE ns.nspname = 'public' AND con.contype = 'f'
201
+ ORDER BY rel.relname, con.conname, cols.ord`
202
+ ),
203
+ executor.query(
204
+ `SELECT rel.relname AS table_name, con.conname AS name,
205
+ pg_get_constraintdef(con.oid) AS definition
206
+ FROM pg_constraint con
207
+ JOIN pg_class rel ON rel.oid = con.conrelid
208
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
209
+ WHERE ns.nspname = 'public' AND con.contype = 'c'
210
+ AND rel.relname NOT LIKE '\\_deleted\\_%'
211
+ ORDER BY rel.relname, con.conname`
212
+ ),
213
+ executor.query(
214
+ `SELECT i.indexdef
215
+ FROM pg_indexes i
216
+ WHERE i.schemaname = 'public'
217
+ AND i.tablename NOT LIKE '\\_deleted\\_%'
218
+ AND NOT EXISTS (
219
+ SELECT 1 FROM pg_constraint c
220
+ JOIN pg_class cls ON cls.oid = c.conindid
221
+ WHERE cls.relname = i.indexname
222
+ )
223
+ ORDER BY i.indexname`
224
+ )
225
+ ]);
226
+ const enumMap = /* @__PURE__ */ new Map();
227
+ for (const row of enums.rows) {
228
+ const name = String(row.name);
229
+ if (!enumMap.has(name)) enumMap.set(name, []);
230
+ enumMap.get(name).push(String(row.value));
231
+ }
232
+ const tableMap = /* @__PURE__ */ new Map();
233
+ for (const row of columns.rows) {
234
+ const tableName = String(row.table_name);
235
+ if (!tableMap.has(tableName)) {
236
+ tableMap.set(tableName, { name: tableName, columns: [], primaryKey: [], uniques: [] });
237
+ }
238
+ const defaultExpr = row.column_default === null ? null : String(row.column_default);
239
+ const isSerial = defaultExpr !== null && /^nextval\(/.test(defaultExpr);
240
+ const isGenerated = row.is_generated === "ALWAYS";
241
+ tableMap.get(tableName).columns.push({
242
+ name: String(row.column_name),
243
+ sqlType: resolveSqlType({
244
+ data_type: String(row.data_type),
245
+ udt_name: String(row.udt_name),
246
+ character_maximum_length: row.character_maximum_length,
247
+ numeric_precision: row.numeric_precision,
248
+ numeric_scale: row.numeric_scale
249
+ }),
250
+ nullable: row.is_nullable === "YES",
251
+ defaultExpr: isGenerated ? null : defaultExpr,
252
+ isSerial: !isGenerated && isSerial,
253
+ generatedExpr: isGenerated ? String(row.generation_expression) : null
254
+ });
255
+ }
256
+ const constraintGroups = /* @__PURE__ */ new Map();
257
+ for (const row of constraints.rows) {
258
+ const key = String(row.constraint_name);
259
+ if (!constraintGroups.has(key)) {
260
+ constraintGroups.set(key, {
261
+ table: String(row.table_name),
262
+ type: String(row.constraint_type),
263
+ columns: []
264
+ });
265
+ }
266
+ constraintGroups.get(key).columns.push(String(row.column_name));
267
+ }
268
+ for (const group of constraintGroups.values()) {
269
+ const table = tableMap.get(group.table);
270
+ if (!table) continue;
271
+ if (group.type === "PRIMARY KEY") {
272
+ table.primaryKey = group.columns;
273
+ } else if (group.type === "UNIQUE") {
274
+ table.uniques.push(group.columns);
275
+ }
276
+ }
277
+ const fkGroups = /* @__PURE__ */ new Map();
278
+ for (const row of fks.rows) {
279
+ const key = String(row.constraint_name);
280
+ if (!fkGroups.has(key)) {
281
+ fkGroups.set(key, {
282
+ constraintName: key,
283
+ table: String(row.table_name),
284
+ columns: [],
285
+ referencedTable: String(row.referenced_table),
286
+ referencedColumns: [],
287
+ onDelete: String(row.delete_rule),
288
+ onUpdate: String(row.update_rule)
289
+ });
290
+ }
291
+ const fk = fkGroups.get(key);
292
+ fk.columns.push(String(row.column_name));
293
+ fk.referencedColumns.push(String(row.referenced_column));
294
+ }
295
+ return {
296
+ extensions: extensions.rows.map((r) => String(r.extname)).filter((name) => TRACKED_EXTENSIONS.has(name)),
297
+ enums: Array.from(enumMap, ([name, values]) => ({ name, values })),
298
+ tables: Array.from(tableMap.values()),
299
+ foreignKeys: Array.from(fkGroups.values()),
300
+ checks: checks.rows.map((r) => ({
301
+ table: String(r.table_name),
302
+ name: String(r.name),
303
+ definition: String(r.definition)
304
+ })),
305
+ indexes: indexes.rows.map((r) => ({ definition: String(r.indexdef) }))
306
+ };
307
+ }
308
+ function columnDdl(column, isSinglePk) {
309
+ let sqlType = column.sqlType;
310
+ if (column.isSerial) {
311
+ sqlType = sqlType === "bigint" ? "bigserial" : sqlType === "smallint" ? "smallserial" : "serial";
312
+ }
313
+ let ddl = `${quoteIdent(column.name)} ${sqlType}`;
314
+ if (isSinglePk) ddl += " PRIMARY KEY";
315
+ if (!column.nullable && !isSinglePk) ddl += " NOT NULL";
316
+ if (column.generatedExpr) {
317
+ ddl += ` GENERATED ALWAYS AS (${column.generatedExpr}) STORED`;
318
+ } else if (column.defaultExpr && !column.isSerial) {
319
+ ddl += ` DEFAULT ${column.defaultExpr}`;
320
+ }
321
+ return ddl;
322
+ }
323
+ function generateSchemaSql(snapshot) {
324
+ const lines = [
325
+ "-- Auto-generated by @stardeck-customer-apps/data-store-sdk",
326
+ "-- Schema snapshot of the live data store, applied to PGlite by the test harness.",
327
+ "-- Do not edit manually \u2014 regenerate with: npx stardeck-data-store generate-types",
328
+ ""
329
+ ];
330
+ for (const ext of snapshot.extensions) {
331
+ lines.push(`CREATE EXTENSION IF NOT EXISTS ${ext === "uuid-ossp" ? '"uuid-ossp"' : ext};`);
332
+ }
333
+ if (snapshot.extensions.length > 0) lines.push("");
334
+ for (const enumType of snapshot.enums) {
335
+ const values = enumType.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
336
+ lines.push(`CREATE TYPE ${quoteIdent(enumType.name)} AS ENUM (${values});`);
337
+ }
338
+ if (snapshot.enums.length > 0) lines.push("");
339
+ for (const table of snapshot.tables) {
340
+ const singlePk = table.primaryKey.length === 1 ? table.primaryKey[0] : null;
341
+ const parts = table.columns.map((col) => " " + columnDdl(col, col.name === singlePk));
342
+ if (table.primaryKey.length > 1) {
343
+ parts.push(` PRIMARY KEY (${table.primaryKey.map(quoteIdent).join(", ")})`);
344
+ }
345
+ for (const unique of table.uniques) {
346
+ parts.push(` UNIQUE (${unique.map(quoteIdent).join(", ")})`);
347
+ }
348
+ lines.push(`CREATE TABLE ${quoteIdent(table.name)} (`);
349
+ lines.push(parts.join(",\n"));
350
+ lines.push(`);`);
351
+ lines.push("");
352
+ }
353
+ for (const check of snapshot.checks) {
354
+ lines.push(
355
+ `ALTER TABLE ${quoteIdent(check.table)} ADD CONSTRAINT ${quoteIdent(check.name)} ${check.definition};`
356
+ );
357
+ }
358
+ if (snapshot.checks.length > 0) lines.push("");
359
+ for (const fk of snapshot.foreignKeys) {
360
+ let stmt = `ALTER TABLE ${quoteIdent(fk.table)} ADD CONSTRAINT ${quoteIdent(fk.constraintName)} FOREIGN KEY (${fk.columns.map(quoteIdent).join(", ")}) REFERENCES ${quoteIdent(fk.referencedTable)} (${fk.referencedColumns.map(quoteIdent).join(", ")})`;
361
+ if (fk.onDelete !== "NO ACTION") stmt += ` ON DELETE ${fk.onDelete}`;
362
+ if (fk.onUpdate !== "NO ACTION") stmt += ` ON UPDATE ${fk.onUpdate}`;
363
+ lines.push(`${stmt};`);
364
+ }
365
+ if (snapshot.foreignKeys.length > 0) lines.push("");
366
+ for (const index of snapshot.indexes) {
367
+ lines.push(`${index.definition};`);
368
+ }
369
+ if (snapshot.indexes.length > 0) lines.push("");
370
+ return lines.join("\n");
371
+ }
372
+
373
+ // src/cli/generate-types.ts
374
+ var PG_TYPE_MAP = {
375
+ text: "string",
376
+ varchar: "string",
377
+ "character varying": "string",
378
+ char: "string",
379
+ character: "string",
380
+ uuid: "string",
381
+ integer: "number",
382
+ int: "number",
383
+ smallint: "number",
384
+ bigint: "string",
385
+ // bigint as string to avoid precision loss
386
+ numeric: "string",
387
+ decimal: "string",
388
+ real: "number",
389
+ "double precision": "number",
390
+ boolean: "boolean",
391
+ jsonb: "unknown",
392
+ json: "unknown",
393
+ "timestamp with time zone": "Date",
394
+ "timestamp without time zone": "Date",
395
+ timestamp: "Date",
396
+ date: "string",
397
+ time: "string",
398
+ "time with time zone": "string",
399
+ "time without time zone": "string",
400
+ bytea: "Buffer",
401
+ ARRAY: "unknown[]"
402
+ };
403
+ function pgTypeToTs(pgType) {
404
+ return PG_TYPE_MAP[pgType] ?? "unknown";
405
+ }
406
+ function parseCliArgs(argv, options = {}) {
407
+ const cwd = options.cwd ?? process.cwd();
408
+ const env = options.env ?? process.env;
409
+ let connectionString = env.DATA_STORE_URL;
410
+ let store;
411
+ let outputPath = "./src/generated/data-store-types.ts";
412
+ let schemaOutputPath;
413
+ let emitSchema = !existsSync2(resolve(cwd, "datastore/ledger-order.json"));
414
+ let modulesDir = resolve(cwd, "./src/modules");
415
+ let noModules = false;
416
+ let help = false;
417
+ for (let i = 0; i < argv.length; i++) {
418
+ if (argv[i] === "--connection-string" && argv[i + 1]) {
419
+ connectionString = argv[++i];
420
+ } else if (argv[i] === "--store" && argv[i + 1]) {
421
+ store = argv[++i];
422
+ } else if (argv[i] === "--output" && argv[i + 1]) {
423
+ outputPath = argv[++i];
424
+ } else if (argv[i] === "--schema-output" && argv[i + 1]) {
425
+ schemaOutputPath = argv[++i];
426
+ emitSchema = true;
427
+ } else if (argv[i] === "--no-schema") {
428
+ emitSchema = false;
429
+ } else if (argv[i] === "--modules-dir" && argv[i + 1]) {
430
+ modulesDir = resolve(cwd, argv[++i]);
431
+ } else if (argv[i] === "--no-modules") {
432
+ noModules = true;
433
+ } else if (argv[i] === "--help") {
434
+ help = true;
435
+ }
436
+ }
437
+ return {
438
+ connectionString,
439
+ store,
440
+ outputPath,
441
+ schemaOutputPath,
442
+ emitSchema,
443
+ modulesDir,
444
+ noModules,
445
+ help
446
+ };
447
+ }
448
+ async function introspectSchema(connectionString) {
449
+ const pool = new Pool({ connectionString });
450
+ try {
451
+ const { rows } = await pool.query(`
452
+ SELECT
453
+ c.table_name,
454
+ c.column_name,
455
+ c.data_type,
456
+ c.is_nullable,
457
+ c.column_default
458
+ FROM information_schema.columns c
459
+ JOIN information_schema.tables t
460
+ ON c.table_name = t.table_name AND c.table_schema = t.table_schema
461
+ WHERE c.table_schema = 'public'
462
+ AND t.table_type = 'BASE TABLE'
463
+ AND t.table_name NOT LIKE '_deleted_%'
464
+ ORDER BY c.table_name, c.ordinal_position
465
+ `);
466
+ const tables = /* @__PURE__ */ new Map();
467
+ for (const row of rows) {
468
+ if (!tables.has(row.table_name)) {
469
+ tables.set(row.table_name, []);
470
+ }
471
+ tables.get(row.table_name).push(row);
472
+ }
473
+ return tables;
474
+ } finally {
475
+ await pool.end();
476
+ }
477
+ }
478
+ function generateTypeScript(tables, moduleSlices) {
479
+ const lines = [
480
+ "// AUTO-GENERATED by @stardeck-customer-apps/data-store-sdk \u2014 DO NOT EDIT.",
481
+ "// This file is overwritten in full on every run of:",
482
+ "// npx stardeck-data-store generate-types",
483
+ "// Anything you add here (hand-written or derived types) WILL BE LOST on the next",
484
+ "// regeneration. Put those in a separate file that imports from this one, e.g.",
485
+ "// src/lib/<domain>-types.ts importing the table types or DB from this file.",
486
+ "// When module manifests are discoverable, per-module DB slices and ModuleDBs are",
487
+ "// appended below the flat DB interface.",
488
+ "",
489
+ 'import type { Generated } from "kysely";',
490
+ ""
491
+ ];
492
+ for (const [tableName, columns] of tables) {
493
+ const interfaceName = toPascalCase(tableName) + "Table";
494
+ lines.push(`export interface ${interfaceName} {`);
495
+ for (const col of columns) {
496
+ const tsType = pgTypeToTs(col.data_type);
497
+ const isGenerated = col.column_default !== null;
498
+ const isNullable = col.is_nullable === "YES";
499
+ let type = tsType;
500
+ if (isNullable) {
501
+ type = `${tsType} | null`;
502
+ }
503
+ if (isGenerated) {
504
+ type = `Generated<${type}>`;
505
+ }
506
+ lines.push(` ${col.column_name}: ${type};`);
507
+ }
508
+ lines.push("}");
509
+ lines.push("");
510
+ }
511
+ lines.push("export interface DB {");
512
+ for (const tableName of tables.keys()) {
513
+ const interfaceName = toPascalCase(tableName) + "Table";
514
+ lines.push(` ${tableName}: ${interfaceName};`);
515
+ }
516
+ lines.push("}");
517
+ lines.push("");
518
+ if (moduleSlices) {
519
+ const trimmed = moduleSlices.trimEnd();
520
+ if (trimmed.length > 0) {
521
+ lines.push(trimmed);
522
+ lines.push("");
523
+ }
524
+ }
525
+ return lines.join("\n");
526
+ }
527
+ async function runGenerateTypes(options) {
528
+ const {
529
+ connectionString,
530
+ outputPath,
531
+ schemaOutputPath,
532
+ emitSchema = true,
533
+ modulesDir,
534
+ noModules,
535
+ introspect = introspectSchema,
536
+ warn = (message) => console.warn(message)
537
+ } = options;
538
+ console.log("Introspecting database schema...");
539
+ const tables = await introspect(connectionString);
540
+ if (tables.size === 0) {
541
+ console.log("No tables found in database.");
542
+ return;
543
+ }
544
+ console.log(`Found ${tables.size} table(s): ${Array.from(tables.keys()).join(", ")}`);
545
+ let moduleSlices = "";
546
+ if (shouldLoadModuleManifests(noModules)) {
547
+ const manifests = discoverModuleManifests(modulesDir, warn);
548
+ if (manifests.length > 0) {
549
+ moduleSlices = emitModuleDbSlices(new Set(tables.keys()), manifests, warn);
550
+ }
551
+ }
552
+ const typeScript = generateTypeScript(tables, moduleSlices || void 0);
553
+ const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
554
+ if (dir) {
555
+ const { mkdirSync } = await import("fs");
556
+ mkdirSync(dir, { recursive: true });
557
+ }
558
+ writeFileSync(outputPath, typeScript, "utf-8");
559
+ console.log(`Types written to ${outputPath}`);
560
+ if (emitSchema) {
561
+ const schemaPath = schemaOutputPath ?? `${dir ? `${dir}/` : ""}data-store-schema.sql`;
562
+ const pool = new Pool({ connectionString });
563
+ try {
564
+ const snapshot = await introspectSchemaSnapshot(pool);
565
+ writeFileSync(schemaPath, generateSchemaSql(snapshot), "utf-8");
566
+ console.log(`Schema snapshot written to ${schemaPath} (used by the test harness)`);
567
+ } finally {
568
+ await pool.end();
569
+ }
570
+ }
571
+ }
572
+ function connectionStringFromManifest(store) {
573
+ const databases = readDataStoreManifest().filter((entry) => entry.storeType !== "storage");
574
+ if (store) {
575
+ const match = resolveDataStore({ storeName: store });
576
+ if (!match?.url) {
577
+ return {
578
+ error: `No connected database store matches --store ${store}. Connected: ${listStores(databases)}.`
579
+ };
580
+ }
581
+ return { url: match.url };
582
+ }
583
+ if (databases.length === 1 && databases[0]?.url) return { url: databases[0].url };
584
+ if (databases.length === 0) {
585
+ return {
586
+ error: "No connected database store found. Run `npm run env:pull` in apps/web to write .env.local, or pass --connection-string."
587
+ };
588
+ }
589
+ return {
590
+ error: `${databases.length} database stores are connected; pass --store <bindingKey>. Connected: ${listStores(databases)}.`
591
+ };
592
+ }
593
+ function listStores(entries) {
594
+ return entries.length === 0 ? "none" : entries.map((entry) => entry.bindingKey ?? entry.slug).join(", ");
595
+ }
596
+ function loadEnvLocal(cwd) {
597
+ const path = resolve(cwd, ".env.local");
598
+ let content;
599
+ try {
600
+ content = readFileSync2(path, "utf8");
601
+ } catch (error) {
602
+ if (error.code !== "ENOENT") {
603
+ console.warn(`Warning: could not read ${path}: ${error.message}`);
604
+ }
605
+ return;
606
+ }
607
+ for (const [key, value] of Object.entries(parseEnv(content))) {
608
+ if (value !== void 0 && process.env[key] === void 0) {
609
+ process.env[key] = value.replace(/\\\$/g, "$");
610
+ }
611
+ }
612
+ }
613
+ async function main(argv = process.argv.slice(2)) {
614
+ loadEnvLocal(process.cwd());
615
+ const parsed = parseCliArgs(argv);
616
+ if (parsed.help) {
617
+ console.log(`Usage: stardeck-data-store generate-types [options]
618
+
619
+ Options:
620
+ --connection-string <url> Postgres connection string (default: DATA_STORE_URL, else the
621
+ app's connected store from STARDECK_DATA_STORES / .env.local)
622
+ --store <bindingKey> Which connected store to read when the app has several
623
+ --output <path> Output file path (default: ./src/generated/data-store-types.ts)
624
+ --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
625
+ --no-schema Skip the DDL snapshot used by the test harness (implied when
626
+ datastore/ledger-order.json exists: a Blueprint composes its own)
627
+ --modules-dir <path> Directory containing module folders (default: ./src/modules)
628
+ --no-modules Skip per-module DB slice generation
629
+ --help Show this help message`);
630
+ process.exit(0);
631
+ }
632
+ let connectionString = parsed.connectionString;
633
+ if (!connectionString) {
634
+ const resolved = connectionStringFromManifest(parsed.store);
635
+ if ("error" in resolved) {
636
+ console.error(`Error: ${resolved.error}`);
637
+ process.exit(1);
638
+ }
639
+ connectionString = resolved.url;
640
+ }
641
+ await runGenerateTypes({
642
+ connectionString,
643
+ outputPath: parsed.outputPath,
644
+ schemaOutputPath: parsed.schemaOutputPath,
645
+ emitSchema: parsed.emitSchema,
646
+ modulesDir: parsed.modulesDir,
647
+ noModules: parsed.noModules
648
+ });
649
+ }
650
+
651
+ export {
652
+ toPascalCase,
653
+ PG_TYPE_MAP,
654
+ pgTypeToTs,
655
+ parseCliArgs,
656
+ introspectSchema,
657
+ generateTypeScript,
658
+ runGenerateTypes,
659
+ connectionStringFromManifest,
660
+ loadEnvLocal,
661
+ main
662
+ };