@stardeck-customer-apps/data-store-sdk 0.1.0 → 0.2.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/SKILL.md CHANGED
@@ -208,8 +208,14 @@ Use this for application code where you want compile-time type safety.
208
208
  ```bash
209
209
  npx stardeck-data-store generate-types --connection-string $DATA_STORE_MYSTORE_URL
210
210
  # Outputs: ./src/generated/data-store-types.ts
211
+ # ./src/generated/data-store-schema.sql (DDL snapshot for the test harness)
211
212
  ```
212
213
 
214
+ Commit both generated files. Re-run this command after every schema change —
215
+ the schema snapshot is what `@stardeck-customer-apps/testing` applies to its
216
+ in-process Postgres, so a stale snapshot means tests run against the wrong
217
+ schema.
218
+
213
219
  ### Initialize
214
220
 
215
221
  ```typescript
@@ -26,6 +26,252 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli/generate-types.ts
27
27
  var import_fs = require("fs");
28
28
  var import_serverless = require("@neondatabase/serverless");
29
+
30
+ // src/cli/schema-ddl.ts
31
+ function quoteIdent(name) {
32
+ return `"${name.replace(/"/g, '""')}"`;
33
+ }
34
+ var TRACKED_EXTENSIONS = /* @__PURE__ */ new Set(["pgcrypto", "pg_trgm", "uuid-ossp", "citext"]);
35
+ function resolveSqlType(row) {
36
+ const dataType = row.data_type;
37
+ if (dataType === "ARRAY") {
38
+ return `${row.udt_name.replace(/^_/, "")}[]`;
39
+ }
40
+ if (dataType === "USER-DEFINED") {
41
+ return quoteIdent(row.udt_name);
42
+ }
43
+ if (dataType === "character varying") {
44
+ return row.character_maximum_length ? `varchar(${row.character_maximum_length})` : "varchar";
45
+ }
46
+ if (dataType === "character") {
47
+ return row.character_maximum_length ? `char(${row.character_maximum_length})` : "char";
48
+ }
49
+ if (dataType === "numeric") {
50
+ return row.numeric_precision !== null && row.numeric_scale !== null ? `numeric(${row.numeric_precision},${row.numeric_scale})` : "numeric";
51
+ }
52
+ if (dataType === "timestamp with time zone") return "timestamptz";
53
+ if (dataType === "timestamp without time zone") return "timestamp";
54
+ if (dataType === "time with time zone") return "timetz";
55
+ if (dataType === "time without time zone") return "time";
56
+ return dataType;
57
+ }
58
+ async function introspectSchemaSnapshot(executor) {
59
+ const [extensions, enums, columns, constraints, fks, checks, indexes] = await Promise.all([
60
+ executor.query(`SELECT extname FROM pg_extension ORDER BY extname`),
61
+ executor.query(
62
+ `SELECT t.typname AS name, e.enumlabel AS value
63
+ FROM pg_type t
64
+ JOIN pg_enum e ON e.enumtypid = t.oid
65
+ JOIN pg_namespace n ON n.oid = t.typnamespace
66
+ WHERE n.nspname = 'public'
67
+ ORDER BY t.typname, e.enumsortorder`
68
+ ),
69
+ executor.query(
70
+ `SELECT c.table_name, c.column_name, c.data_type, c.udt_name, c.is_nullable,
71
+ c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale,
72
+ c.is_generated, c.generation_expression
73
+ FROM information_schema.columns c
74
+ JOIN information_schema.tables t
75
+ ON c.table_name = t.table_name AND c.table_schema = t.table_schema
76
+ WHERE c.table_schema = 'public'
77
+ AND t.table_type = 'BASE TABLE'
78
+ AND t.table_name NOT LIKE '\\_deleted\\_%'
79
+ ORDER BY c.table_name, c.ordinal_position`
80
+ ),
81
+ executor.query(
82
+ `SELECT tc.table_name, tc.constraint_name, tc.constraint_type, ku.column_name
83
+ FROM information_schema.table_constraints tc
84
+ JOIN information_schema.key_column_usage ku
85
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
86
+ WHERE tc.table_schema = 'public'
87
+ AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE')
88
+ ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
89
+ ),
90
+ executor.query(
91
+ `SELECT tc.constraint_name, tc.table_name, ku.column_name,
92
+ ccu.table_name AS referenced_table, ccu.column_name AS referenced_column,
93
+ rc.delete_rule, rc.update_rule
94
+ FROM information_schema.table_constraints tc
95
+ JOIN information_schema.key_column_usage ku
96
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
97
+ JOIN information_schema.constraint_column_usage ccu
98
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
99
+ JOIN information_schema.referential_constraints rc
100
+ ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema
101
+ WHERE tc.table_schema = 'public' AND tc.constraint_type = 'FOREIGN KEY'
102
+ ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
103
+ ),
104
+ executor.query(
105
+ `SELECT rel.relname AS table_name, con.conname AS name,
106
+ pg_get_constraintdef(con.oid) AS definition
107
+ FROM pg_constraint con
108
+ JOIN pg_class rel ON rel.oid = con.conrelid
109
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
110
+ WHERE ns.nspname = 'public' AND con.contype = 'c'
111
+ AND rel.relname NOT LIKE '\\_deleted\\_%'
112
+ ORDER BY rel.relname, con.conname`
113
+ ),
114
+ executor.query(
115
+ `SELECT i.indexdef
116
+ FROM pg_indexes i
117
+ WHERE i.schemaname = 'public'
118
+ AND i.tablename NOT LIKE '\\_deleted\\_%'
119
+ AND NOT EXISTS (
120
+ SELECT 1 FROM pg_constraint c
121
+ JOIN pg_class cls ON cls.oid = c.conindid
122
+ WHERE cls.relname = i.indexname
123
+ )
124
+ ORDER BY i.indexname`
125
+ )
126
+ ]);
127
+ const enumMap = /* @__PURE__ */ new Map();
128
+ for (const row of enums.rows) {
129
+ const name = String(row.name);
130
+ if (!enumMap.has(name)) enumMap.set(name, []);
131
+ enumMap.get(name).push(String(row.value));
132
+ }
133
+ const tableMap = /* @__PURE__ */ new Map();
134
+ for (const row of columns.rows) {
135
+ const tableName = String(row.table_name);
136
+ if (!tableMap.has(tableName)) {
137
+ tableMap.set(tableName, { name: tableName, columns: [], primaryKey: [], uniques: [] });
138
+ }
139
+ const defaultExpr = row.column_default === null ? null : String(row.column_default);
140
+ const isSerial = defaultExpr !== null && /^nextval\(/.test(defaultExpr);
141
+ const isGenerated = row.is_generated === "ALWAYS";
142
+ tableMap.get(tableName).columns.push({
143
+ name: String(row.column_name),
144
+ sqlType: resolveSqlType({
145
+ data_type: String(row.data_type),
146
+ udt_name: String(row.udt_name),
147
+ character_maximum_length: row.character_maximum_length,
148
+ numeric_precision: row.numeric_precision,
149
+ numeric_scale: row.numeric_scale
150
+ }),
151
+ nullable: row.is_nullable === "YES",
152
+ defaultExpr: isGenerated ? null : defaultExpr,
153
+ isSerial: !isGenerated && isSerial,
154
+ generatedExpr: isGenerated ? String(row.generation_expression) : null
155
+ });
156
+ }
157
+ const constraintGroups = /* @__PURE__ */ new Map();
158
+ for (const row of constraints.rows) {
159
+ const key = String(row.constraint_name);
160
+ if (!constraintGroups.has(key)) {
161
+ constraintGroups.set(key, {
162
+ table: String(row.table_name),
163
+ type: String(row.constraint_type),
164
+ columns: []
165
+ });
166
+ }
167
+ constraintGroups.get(key).columns.push(String(row.column_name));
168
+ }
169
+ for (const group of constraintGroups.values()) {
170
+ const table = tableMap.get(group.table);
171
+ if (!table) continue;
172
+ if (group.type === "PRIMARY KEY") {
173
+ table.primaryKey = group.columns;
174
+ } else if (group.type === "UNIQUE") {
175
+ table.uniques.push(group.columns);
176
+ }
177
+ }
178
+ const fkGroups = /* @__PURE__ */ new Map();
179
+ for (const row of fks.rows) {
180
+ const key = String(row.constraint_name);
181
+ if (!fkGroups.has(key)) {
182
+ fkGroups.set(key, {
183
+ constraintName: key,
184
+ table: String(row.table_name),
185
+ columns: [],
186
+ referencedTable: String(row.referenced_table),
187
+ referencedColumns: [],
188
+ onDelete: String(row.delete_rule),
189
+ onUpdate: String(row.update_rule)
190
+ });
191
+ }
192
+ const fk = fkGroups.get(key);
193
+ fk.columns.push(String(row.column_name));
194
+ fk.referencedColumns.push(String(row.referenced_column));
195
+ }
196
+ return {
197
+ extensions: extensions.rows.map((r) => String(r.extname)).filter((name) => TRACKED_EXTENSIONS.has(name)),
198
+ enums: Array.from(enumMap, ([name, values]) => ({ name, values })),
199
+ tables: Array.from(tableMap.values()),
200
+ foreignKeys: Array.from(fkGroups.values()),
201
+ checks: checks.rows.map((r) => ({
202
+ table: String(r.table_name),
203
+ name: String(r.name),
204
+ definition: String(r.definition)
205
+ })),
206
+ indexes: indexes.rows.map((r) => ({ definition: String(r.indexdef) }))
207
+ };
208
+ }
209
+ function columnDdl(column, isSinglePk) {
210
+ let sqlType = column.sqlType;
211
+ if (column.isSerial) {
212
+ sqlType = sqlType === "bigint" ? "bigserial" : sqlType === "smallint" ? "smallserial" : "serial";
213
+ }
214
+ let ddl = `${quoteIdent(column.name)} ${sqlType}`;
215
+ if (isSinglePk) ddl += " PRIMARY KEY";
216
+ if (!column.nullable && !isSinglePk) ddl += " NOT NULL";
217
+ if (column.generatedExpr) {
218
+ ddl += ` GENERATED ALWAYS AS (${column.generatedExpr}) STORED`;
219
+ } else if (column.defaultExpr && !column.isSerial) {
220
+ ddl += ` DEFAULT ${column.defaultExpr}`;
221
+ }
222
+ return ddl;
223
+ }
224
+ function generateSchemaSql(snapshot) {
225
+ const lines = [
226
+ "-- Auto-generated by @stardeck-customer-apps/data-store-sdk",
227
+ "-- Schema snapshot of the live data store, applied to PGlite by the test harness.",
228
+ "-- Do not edit manually \u2014 regenerate with: npx stardeck-data-store generate-types",
229
+ ""
230
+ ];
231
+ for (const ext of snapshot.extensions) {
232
+ lines.push(`CREATE EXTENSION IF NOT EXISTS ${ext === "uuid-ossp" ? '"uuid-ossp"' : ext};`);
233
+ }
234
+ if (snapshot.extensions.length > 0) lines.push("");
235
+ for (const enumType of snapshot.enums) {
236
+ const values = enumType.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
237
+ lines.push(`CREATE TYPE ${quoteIdent(enumType.name)} AS ENUM (${values});`);
238
+ }
239
+ if (snapshot.enums.length > 0) lines.push("");
240
+ for (const table of snapshot.tables) {
241
+ const singlePk = table.primaryKey.length === 1 ? table.primaryKey[0] : null;
242
+ const parts = table.columns.map((col) => " " + columnDdl(col, col.name === singlePk));
243
+ if (table.primaryKey.length > 1) {
244
+ parts.push(` PRIMARY KEY (${table.primaryKey.map(quoteIdent).join(", ")})`);
245
+ }
246
+ for (const unique of table.uniques) {
247
+ parts.push(` UNIQUE (${unique.map(quoteIdent).join(", ")})`);
248
+ }
249
+ lines.push(`CREATE TABLE ${quoteIdent(table.name)} (`);
250
+ lines.push(parts.join(",\n"));
251
+ lines.push(`);`);
252
+ lines.push("");
253
+ }
254
+ for (const check of snapshot.checks) {
255
+ lines.push(
256
+ `ALTER TABLE ${quoteIdent(check.table)} ADD CONSTRAINT ${quoteIdent(check.name)} ${check.definition};`
257
+ );
258
+ }
259
+ if (snapshot.checks.length > 0) lines.push("");
260
+ for (const fk of snapshot.foreignKeys) {
261
+ 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(", ")})`;
262
+ if (fk.onDelete !== "NO ACTION") stmt += ` ON DELETE ${fk.onDelete}`;
263
+ if (fk.onUpdate !== "NO ACTION") stmt += ` ON UPDATE ${fk.onUpdate}`;
264
+ lines.push(`${stmt};`);
265
+ }
266
+ if (snapshot.foreignKeys.length > 0) lines.push("");
267
+ for (const index of snapshot.indexes) {
268
+ lines.push(`${index.definition};`);
269
+ }
270
+ if (snapshot.indexes.length > 0) lines.push("");
271
+ return lines.join("\n");
272
+ }
273
+
274
+ // src/cli/generate-types.ts
29
275
  var PG_TYPE_MAP = {
30
276
  text: "string",
31
277
  varchar: "string",
@@ -131,17 +377,25 @@ async function main() {
131
377
  const args = process.argv.slice(2);
132
378
  let connectionString = process.env.DATA_STORE_URL;
133
379
  let outputPath = "./src/generated/data-store-types.ts";
380
+ let schemaOutputPath;
381
+ let emitSchema = true;
134
382
  for (let i = 0; i < args.length; i++) {
135
383
  if (args[i] === "--connection-string" && args[i + 1]) {
136
384
  connectionString = args[++i];
137
385
  } else if (args[i] === "--output" && args[i + 1]) {
138
386
  outputPath = args[++i];
387
+ } else if (args[i] === "--schema-output" && args[i + 1]) {
388
+ schemaOutputPath = args[++i];
389
+ } else if (args[i] === "--no-schema") {
390
+ emitSchema = false;
139
391
  } else if (args[i] === "--help") {
140
392
  console.log(`Usage: stardeck-data-store generate-types [options]
141
393
 
142
394
  Options:
143
395
  --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
144
396
  --output <path> Output file path (default: ./src/generated/data-store-types.ts)
397
+ --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
398
+ --no-schema Skip the DDL snapshot used by the test harness
145
399
  --help Show this help message`);
146
400
  process.exit(0);
147
401
  }
@@ -167,6 +421,17 @@ Options:
167
421
  }
168
422
  (0, import_fs.writeFileSync)(outputPath, typeScript, "utf-8");
169
423
  console.log(`Types written to ${outputPath}`);
424
+ if (emitSchema) {
425
+ const schemaPath = schemaOutputPath ?? `${dir ? `${dir}/` : ""}data-store-schema.sql`;
426
+ const pool = new import_serverless.Pool({ connectionString });
427
+ try {
428
+ const snapshot = await introspectSchemaSnapshot(pool);
429
+ (0, import_fs.writeFileSync)(schemaPath, generateSchemaSql(snapshot), "utf-8");
430
+ console.log(`Schema snapshot written to ${schemaPath} (used by the test harness)`);
431
+ } finally {
432
+ await pool.end();
433
+ }
434
+ }
170
435
  }
171
436
  main().catch((error) => {
172
437
  console.error("Failed to generate types:", error);
@@ -3,6 +3,252 @@
3
3
  // src/cli/generate-types.ts
4
4
  import { writeFileSync } from "fs";
5
5
  import { Pool } from "@neondatabase/serverless";
6
+
7
+ // src/cli/schema-ddl.ts
8
+ function quoteIdent(name) {
9
+ return `"${name.replace(/"/g, '""')}"`;
10
+ }
11
+ var TRACKED_EXTENSIONS = /* @__PURE__ */ new Set(["pgcrypto", "pg_trgm", "uuid-ossp", "citext"]);
12
+ function resolveSqlType(row) {
13
+ const dataType = row.data_type;
14
+ if (dataType === "ARRAY") {
15
+ return `${row.udt_name.replace(/^_/, "")}[]`;
16
+ }
17
+ if (dataType === "USER-DEFINED") {
18
+ return quoteIdent(row.udt_name);
19
+ }
20
+ if (dataType === "character varying") {
21
+ return row.character_maximum_length ? `varchar(${row.character_maximum_length})` : "varchar";
22
+ }
23
+ if (dataType === "character") {
24
+ return row.character_maximum_length ? `char(${row.character_maximum_length})` : "char";
25
+ }
26
+ if (dataType === "numeric") {
27
+ return row.numeric_precision !== null && row.numeric_scale !== null ? `numeric(${row.numeric_precision},${row.numeric_scale})` : "numeric";
28
+ }
29
+ if (dataType === "timestamp with time zone") return "timestamptz";
30
+ if (dataType === "timestamp without time zone") return "timestamp";
31
+ if (dataType === "time with time zone") return "timetz";
32
+ if (dataType === "time without time zone") return "time";
33
+ return dataType;
34
+ }
35
+ async function introspectSchemaSnapshot(executor) {
36
+ const [extensions, enums, columns, constraints, fks, checks, indexes] = await Promise.all([
37
+ executor.query(`SELECT extname FROM pg_extension ORDER BY extname`),
38
+ executor.query(
39
+ `SELECT t.typname AS name, e.enumlabel AS value
40
+ FROM pg_type t
41
+ JOIN pg_enum e ON e.enumtypid = t.oid
42
+ JOIN pg_namespace n ON n.oid = t.typnamespace
43
+ WHERE n.nspname = 'public'
44
+ ORDER BY t.typname, e.enumsortorder`
45
+ ),
46
+ executor.query(
47
+ `SELECT c.table_name, c.column_name, c.data_type, c.udt_name, c.is_nullable,
48
+ c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale,
49
+ c.is_generated, c.generation_expression
50
+ FROM information_schema.columns c
51
+ JOIN information_schema.tables t
52
+ ON c.table_name = t.table_name AND c.table_schema = t.table_schema
53
+ WHERE c.table_schema = 'public'
54
+ AND t.table_type = 'BASE TABLE'
55
+ AND t.table_name NOT LIKE '\\_deleted\\_%'
56
+ ORDER BY c.table_name, c.ordinal_position`
57
+ ),
58
+ executor.query(
59
+ `SELECT tc.table_name, tc.constraint_name, tc.constraint_type, ku.column_name
60
+ FROM information_schema.table_constraints tc
61
+ JOIN information_schema.key_column_usage ku
62
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
63
+ WHERE tc.table_schema = 'public'
64
+ AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE')
65
+ ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
66
+ ),
67
+ executor.query(
68
+ `SELECT tc.constraint_name, tc.table_name, ku.column_name,
69
+ ccu.table_name AS referenced_table, ccu.column_name AS referenced_column,
70
+ rc.delete_rule, rc.update_rule
71
+ FROM information_schema.table_constraints tc
72
+ JOIN information_schema.key_column_usage ku
73
+ ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
74
+ JOIN information_schema.constraint_column_usage ccu
75
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
76
+ JOIN information_schema.referential_constraints rc
77
+ ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema
78
+ WHERE tc.table_schema = 'public' AND tc.constraint_type = 'FOREIGN KEY'
79
+ ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
80
+ ),
81
+ executor.query(
82
+ `SELECT rel.relname AS table_name, con.conname AS name,
83
+ pg_get_constraintdef(con.oid) AS definition
84
+ FROM pg_constraint con
85
+ JOIN pg_class rel ON rel.oid = con.conrelid
86
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
87
+ WHERE ns.nspname = 'public' AND con.contype = 'c'
88
+ AND rel.relname NOT LIKE '\\_deleted\\_%'
89
+ ORDER BY rel.relname, con.conname`
90
+ ),
91
+ executor.query(
92
+ `SELECT i.indexdef
93
+ FROM pg_indexes i
94
+ WHERE i.schemaname = 'public'
95
+ AND i.tablename NOT LIKE '\\_deleted\\_%'
96
+ AND NOT EXISTS (
97
+ SELECT 1 FROM pg_constraint c
98
+ JOIN pg_class cls ON cls.oid = c.conindid
99
+ WHERE cls.relname = i.indexname
100
+ )
101
+ ORDER BY i.indexname`
102
+ )
103
+ ]);
104
+ const enumMap = /* @__PURE__ */ new Map();
105
+ for (const row of enums.rows) {
106
+ const name = String(row.name);
107
+ if (!enumMap.has(name)) enumMap.set(name, []);
108
+ enumMap.get(name).push(String(row.value));
109
+ }
110
+ const tableMap = /* @__PURE__ */ new Map();
111
+ for (const row of columns.rows) {
112
+ const tableName = String(row.table_name);
113
+ if (!tableMap.has(tableName)) {
114
+ tableMap.set(tableName, { name: tableName, columns: [], primaryKey: [], uniques: [] });
115
+ }
116
+ const defaultExpr = row.column_default === null ? null : String(row.column_default);
117
+ const isSerial = defaultExpr !== null && /^nextval\(/.test(defaultExpr);
118
+ const isGenerated = row.is_generated === "ALWAYS";
119
+ tableMap.get(tableName).columns.push({
120
+ name: String(row.column_name),
121
+ sqlType: resolveSqlType({
122
+ data_type: String(row.data_type),
123
+ udt_name: String(row.udt_name),
124
+ character_maximum_length: row.character_maximum_length,
125
+ numeric_precision: row.numeric_precision,
126
+ numeric_scale: row.numeric_scale
127
+ }),
128
+ nullable: row.is_nullable === "YES",
129
+ defaultExpr: isGenerated ? null : defaultExpr,
130
+ isSerial: !isGenerated && isSerial,
131
+ generatedExpr: isGenerated ? String(row.generation_expression) : null
132
+ });
133
+ }
134
+ const constraintGroups = /* @__PURE__ */ new Map();
135
+ for (const row of constraints.rows) {
136
+ const key = String(row.constraint_name);
137
+ if (!constraintGroups.has(key)) {
138
+ constraintGroups.set(key, {
139
+ table: String(row.table_name),
140
+ type: String(row.constraint_type),
141
+ columns: []
142
+ });
143
+ }
144
+ constraintGroups.get(key).columns.push(String(row.column_name));
145
+ }
146
+ for (const group of constraintGroups.values()) {
147
+ const table = tableMap.get(group.table);
148
+ if (!table) continue;
149
+ if (group.type === "PRIMARY KEY") {
150
+ table.primaryKey = group.columns;
151
+ } else if (group.type === "UNIQUE") {
152
+ table.uniques.push(group.columns);
153
+ }
154
+ }
155
+ const fkGroups = /* @__PURE__ */ new Map();
156
+ for (const row of fks.rows) {
157
+ const key = String(row.constraint_name);
158
+ if (!fkGroups.has(key)) {
159
+ fkGroups.set(key, {
160
+ constraintName: key,
161
+ table: String(row.table_name),
162
+ columns: [],
163
+ referencedTable: String(row.referenced_table),
164
+ referencedColumns: [],
165
+ onDelete: String(row.delete_rule),
166
+ onUpdate: String(row.update_rule)
167
+ });
168
+ }
169
+ const fk = fkGroups.get(key);
170
+ fk.columns.push(String(row.column_name));
171
+ fk.referencedColumns.push(String(row.referenced_column));
172
+ }
173
+ return {
174
+ extensions: extensions.rows.map((r) => String(r.extname)).filter((name) => TRACKED_EXTENSIONS.has(name)),
175
+ enums: Array.from(enumMap, ([name, values]) => ({ name, values })),
176
+ tables: Array.from(tableMap.values()),
177
+ foreignKeys: Array.from(fkGroups.values()),
178
+ checks: checks.rows.map((r) => ({
179
+ table: String(r.table_name),
180
+ name: String(r.name),
181
+ definition: String(r.definition)
182
+ })),
183
+ indexes: indexes.rows.map((r) => ({ definition: String(r.indexdef) }))
184
+ };
185
+ }
186
+ function columnDdl(column, isSinglePk) {
187
+ let sqlType = column.sqlType;
188
+ if (column.isSerial) {
189
+ sqlType = sqlType === "bigint" ? "bigserial" : sqlType === "smallint" ? "smallserial" : "serial";
190
+ }
191
+ let ddl = `${quoteIdent(column.name)} ${sqlType}`;
192
+ if (isSinglePk) ddl += " PRIMARY KEY";
193
+ if (!column.nullable && !isSinglePk) ddl += " NOT NULL";
194
+ if (column.generatedExpr) {
195
+ ddl += ` GENERATED ALWAYS AS (${column.generatedExpr}) STORED`;
196
+ } else if (column.defaultExpr && !column.isSerial) {
197
+ ddl += ` DEFAULT ${column.defaultExpr}`;
198
+ }
199
+ return ddl;
200
+ }
201
+ function generateSchemaSql(snapshot) {
202
+ const lines = [
203
+ "-- Auto-generated by @stardeck-customer-apps/data-store-sdk",
204
+ "-- Schema snapshot of the live data store, applied to PGlite by the test harness.",
205
+ "-- Do not edit manually \u2014 regenerate with: npx stardeck-data-store generate-types",
206
+ ""
207
+ ];
208
+ for (const ext of snapshot.extensions) {
209
+ lines.push(`CREATE EXTENSION IF NOT EXISTS ${ext === "uuid-ossp" ? '"uuid-ossp"' : ext};`);
210
+ }
211
+ if (snapshot.extensions.length > 0) lines.push("");
212
+ for (const enumType of snapshot.enums) {
213
+ const values = enumType.values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
214
+ lines.push(`CREATE TYPE ${quoteIdent(enumType.name)} AS ENUM (${values});`);
215
+ }
216
+ if (snapshot.enums.length > 0) lines.push("");
217
+ for (const table of snapshot.tables) {
218
+ const singlePk = table.primaryKey.length === 1 ? table.primaryKey[0] : null;
219
+ const parts = table.columns.map((col) => " " + columnDdl(col, col.name === singlePk));
220
+ if (table.primaryKey.length > 1) {
221
+ parts.push(` PRIMARY KEY (${table.primaryKey.map(quoteIdent).join(", ")})`);
222
+ }
223
+ for (const unique of table.uniques) {
224
+ parts.push(` UNIQUE (${unique.map(quoteIdent).join(", ")})`);
225
+ }
226
+ lines.push(`CREATE TABLE ${quoteIdent(table.name)} (`);
227
+ lines.push(parts.join(",\n"));
228
+ lines.push(`);`);
229
+ lines.push("");
230
+ }
231
+ for (const check of snapshot.checks) {
232
+ lines.push(
233
+ `ALTER TABLE ${quoteIdent(check.table)} ADD CONSTRAINT ${quoteIdent(check.name)} ${check.definition};`
234
+ );
235
+ }
236
+ if (snapshot.checks.length > 0) lines.push("");
237
+ for (const fk of snapshot.foreignKeys) {
238
+ 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(", ")})`;
239
+ if (fk.onDelete !== "NO ACTION") stmt += ` ON DELETE ${fk.onDelete}`;
240
+ if (fk.onUpdate !== "NO ACTION") stmt += ` ON UPDATE ${fk.onUpdate}`;
241
+ lines.push(`${stmt};`);
242
+ }
243
+ if (snapshot.foreignKeys.length > 0) lines.push("");
244
+ for (const index of snapshot.indexes) {
245
+ lines.push(`${index.definition};`);
246
+ }
247
+ if (snapshot.indexes.length > 0) lines.push("");
248
+ return lines.join("\n");
249
+ }
250
+
251
+ // src/cli/generate-types.ts
6
252
  var PG_TYPE_MAP = {
7
253
  text: "string",
8
254
  varchar: "string",
@@ -108,17 +354,25 @@ async function main() {
108
354
  const args = process.argv.slice(2);
109
355
  let connectionString = process.env.DATA_STORE_URL;
110
356
  let outputPath = "./src/generated/data-store-types.ts";
357
+ let schemaOutputPath;
358
+ let emitSchema = true;
111
359
  for (let i = 0; i < args.length; i++) {
112
360
  if (args[i] === "--connection-string" && args[i + 1]) {
113
361
  connectionString = args[++i];
114
362
  } else if (args[i] === "--output" && args[i + 1]) {
115
363
  outputPath = args[++i];
364
+ } else if (args[i] === "--schema-output" && args[i + 1]) {
365
+ schemaOutputPath = args[++i];
366
+ } else if (args[i] === "--no-schema") {
367
+ emitSchema = false;
116
368
  } else if (args[i] === "--help") {
117
369
  console.log(`Usage: stardeck-data-store generate-types [options]
118
370
 
119
371
  Options:
120
372
  --connection-string <url> Postgres connection string (default: DATA_STORE_URL env var)
121
373
  --output <path> Output file path (default: ./src/generated/data-store-types.ts)
374
+ --schema-output <path> DDL snapshot path (default: data-store-schema.sql next to --output)
375
+ --no-schema Skip the DDL snapshot used by the test harness
122
376
  --help Show this help message`);
123
377
  process.exit(0);
124
378
  }
@@ -144,6 +398,17 @@ Options:
144
398
  }
145
399
  writeFileSync(outputPath, typeScript, "utf-8");
146
400
  console.log(`Types written to ${outputPath}`);
401
+ if (emitSchema) {
402
+ const schemaPath = schemaOutputPath ?? `${dir ? `${dir}/` : ""}data-store-schema.sql`;
403
+ const pool = new Pool({ connectionString });
404
+ try {
405
+ const snapshot = await introspectSchemaSnapshot(pool);
406
+ writeFileSync(schemaPath, generateSchemaSql(snapshot), "utf-8");
407
+ console.log(`Schema snapshot written to ${schemaPath} (used by the test harness)`);
408
+ } finally {
409
+ await pool.end();
410
+ }
411
+ }
147
412
  }
148
413
  main().catch((error) => {
149
414
  console.error("Failed to generate types:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/data-store-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -55,10 +55,10 @@
55
55
  }
56
56
  },
57
57
  "devDependencies": {
58
- "@neondatabase/serverless": "^0.10.0",
59
- "kysely-neon": "^2.0.0",
58
+ "@neondatabase/serverless": "^1.1.0",
60
59
  "@stardeck-customer-apps/tsconfig": "*",
61
60
  "@types/node": "^24.10.1",
61
+ "kysely-neon": "^2.0.0",
62
62
  "tsup": "^8.0.0",
63
63
  "typescript": "^5.0.0",
64
64
  "vitest": "^3.2.4"