@stardeck-customer-apps/data-store-sdk 0.3.3 → 0.5.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.
@@ -1 +1,50 @@
1
1
  #!/usr/bin/env node
2
+ declare function toPascalCase(name: string): string;
3
+
4
+ interface ColumnInfo {
5
+ table_name: string;
6
+ column_name: string;
7
+ data_type: string;
8
+ is_nullable: string;
9
+ column_default: string | null;
10
+ }
11
+ declare const PG_TYPE_MAP: Record<string, string>;
12
+ declare function pgTypeToTs(pgType: string): string;
13
+
14
+ type ParsedCliArgs = {
15
+ connectionString: string | undefined;
16
+ outputPath: string;
17
+ schemaOutputPath: string | undefined;
18
+ emitSchema: boolean;
19
+ modulesDir: string;
20
+ noModules: boolean;
21
+ help: boolean;
22
+ };
23
+ /**
24
+ * Pure argv parser for generate-types flags. Exported for unit tests.
25
+ */
26
+ declare function parseCliArgs(argv: string[], options?: {
27
+ cwd?: string;
28
+ env?: NodeJS.ProcessEnv;
29
+ }): ParsedCliArgs;
30
+ declare function introspectSchema(connectionString: string): Promise<Map<string, ColumnInfo[]>>;
31
+ declare function generateTypeScript(tables: Map<string, ColumnInfo[]>, moduleSlices?: string): string;
32
+ type RunGenerateTypesOptions = {
33
+ connectionString: string;
34
+ outputPath: string;
35
+ schemaOutputPath?: string;
36
+ emitSchema?: boolean;
37
+ modulesDir: string;
38
+ noModules: boolean;
39
+ /** Injectable for tests — defaults to live Postgres introspection. */
40
+ introspect?: (connectionString: string) => Promise<Map<string, ColumnInfo[]>>;
41
+ warn?: (message: string) => void;
42
+ };
43
+ /**
44
+ * Flag → discovery → emitter orchestration. Accepts an injectable introspect
45
+ * so tests can stub the pool without a live database.
46
+ */
47
+ declare function runGenerateTypes(options: RunGenerateTypesOptions): Promise<void>;
48
+ declare function main(argv?: string[]): Promise<void>;
49
+
50
+ export { type ColumnInfo, PG_TYPE_MAP, type ParsedCliArgs, type RunGenerateTypesOptions, generateTypeScript, introspectSchema, main, parseCliArgs, pgTypeToTs, runGenerateTypes, toPascalCase };
@@ -1 +1,50 @@
1
1
  #!/usr/bin/env node
2
+ declare function toPascalCase(name: string): string;
3
+
4
+ interface ColumnInfo {
5
+ table_name: string;
6
+ column_name: string;
7
+ data_type: string;
8
+ is_nullable: string;
9
+ column_default: string | null;
10
+ }
11
+ declare const PG_TYPE_MAP: Record<string, string>;
12
+ declare function pgTypeToTs(pgType: string): string;
13
+
14
+ type ParsedCliArgs = {
15
+ connectionString: string | undefined;
16
+ outputPath: string;
17
+ schemaOutputPath: string | undefined;
18
+ emitSchema: boolean;
19
+ modulesDir: string;
20
+ noModules: boolean;
21
+ help: boolean;
22
+ };
23
+ /**
24
+ * Pure argv parser for generate-types flags. Exported for unit tests.
25
+ */
26
+ declare function parseCliArgs(argv: string[], options?: {
27
+ cwd?: string;
28
+ env?: NodeJS.ProcessEnv;
29
+ }): ParsedCliArgs;
30
+ declare function introspectSchema(connectionString: string): Promise<Map<string, ColumnInfo[]>>;
31
+ declare function generateTypeScript(tables: Map<string, ColumnInfo[]>, moduleSlices?: string): string;
32
+ type RunGenerateTypesOptions = {
33
+ connectionString: string;
34
+ outputPath: string;
35
+ schemaOutputPath?: string;
36
+ emitSchema?: boolean;
37
+ modulesDir: string;
38
+ noModules: boolean;
39
+ /** Injectable for tests — defaults to live Postgres introspection. */
40
+ introspect?: (connectionString: string) => Promise<Map<string, ColumnInfo[]>>;
41
+ warn?: (message: string) => void;
42
+ };
43
+ /**
44
+ * Flag → discovery → emitter orchestration. Accepts an injectable introspect
45
+ * so tests can stub the pool without a live database.
46
+ */
47
+ declare function runGenerateTypes(options: RunGenerateTypesOptions): Promise<void>;
48
+ declare function main(argv?: string[]): Promise<void>;
49
+
50
+ export { type ColumnInfo, PG_TYPE_MAP, type ParsedCliArgs, type RunGenerateTypesOptions, generateTypeScript, introspectSchema, main, parseCliArgs, pgTypeToTs, runGenerateTypes, toPascalCase };
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,11 +26,132 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
30
 
26
31
  // src/cli/generate-types.ts
27
- var import_fs = require("fs");
32
+ var generate_types_exports = {};
33
+ __export(generate_types_exports, {
34
+ PG_TYPE_MAP: () => PG_TYPE_MAP,
35
+ generateTypeScript: () => generateTypeScript,
36
+ introspectSchema: () => introspectSchema,
37
+ main: () => main,
38
+ parseCliArgs: () => parseCliArgs,
39
+ pgTypeToTs: () => pgTypeToTs,
40
+ runGenerateTypes: () => runGenerateTypes,
41
+ toPascalCase: () => toPascalCase
42
+ });
43
+ module.exports = __toCommonJS(generate_types_exports);
44
+ var import_fs2 = require("fs");
45
+ var import_path2 = require("path");
46
+ var import_url = require("url");
28
47
  var import_serverless = require("@neondatabase/serverless");
29
48
 
49
+ // src/cli/module-type-slices.ts
50
+ var import_fs = require("fs");
51
+ var import_path = require("path");
52
+ function toPascalCase(name) {
53
+ return name.split(/[_-]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
54
+ }
55
+ function parseModuleManifestSlice(content, sourceLabel, warn) {
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(content);
59
+ } catch {
60
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
61
+ return null;
62
+ }
63
+ if (typeof parsed !== "object" || parsed === null) {
64
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
65
+ return null;
66
+ }
67
+ const record = parsed;
68
+ const name = record.name;
69
+ const tables = record.tables;
70
+ if (typeof name !== "string" || name.length === 0) {
71
+ warn(`Skipping module manifest at ${sourceLabel}: missing or invalid "name"`);
72
+ return null;
73
+ }
74
+ if (!Array.isArray(tables) || tables.length === 0) {
75
+ return null;
76
+ }
77
+ const tableNames = [];
78
+ for (const entry of tables) {
79
+ if (typeof entry !== "string" || entry.length === 0) {
80
+ warn(`Skipping module manifest at ${sourceLabel}: invalid "tables" entry`);
81
+ return null;
82
+ }
83
+ tableNames.push(entry);
84
+ }
85
+ return { name, tables: tableNames };
86
+ }
87
+ function discoverModuleManifests(modulesDir, warn) {
88
+ if (!(0, import_fs.existsSync)(modulesDir)) {
89
+ return [];
90
+ }
91
+ let entries;
92
+ try {
93
+ entries = (0, import_fs.readdirSync)(modulesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
94
+ } catch {
95
+ warn(`Could not read modules directory: ${modulesDir}`);
96
+ return [];
97
+ }
98
+ const manifests = [];
99
+ for (const dirName of entries) {
100
+ const manifestPath = (0, import_path.join)(modulesDir, dirName, "module.json");
101
+ if (!(0, import_fs.existsSync)(manifestPath)) {
102
+ continue;
103
+ }
104
+ let content;
105
+ try {
106
+ content = (0, import_fs.readFileSync)(manifestPath, "utf-8");
107
+ } catch {
108
+ warn(`Skipping unreadable module manifest at ${manifestPath}`);
109
+ continue;
110
+ }
111
+ const slice = parseModuleManifestSlice(content, manifestPath, warn);
112
+ if (slice) {
113
+ manifests.push(slice);
114
+ }
115
+ }
116
+ return manifests.sort((a, b) => a.name.localeCompare(b.name));
117
+ }
118
+ function shouldLoadModuleManifests(noModules) {
119
+ return !noModules;
120
+ }
121
+ function emitModuleDbSlices(schemaTableNames, manifests, warn) {
122
+ const lines = [];
123
+ const moduleMapEntries = [];
124
+ for (const manifest of manifests) {
125
+ const presentTables = manifest.tables.filter((t) => schemaTableNames.has(t));
126
+ const absentTables = manifest.tables.filter((t) => !schemaTableNames.has(t));
127
+ if (absentTables.length > 0) {
128
+ warn(`Module "${manifest.name}": table(s) not in database: ${absentTables.join(", ")}`);
129
+ }
130
+ if (presentTables.length === 0) {
131
+ continue;
132
+ }
133
+ const interfaceName = `${toPascalCase(manifest.name)}ModuleDB`;
134
+ lines.push(`export interface ${interfaceName} {`);
135
+ for (const tableName of presentTables) {
136
+ const tableTypeName = `${toPascalCase(tableName)}Table`;
137
+ lines.push(` ${tableName}: ${tableTypeName};`);
138
+ }
139
+ lines.push("}");
140
+ lines.push("");
141
+ moduleMapEntries.push({ name: manifest.name, interfaceName });
142
+ }
143
+ if (moduleMapEntries.length === 0) {
144
+ return "";
145
+ }
146
+ lines.push("export interface ModuleDBs {");
147
+ for (const { name, interfaceName } of moduleMapEntries) {
148
+ lines.push(` "${name}": ${interfaceName};`);
149
+ }
150
+ lines.push("}");
151
+ lines.push("");
152
+ return lines.join("\n");
153
+ }
154
+
30
155
  // src/cli/schema-ddl.ts
31
156
  function quoteIdent(name) {
32
157
  return `"${name.replace(/"/g, '""')}"`;
@@ -283,6 +408,7 @@ function generateSchemaSql(snapshot) {
283
408
  }
284
409
 
285
410
  // src/cli/generate-types.ts
411
+ var import_meta = {};
286
412
  var PG_TYPE_MAP = {
287
413
  text: "string",
288
414
  varchar: "string",
@@ -315,8 +441,42 @@ var PG_TYPE_MAP = {
315
441
  function pgTypeToTs(pgType) {
316
442
  return PG_TYPE_MAP[pgType] ?? "unknown";
317
443
  }
318
- function toPascalCase(name) {
319
- return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
444
+ function parseCliArgs(argv, options = {}) {
445
+ const cwd = options.cwd ?? process.cwd();
446
+ const env = options.env ?? process.env;
447
+ let connectionString = env.DATA_STORE_URL;
448
+ let outputPath = "./src/generated/data-store-types.ts";
449
+ let schemaOutputPath;
450
+ let emitSchema = true;
451
+ let modulesDir = (0, import_path2.resolve)(cwd, "./src/modules");
452
+ let noModules = false;
453
+ let help = false;
454
+ for (let i = 0; i < argv.length; i++) {
455
+ if (argv[i] === "--connection-string" && argv[i + 1]) {
456
+ connectionString = argv[++i];
457
+ } else if (argv[i] === "--output" && argv[i + 1]) {
458
+ outputPath = argv[++i];
459
+ } else if (argv[i] === "--schema-output" && argv[i + 1]) {
460
+ schemaOutputPath = argv[++i];
461
+ } else if (argv[i] === "--no-schema") {
462
+ emitSchema = false;
463
+ } else if (argv[i] === "--modules-dir" && argv[i + 1]) {
464
+ modulesDir = (0, import_path2.resolve)(cwd, argv[++i]);
465
+ } else if (argv[i] === "--no-modules") {
466
+ noModules = true;
467
+ } else if (argv[i] === "--help") {
468
+ help = true;
469
+ }
470
+ }
471
+ return {
472
+ connectionString,
473
+ outputPath,
474
+ schemaOutputPath,
475
+ emitSchema,
476
+ modulesDir,
477
+ noModules,
478
+ help
479
+ };
320
480
  }
321
481
  async function introspectSchema(connectionString) {
322
482
  const pool = new import_serverless.Pool({ connectionString });
@@ -348,7 +508,7 @@ async function introspectSchema(connectionString) {
348
508
  await pool.end();
349
509
  }
350
510
  }
351
- function generateTypeScript(tables) {
511
+ function generateTypeScript(tables, moduleSlices) {
352
512
  const lines = [
353
513
  "// AUTO-GENERATED by @stardeck-customer-apps/data-store-sdk \u2014 DO NOT EDIT.",
354
514
  "// This file is overwritten in full on every run of:",
@@ -356,6 +516,8 @@ function generateTypeScript(tables) {
356
516
  "// Anything you add here (hand-written or derived types) WILL BE LOST on the next",
357
517
  "// regeneration. Put those in a separate file that imports from this one, e.g.",
358
518
  "// src/lib/<domain>-types.ts importing the table types or DB from this file.",
519
+ "// When module manifests are discoverable, per-module DB slices and ModuleDBs are",
520
+ "// appended below the flat DB interface.",
359
521
  "",
360
522
  'import type { Generated } from "kysely";',
361
523
  ""
@@ -386,69 +548,113 @@ function generateTypeScript(tables) {
386
548
  }
387
549
  lines.push("}");
388
550
  lines.push("");
389
- return lines.join("\n");
390
- }
391
- async function main() {
392
- const args = process.argv.slice(2);
393
- let connectionString = process.env.DATA_STORE_URL;
394
- let outputPath = "./src/generated/data-store-types.ts";
395
- let schemaOutputPath;
396
- let emitSchema = true;
397
- for (let i = 0; i < args.length; i++) {
398
- if (args[i] === "--connection-string" && args[i + 1]) {
399
- connectionString = args[++i];
400
- } else if (args[i] === "--output" && args[i + 1]) {
401
- outputPath = args[++i];
402
- } else if (args[i] === "--schema-output" && args[i + 1]) {
403
- schemaOutputPath = args[++i];
404
- } else if (args[i] === "--no-schema") {
405
- emitSchema = false;
406
- } else if (args[i] === "--help") {
407
- console.log(`Usage: stardeck-data-store generate-types [options]
408
-
409
- Options:
410
- --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
411
- --output <path> Output file path (default: ./src/generated/data-store-types.ts)
412
- --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
413
- --no-schema Skip the DDL snapshot used by the test harness
414
- --help Show this help message`);
415
- process.exit(0);
551
+ if (moduleSlices) {
552
+ const trimmed = moduleSlices.trimEnd();
553
+ if (trimmed.length > 0) {
554
+ lines.push(trimmed);
555
+ lines.push("");
416
556
  }
417
557
  }
418
- if (!connectionString) {
419
- console.error(
420
- "Error: No connection string provided. Set DATA_STORE_URL or use --connection-string."
421
- );
422
- process.exit(1);
423
- }
558
+ return lines.join("\n");
559
+ }
560
+ async function runGenerateTypes(options) {
561
+ const {
562
+ connectionString,
563
+ outputPath,
564
+ schemaOutputPath,
565
+ emitSchema = true,
566
+ modulesDir,
567
+ noModules,
568
+ introspect = introspectSchema,
569
+ warn = (message) => console.warn(message)
570
+ } = options;
424
571
  console.log("Introspecting database schema...");
425
- const tables = await introspectSchema(connectionString);
572
+ const tables = await introspect(connectionString);
426
573
  if (tables.size === 0) {
427
574
  console.log("No tables found in database.");
428
575
  return;
429
576
  }
430
577
  console.log(`Found ${tables.size} table(s): ${Array.from(tables.keys()).join(", ")}`);
431
- const typeScript = generateTypeScript(tables);
578
+ let moduleSlices = "";
579
+ if (shouldLoadModuleManifests(noModules)) {
580
+ const manifests = discoverModuleManifests(modulesDir, warn);
581
+ if (manifests.length > 0) {
582
+ moduleSlices = emitModuleDbSlices(new Set(tables.keys()), manifests, warn);
583
+ }
584
+ }
585
+ const typeScript = generateTypeScript(tables, moduleSlices || void 0);
432
586
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
433
587
  if (dir) {
434
588
  const { mkdirSync } = await import("fs");
435
589
  mkdirSync(dir, { recursive: true });
436
590
  }
437
- (0, import_fs.writeFileSync)(outputPath, typeScript, "utf-8");
591
+ (0, import_fs2.writeFileSync)(outputPath, typeScript, "utf-8");
438
592
  console.log(`Types written to ${outputPath}`);
439
593
  if (emitSchema) {
440
594
  const schemaPath = schemaOutputPath ?? `${dir ? `${dir}/` : ""}data-store-schema.sql`;
441
595
  const pool = new import_serverless.Pool({ connectionString });
442
596
  try {
443
597
  const snapshot = await introspectSchemaSnapshot(pool);
444
- (0, import_fs.writeFileSync)(schemaPath, generateSchemaSql(snapshot), "utf-8");
598
+ (0, import_fs2.writeFileSync)(schemaPath, generateSchemaSql(snapshot), "utf-8");
445
599
  console.log(`Schema snapshot written to ${schemaPath} (used by the test harness)`);
446
600
  } finally {
447
601
  await pool.end();
448
602
  }
449
603
  }
450
604
  }
451
- main().catch((error) => {
452
- console.error("Failed to generate types:", error);
453
- process.exit(1);
605
+ async function main(argv = process.argv.slice(2)) {
606
+ const parsed = parseCliArgs(argv);
607
+ if (parsed.help) {
608
+ console.log(`Usage: stardeck-data-store generate-types [options]
609
+
610
+ Options:
611
+ --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
612
+ --output <path> Output file path (default: ./src/generated/data-store-types.ts)
613
+ --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
614
+ --no-schema Skip the DDL snapshot used by the test harness
615
+ --modules-dir <path> Directory containing module folders (default: ./src/modules)
616
+ --no-modules Skip per-module DB slice generation
617
+ --help Show this help message`);
618
+ process.exit(0);
619
+ }
620
+ if (!parsed.connectionString) {
621
+ console.error(
622
+ "Error: No connection string provided. Set DATA_STORE_URL or use --connection-string."
623
+ );
624
+ process.exit(1);
625
+ }
626
+ await runGenerateTypes({
627
+ connectionString: parsed.connectionString,
628
+ outputPath: parsed.outputPath,
629
+ schemaOutputPath: parsed.schemaOutputPath,
630
+ emitSchema: parsed.emitSchema,
631
+ modulesDir: parsed.modulesDir,
632
+ noModules: parsed.noModules
633
+ });
634
+ }
635
+ function isDirectCliRun() {
636
+ const entry = process.argv[1];
637
+ if (!entry) return false;
638
+ try {
639
+ return import_meta.url === (0, import_url.pathToFileURL)((0, import_path2.resolve)(entry)).href;
640
+ } catch {
641
+ return false;
642
+ }
643
+ }
644
+ if (isDirectCliRun()) {
645
+ main().catch((error) => {
646
+ console.error("Failed to generate types:", error);
647
+ process.exit(1);
648
+ });
649
+ }
650
+ // Annotate the CommonJS export names for ESM import in node:
651
+ 0 && (module.exports = {
652
+ PG_TYPE_MAP,
653
+ generateTypeScript,
654
+ introspectSchema,
655
+ main,
656
+ parseCliArgs,
657
+ pgTypeToTs,
658
+ runGenerateTypes,
659
+ toPascalCase
454
660
  });
@@ -2,8 +2,116 @@
2
2
 
3
3
  // src/cli/generate-types.ts
4
4
  import { writeFileSync } from "fs";
5
+ import { resolve } from "path";
6
+ import { pathToFileURL } from "url";
5
7
  import { Pool } from "@neondatabase/serverless";
6
8
 
9
+ // src/cli/module-type-slices.ts
10
+ import { existsSync, readdirSync, readFileSync } from "fs";
11
+ import { join } from "path";
12
+ function toPascalCase(name) {
13
+ return name.split(/[_-]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
14
+ }
15
+ function parseModuleManifestSlice(content, sourceLabel, warn) {
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(content);
19
+ } catch {
20
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
21
+ return null;
22
+ }
23
+ if (typeof parsed !== "object" || parsed === null) {
24
+ warn(`Skipping malformed module manifest at ${sourceLabel}`);
25
+ return null;
26
+ }
27
+ const record = parsed;
28
+ const name = record.name;
29
+ const tables = record.tables;
30
+ if (typeof name !== "string" || name.length === 0) {
31
+ warn(`Skipping module manifest at ${sourceLabel}: missing or invalid "name"`);
32
+ return null;
33
+ }
34
+ if (!Array.isArray(tables) || tables.length === 0) {
35
+ return null;
36
+ }
37
+ const tableNames = [];
38
+ for (const entry of tables) {
39
+ if (typeof entry !== "string" || entry.length === 0) {
40
+ warn(`Skipping module manifest at ${sourceLabel}: invalid "tables" entry`);
41
+ return null;
42
+ }
43
+ tableNames.push(entry);
44
+ }
45
+ return { name, tables: tableNames };
46
+ }
47
+ function discoverModuleManifests(modulesDir, warn) {
48
+ if (!existsSync(modulesDir)) {
49
+ return [];
50
+ }
51
+ let entries;
52
+ try {
53
+ entries = readdirSync(modulesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
54
+ } catch {
55
+ warn(`Could not read modules directory: ${modulesDir}`);
56
+ return [];
57
+ }
58
+ const manifests = [];
59
+ for (const dirName of entries) {
60
+ const manifestPath = join(modulesDir, dirName, "module.json");
61
+ if (!existsSync(manifestPath)) {
62
+ continue;
63
+ }
64
+ let content;
65
+ try {
66
+ content = readFileSync(manifestPath, "utf-8");
67
+ } catch {
68
+ warn(`Skipping unreadable module manifest at ${manifestPath}`);
69
+ continue;
70
+ }
71
+ const slice = parseModuleManifestSlice(content, manifestPath, warn);
72
+ if (slice) {
73
+ manifests.push(slice);
74
+ }
75
+ }
76
+ return manifests.sort((a, b) => a.name.localeCompare(b.name));
77
+ }
78
+ function shouldLoadModuleManifests(noModules) {
79
+ return !noModules;
80
+ }
81
+ function emitModuleDbSlices(schemaTableNames, manifests, warn) {
82
+ const lines = [];
83
+ const moduleMapEntries = [];
84
+ for (const manifest of manifests) {
85
+ const presentTables = manifest.tables.filter((t) => schemaTableNames.has(t));
86
+ const absentTables = manifest.tables.filter((t) => !schemaTableNames.has(t));
87
+ if (absentTables.length > 0) {
88
+ warn(`Module "${manifest.name}": table(s) not in database: ${absentTables.join(", ")}`);
89
+ }
90
+ if (presentTables.length === 0) {
91
+ continue;
92
+ }
93
+ const interfaceName = `${toPascalCase(manifest.name)}ModuleDB`;
94
+ lines.push(`export interface ${interfaceName} {`);
95
+ for (const tableName of presentTables) {
96
+ const tableTypeName = `${toPascalCase(tableName)}Table`;
97
+ lines.push(` ${tableName}: ${tableTypeName};`);
98
+ }
99
+ lines.push("}");
100
+ lines.push("");
101
+ moduleMapEntries.push({ name: manifest.name, interfaceName });
102
+ }
103
+ if (moduleMapEntries.length === 0) {
104
+ return "";
105
+ }
106
+ lines.push("export interface ModuleDBs {");
107
+ for (const { name, interfaceName } of moduleMapEntries) {
108
+ lines.push(` "${name}": ${interfaceName};`);
109
+ }
110
+ lines.push("}");
111
+ lines.push("");
112
+ return lines.join("\n");
113
+ }
114
+
7
115
  // src/cli/schema-ddl.ts
8
116
  function quoteIdent(name) {
9
117
  return `"${name.replace(/"/g, '""')}"`;
@@ -292,8 +400,42 @@ var PG_TYPE_MAP = {
292
400
  function pgTypeToTs(pgType) {
293
401
  return PG_TYPE_MAP[pgType] ?? "unknown";
294
402
  }
295
- function toPascalCase(name) {
296
- return name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
403
+ function parseCliArgs(argv, options = {}) {
404
+ const cwd = options.cwd ?? process.cwd();
405
+ const env = options.env ?? process.env;
406
+ let connectionString = env.DATA_STORE_URL;
407
+ let outputPath = "./src/generated/data-store-types.ts";
408
+ let schemaOutputPath;
409
+ let emitSchema = true;
410
+ let modulesDir = resolve(cwd, "./src/modules");
411
+ let noModules = false;
412
+ let help = false;
413
+ for (let i = 0; i < argv.length; i++) {
414
+ if (argv[i] === "--connection-string" && argv[i + 1]) {
415
+ connectionString = argv[++i];
416
+ } else if (argv[i] === "--output" && argv[i + 1]) {
417
+ outputPath = argv[++i];
418
+ } else if (argv[i] === "--schema-output" && argv[i + 1]) {
419
+ schemaOutputPath = argv[++i];
420
+ } else if (argv[i] === "--no-schema") {
421
+ emitSchema = false;
422
+ } else if (argv[i] === "--modules-dir" && argv[i + 1]) {
423
+ modulesDir = resolve(cwd, argv[++i]);
424
+ } else if (argv[i] === "--no-modules") {
425
+ noModules = true;
426
+ } else if (argv[i] === "--help") {
427
+ help = true;
428
+ }
429
+ }
430
+ return {
431
+ connectionString,
432
+ outputPath,
433
+ schemaOutputPath,
434
+ emitSchema,
435
+ modulesDir,
436
+ noModules,
437
+ help
438
+ };
297
439
  }
298
440
  async function introspectSchema(connectionString) {
299
441
  const pool = new Pool({ connectionString });
@@ -325,7 +467,7 @@ async function introspectSchema(connectionString) {
325
467
  await pool.end();
326
468
  }
327
469
  }
328
- function generateTypeScript(tables) {
470
+ function generateTypeScript(tables, moduleSlices) {
329
471
  const lines = [
330
472
  "// AUTO-GENERATED by @stardeck-customer-apps/data-store-sdk \u2014 DO NOT EDIT.",
331
473
  "// This file is overwritten in full on every run of:",
@@ -333,6 +475,8 @@ function generateTypeScript(tables) {
333
475
  "// Anything you add here (hand-written or derived types) WILL BE LOST on the next",
334
476
  "// regeneration. Put those in a separate file that imports from this one, e.g.",
335
477
  "// src/lib/<domain>-types.ts importing the table types or DB from this file.",
478
+ "// When module manifests are discoverable, per-module DB slices and ModuleDBs are",
479
+ "// appended below the flat DB interface.",
336
480
  "",
337
481
  'import type { Generated } from "kysely";',
338
482
  ""
@@ -363,49 +507,41 @@ function generateTypeScript(tables) {
363
507
  }
364
508
  lines.push("}");
365
509
  lines.push("");
366
- return lines.join("\n");
367
- }
368
- async function main() {
369
- const args = process.argv.slice(2);
370
- let connectionString = process.env.DATA_STORE_URL;
371
- let outputPath = "./src/generated/data-store-types.ts";
372
- let schemaOutputPath;
373
- let emitSchema = true;
374
- for (let i = 0; i < args.length; i++) {
375
- if (args[i] === "--connection-string" && args[i + 1]) {
376
- connectionString = args[++i];
377
- } else if (args[i] === "--output" && args[i + 1]) {
378
- outputPath = args[++i];
379
- } else if (args[i] === "--schema-output" && args[i + 1]) {
380
- schemaOutputPath = args[++i];
381
- } else if (args[i] === "--no-schema") {
382
- emitSchema = false;
383
- } else if (args[i] === "--help") {
384
- console.log(`Usage: stardeck-data-store generate-types [options]
385
-
386
- Options:
387
- --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
388
- --output <path> Output file path (default: ./src/generated/data-store-types.ts)
389
- --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
390
- --no-schema Skip the DDL snapshot used by the test harness
391
- --help Show this help message`);
392
- process.exit(0);
510
+ if (moduleSlices) {
511
+ const trimmed = moduleSlices.trimEnd();
512
+ if (trimmed.length > 0) {
513
+ lines.push(trimmed);
514
+ lines.push("");
393
515
  }
394
516
  }
395
- if (!connectionString) {
396
- console.error(
397
- "Error: No connection string provided. Set DATA_STORE_URL or use --connection-string."
398
- );
399
- process.exit(1);
400
- }
517
+ return lines.join("\n");
518
+ }
519
+ async function runGenerateTypes(options) {
520
+ const {
521
+ connectionString,
522
+ outputPath,
523
+ schemaOutputPath,
524
+ emitSchema = true,
525
+ modulesDir,
526
+ noModules,
527
+ introspect = introspectSchema,
528
+ warn = (message) => console.warn(message)
529
+ } = options;
401
530
  console.log("Introspecting database schema...");
402
- const tables = await introspectSchema(connectionString);
531
+ const tables = await introspect(connectionString);
403
532
  if (tables.size === 0) {
404
533
  console.log("No tables found in database.");
405
534
  return;
406
535
  }
407
536
  console.log(`Found ${tables.size} table(s): ${Array.from(tables.keys()).join(", ")}`);
408
- const typeScript = generateTypeScript(tables);
537
+ let moduleSlices = "";
538
+ if (shouldLoadModuleManifests(noModules)) {
539
+ const manifests = discoverModuleManifests(modulesDir, warn);
540
+ if (manifests.length > 0) {
541
+ moduleSlices = emitModuleDbSlices(new Set(tables.keys()), manifests, warn);
542
+ }
543
+ }
544
+ const typeScript = generateTypeScript(tables, moduleSlices || void 0);
409
545
  const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
410
546
  if (dir) {
411
547
  const { mkdirSync } = await import("fs");
@@ -425,7 +561,58 @@ Options:
425
561
  }
426
562
  }
427
563
  }
428
- main().catch((error) => {
429
- console.error("Failed to generate types:", error);
430
- process.exit(1);
431
- });
564
+ async function main(argv = process.argv.slice(2)) {
565
+ const parsed = parseCliArgs(argv);
566
+ if (parsed.help) {
567
+ console.log(`Usage: stardeck-data-store generate-types [options]
568
+
569
+ Options:
570
+ --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
571
+ --output <path> Output file path (default: ./src/generated/data-store-types.ts)
572
+ --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
573
+ --no-schema Skip the DDL snapshot used by the test harness
574
+ --modules-dir <path> Directory containing module folders (default: ./src/modules)
575
+ --no-modules Skip per-module DB slice generation
576
+ --help Show this help message`);
577
+ process.exit(0);
578
+ }
579
+ if (!parsed.connectionString) {
580
+ console.error(
581
+ "Error: No connection string provided. Set DATA_STORE_URL or use --connection-string."
582
+ );
583
+ process.exit(1);
584
+ }
585
+ await runGenerateTypes({
586
+ connectionString: parsed.connectionString,
587
+ outputPath: parsed.outputPath,
588
+ schemaOutputPath: parsed.schemaOutputPath,
589
+ emitSchema: parsed.emitSchema,
590
+ modulesDir: parsed.modulesDir,
591
+ noModules: parsed.noModules
592
+ });
593
+ }
594
+ function isDirectCliRun() {
595
+ const entry = process.argv[1];
596
+ if (!entry) return false;
597
+ try {
598
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
599
+ } catch {
600
+ return false;
601
+ }
602
+ }
603
+ if (isDirectCliRun()) {
604
+ main().catch((error) => {
605
+ console.error("Failed to generate types:", error);
606
+ process.exit(1);
607
+ });
608
+ }
609
+ export {
610
+ PG_TYPE_MAP,
611
+ generateTypeScript,
612
+ introspectSchema,
613
+ main,
614
+ parseCliArgs,
615
+ pgTypeToTs,
616
+ runGenerateTypes,
617
+ toPascalCase
618
+ };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColumnDefinition, D as DataStoreClientConfig, F as FilterOperator, L as ListObjectsResult, Q as QueryFilter, b as QueryOptions, c as QueryResult, S as StorageObject, T as TableColumn, a as TableSchema } from './types-D2D9kZNF.mjs';
1
+ export { C as ColumnDefinition, D as DataStoreClientConfig, F as FilterOperator, L as ListObjectsResult, Q as QueryFilter, a as QueryOptions, b as QueryResult, S as StorageObject, T as TableColumn, c as TableSchema } from './types-P3VlojJ9.mjs';
2
2
 
3
3
  declare class DataStoreError extends Error {
4
4
  code: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as ColumnDefinition, D as DataStoreClientConfig, F as FilterOperator, L as ListObjectsResult, Q as QueryFilter, b as QueryOptions, c as QueryResult, S as StorageObject, T as TableColumn, a as TableSchema } from './types-D2D9kZNF.js';
1
+ export { C as ColumnDefinition, D as DataStoreClientConfig, F as FilterOperator, L as ListObjectsResult, Q as QueryFilter, a as QueryOptions, b as QueryResult, S as StorageObject, T as TableColumn, c as TableSchema } from './types-P3VlojJ9.js';
2
2
 
3
3
  declare class DataStoreError extends Error {
4
4
  code: string;
@@ -1,4 +1,4 @@
1
- import { D as DataStoreClientConfig, a as TableSchema, C as ColumnDefinition, b as QueryOptions, c as QueryResult, L as ListObjectsResult } from '../types-D2D9kZNF.mjs';
1
+ import { D as DataStoreClientConfig, c as TableSchema, C as ColumnDefinition, a as QueryOptions, b as QueryResult, L as ListObjectsResult } from '../types-P3VlojJ9.mjs';
2
2
  import { KyselyConfig, Kysely } from 'kysely';
3
3
 
4
4
  /**
@@ -140,7 +140,19 @@ interface DataStoreManifestEntry {
140
140
  id: string;
141
141
  name: string;
142
142
  slug: string;
143
- url: string;
143
+ /**
144
+ * App-local handle for this project's connection. When present, the resolver
145
+ * matches it before the org-global slug/name.
146
+ */
147
+ bindingKey?: string;
148
+ /**
149
+ * Store kind. `database` stores carry a `url`; `storage` stores do not (access
150
+ * is API-mediated by store id). Absent on manifests emitted before this field
151
+ * existed — treat absent as `database`.
152
+ */
153
+ storeType?: "database" | "storage";
154
+ /** Connection string. Present for `database` stores only. */
155
+ url?: string;
144
156
  accessLevel: "read" | "write" | "admin";
145
157
  }
146
158
  /**
@@ -1,4 +1,4 @@
1
- import { D as DataStoreClientConfig, a as TableSchema, C as ColumnDefinition, b as QueryOptions, c as QueryResult, L as ListObjectsResult } from '../types-D2D9kZNF.js';
1
+ import { D as DataStoreClientConfig, c as TableSchema, C as ColumnDefinition, a as QueryOptions, b as QueryResult, L as ListObjectsResult } from '../types-P3VlojJ9.js';
2
2
  import { KyselyConfig, Kysely } from 'kysely';
3
3
 
4
4
  /**
@@ -140,7 +140,19 @@ interface DataStoreManifestEntry {
140
140
  id: string;
141
141
  name: string;
142
142
  slug: string;
143
- url: string;
143
+ /**
144
+ * App-local handle for this project's connection. When present, the resolver
145
+ * matches it before the org-global slug/name.
146
+ */
147
+ bindingKey?: string;
148
+ /**
149
+ * Store kind. `database` stores carry a `url`; `storage` stores do not (access
150
+ * is API-mediated by store id). Absent on manifests emitted before this field
151
+ * existed — treat absent as `database`.
152
+ */
153
+ storeType?: "database" | "storage";
154
+ /** Connection string. Present for `database` stores only. */
155
+ url?: string;
144
156
  accessLevel: "read" | "write" | "admin";
145
157
  }
146
158
  /**
@@ -94,9 +94,14 @@ function readDataStoreManifest() {
94
94
  try {
95
95
  const parsed = JSON.parse(raw);
96
96
  if (!Array.isArray(parsed)) return [];
97
- return parsed.filter(
98
- (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.name === "string" && typeof e.slug === "string" && typeof e.url === "string"
99
- );
97
+ return parsed.filter((e) => {
98
+ if (typeof e !== "object" || e === null) return false;
99
+ const entry = e;
100
+ if (typeof entry.id !== "string" || typeof entry.name !== "string" || typeof entry.slug !== "string") {
101
+ return false;
102
+ }
103
+ return entry.storeType === "storage" || typeof entry.url === "string";
104
+ });
100
105
  } catch {
101
106
  return [];
102
107
  }
@@ -111,9 +116,13 @@ function resolveManifestEntry(entries, ref) {
111
116
  }
112
117
  if (ref.storeName) {
113
118
  const target = ref.storeName;
119
+ const byBindingKey = entries.find((e) => e.bindingKey === target);
120
+ if (byBindingKey) return byBindingKey;
114
121
  const exact = entries.find((e) => e.slug === target || e.id === target || e.name === target);
115
122
  if (exact) return exact;
116
123
  const norm = dataStoreSlug(target);
124
+ const byBindingKeyNorm = entries.find((e) => e.bindingKey === norm);
125
+ if (byBindingKeyNorm) return byBindingKeyNorm;
117
126
  return entries.find(
118
127
  (e) => e.slug === norm || typeof e.name === "string" && dataStoreSlug(e.name) === norm
119
128
  );
@@ -324,6 +333,12 @@ async function createDataStore(options) {
324
333
  if (!connectionString) {
325
334
  const name = options?.storeName ?? options?.storeId;
326
335
  const ref = name ? `data store "${name}"` : "a data store";
336
+ const resolved = options?.storeId || options?.storeName ? resolveDataStore({ storeId: options?.storeId, storeName: options?.storeName }) : void 0;
337
+ if (resolved?.storeType === "storage") {
338
+ throw new Error(
339
+ `${ref} is a storage store and has no SQL connection. Use DataStoreClient for storage operations (upload/download/list) instead of createDataStore.`
340
+ );
341
+ }
327
342
  throw new Error(
328
343
  `Could not resolve a connection string for ${ref}. Checked the STARDECK_DATA_STORES manifest, the legacy DATA_STORE_*_URL env var, and DATA_STORE_URL. Confirm the store is connected to this project, or pass connectionString in options.`
329
344
  );
@@ -41,9 +41,14 @@ function readDataStoreManifest() {
41
41
  try {
42
42
  const parsed = JSON.parse(raw);
43
43
  if (!Array.isArray(parsed)) return [];
44
- return parsed.filter(
45
- (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.name === "string" && typeof e.slug === "string" && typeof e.url === "string"
46
- );
44
+ return parsed.filter((e) => {
45
+ if (typeof e !== "object" || e === null) return false;
46
+ const entry = e;
47
+ if (typeof entry.id !== "string" || typeof entry.name !== "string" || typeof entry.slug !== "string") {
48
+ return false;
49
+ }
50
+ return entry.storeType === "storage" || typeof entry.url === "string";
51
+ });
47
52
  } catch {
48
53
  return [];
49
54
  }
@@ -58,9 +63,13 @@ function resolveManifestEntry(entries, ref) {
58
63
  }
59
64
  if (ref.storeName) {
60
65
  const target = ref.storeName;
66
+ const byBindingKey = entries.find((e) => e.bindingKey === target);
67
+ if (byBindingKey) return byBindingKey;
61
68
  const exact = entries.find((e) => e.slug === target || e.id === target || e.name === target);
62
69
  if (exact) return exact;
63
70
  const norm = dataStoreSlug(target);
71
+ const byBindingKeyNorm = entries.find((e) => e.bindingKey === norm);
72
+ if (byBindingKeyNorm) return byBindingKeyNorm;
64
73
  return entries.find(
65
74
  (e) => e.slug === norm || typeof e.name === "string" && dataStoreSlug(e.name) === norm
66
75
  );
@@ -271,6 +280,12 @@ async function createDataStore(options) {
271
280
  if (!connectionString) {
272
281
  const name = options?.storeName ?? options?.storeId;
273
282
  const ref = name ? `data store "${name}"` : "a data store";
283
+ const resolved = options?.storeId || options?.storeName ? resolveDataStore({ storeId: options?.storeId, storeName: options?.storeName }) : void 0;
284
+ if (resolved?.storeType === "storage") {
285
+ throw new Error(
286
+ `${ref} is a storage store and has no SQL connection. Use DataStoreClient for storage operations (upload/download/list) instead of createDataStore.`
287
+ );
288
+ }
274
289
  throw new Error(
275
290
  `Could not resolve a connection string for ${ref}. Checked the STARDECK_DATA_STORES manifest, the legacy DATA_STORE_*_URL env var, and DATA_STORE_URL. Confirm the store is connected to this project, or pass connectionString in options.`
276
291
  );
@@ -82,4 +82,4 @@ interface ListObjectsResult {
82
82
  hasMore: boolean;
83
83
  }
84
84
 
85
- export type { ColumnDefinition as C, DataStoreClientConfig as D, FilterOperator as F, ListObjectsResult as L, QueryFilter as Q, StorageObject as S, TableColumn as T, TableSchema as a, QueryOptions as b, QueryResult as c };
85
+ export type { ColumnDefinition as C, DataStoreClientConfig as D, FilterOperator as F, ListObjectsResult as L, QueryFilter as Q, StorageObject as S, TableColumn as T, QueryOptions as a, QueryResult as b, TableSchema as c };
@@ -82,4 +82,4 @@ interface ListObjectsResult {
82
82
  hasMore: boolean;
83
83
  }
84
84
 
85
- export type { ColumnDefinition as C, DataStoreClientConfig as D, FilterOperator as F, ListObjectsResult as L, QueryFilter as Q, StorageObject as S, TableColumn as T, TableSchema as a, QueryOptions as b, QueryResult as c };
85
+ export type { ColumnDefinition as C, DataStoreClientConfig as D, FilterOperator as F, ListObjectsResult as L, QueryFilter as Q, StorageObject as S, TableColumn as T, QueryOptions as a, QueryResult as b, TableSchema as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/data-store-sdk",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "SDK for accessing Stardeck data stores from deployed projects",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",