@rebasepro/server-postgresql 0.0.1-canary.f81da60 → 0.1.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.
Files changed (59) hide show
  1. package/dist/index.es.js +287 -21
  2. package/dist/index.es.js.map +1 -1
  3. package/dist/index.umd.js +287 -21
  4. package/dist/index.umd.js.map +1 -1
  5. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  6. package/dist/server-postgresql/src/schema/introspect-db-inference.d.ts +5 -0
  7. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +44 -9
  8. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +9 -0
  9. package/dist/types/src/controllers/auth.d.ts +8 -2
  10. package/dist/types/src/controllers/client.d.ts +13 -0
  11. package/dist/types/src/controllers/navigation.d.ts +18 -6
  12. package/dist/types/src/controllers/registry.d.ts +9 -1
  13. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
  14. package/dist/types/src/rebase_context.d.ts +17 -0
  15. package/dist/types/src/types/collections.d.ts +20 -1
  16. package/dist/types/src/types/component_ref.d.ts +47 -0
  17. package/dist/types/src/types/entity_views.d.ts +2 -1
  18. package/dist/types/src/types/index.d.ts +1 -0
  19. package/dist/types/src/types/properties.d.ts +15 -3
  20. package/dist/types/src/types/translations.d.ts +2 -0
  21. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +21 -0
  22. package/examples/sdk-demo/node_modules/esbuild/README.md +3 -0
  23. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +223 -0
  24. package/examples/sdk-demo/node_modules/esbuild/install.js +289 -0
  25. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +716 -0
  26. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +2242 -0
  27. package/examples/sdk-demo/node_modules/esbuild/package.json +49 -0
  28. package/package.json +5 -5
  29. package/src/PostgresBackendDriver.ts +23 -6
  30. package/src/cli.ts +10 -2
  31. package/src/data-transformer.ts +84 -1
  32. package/src/schema/doctor.ts +14 -2
  33. package/src/schema/generate-drizzle-schema-logic.ts +52 -5
  34. package/src/schema/introspect-db-inference.ts +238 -0
  35. package/src/schema/introspect-db-logic.ts +365 -61
  36. package/src/schema/introspect-db.ts +66 -23
  37. package/src/services/EntityFetchService.ts +16 -0
  38. package/src/services/EntityPersistService.ts +88 -12
  39. package/test/generate-drizzle-schema.test.ts +295 -0
  40. package/test/introspect-db-generation.test.ts +32 -10
  41. package/test/property-ordering.test.ts +395 -0
  42. package/jest-all.log +0 -3128
  43. package/jest.log +0 -49
  44. package/scratch.ts +0 -41
  45. package/test-drizzle-bug.ts +0 -18
  46. package/test-drizzle-out/0000_cultured_freak.sql +0 -7
  47. package/test-drizzle-out/0001_tiresome_professor_monster.sql +0 -1
  48. package/test-drizzle-out/meta/0000_snapshot.json +0 -55
  49. package/test-drizzle-out/meta/0001_snapshot.json +0 -63
  50. package/test-drizzle-out/meta/_journal.json +0 -20
  51. package/test-drizzle-prompt.sh +0 -2
  52. package/test-policy-prompt.sh +0 -3
  53. package/test-programmatic.ts +0 -30
  54. package/test-programmatic2.ts +0 -59
  55. package/test-schema-no-policies.ts +0 -12
  56. package/test_drizzle_mock.js +0 -3
  57. package/test_find_changed.mjs +0 -32
  58. package/test_hash.js +0 -14
  59. package/test_output.txt +0 -3145
@@ -4,6 +4,7 @@ import path from "path";
4
4
  import pg from "pg";
5
5
  import arg from "arg";
6
6
  import * as dotenv from "dotenv";
7
+ import readline from "readline";
7
8
 
8
9
  import {
9
10
  TableRow,
@@ -24,15 +25,24 @@ async function main() {
24
25
  const args = arg(
25
26
  {
26
27
  "--output": String,
28
+ "--collections": String,
27
29
  "--force": Boolean,
28
30
  "--schema": String,
31
+ "--data-inference": Boolean,
29
32
  "-o": "--output",
33
+ "-c": "--collections",
30
34
  "-f": "--force",
31
35
  },
32
36
  { permissive: true }
33
37
  );
34
38
 
35
- const outDir = args["--output"] || path.resolve(process.cwd(), "config", "collections");
39
+ const cwd = process.cwd();
40
+ const isBackendDir = path.basename(cwd) === "backend";
41
+ const defaultOutDir = isBackendDir
42
+ ? path.resolve(cwd, "..", "config", "collections")
43
+ : path.resolve(cwd, "config", "collections");
44
+
45
+ const outDir = args["--output"] || args["--collections"] || defaultOutDir;
36
46
  const force = args["--force"] || false;
37
47
  const pgSchema = args["--schema"] || "public";
38
48
 
@@ -143,32 +153,65 @@ async function main() {
143
153
 
144
154
  console.log(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
145
155
 
156
+ let runDataInference = false;
157
+ if (args["--data-inference"] !== undefined) {
158
+ runDataInference = args["--data-inference"];
159
+ } else {
160
+ const rl = readline.createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout
163
+ });
164
+ const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow("? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) "), resolve));
165
+ runDataInference = answer.trim().toLowerCase() === 'y';
166
+ rl.close();
167
+ }
168
+
169
+ if (runDataInference) {
170
+ console.log(chalk.gray(`Sampling database rows for data inference...`));
171
+ }
172
+
146
173
  // Generate Collections
147
174
  const generatedFiles: string[] = [];
148
175
  const skippedFiles: string[] = [];
149
176
 
150
- for (const [tableName, meta] of tablesMap.entries()) {
151
- if (joinTables.has(tableName)) continue; // We don't generate base collections for pure join tables
152
-
153
- // ── File overwrite protection ──────────────────────────────
154
- const filePath = path.join(outDir, `${tableName}.ts`);
155
- if (fs.existsSync(filePath) && !force) {
156
- skippedFiles.push(tableName);
157
- continue;
158
- }
159
-
160
- const fileContent = generateCollectionFile(
161
- tableName,
162
- meta,
163
- fks,
164
- joinTables,
165
- tablesMap,
166
- enumMap,
167
- );
168
-
169
- fs.writeFileSync(filePath, fileContent, "utf-8");
170
- generatedFiles.push(tableName);
171
- console.log(chalk.green(` ✓ ${filePath}`));
177
+ const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
178
+
179
+ const BATCH_SIZE = 10;
180
+ for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
181
+ const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
182
+
183
+ await Promise.all(batch.map(async ([tableName, meta]) => {
184
+ // ── File overwrite protection ──────────────────────────────
185
+ const filePath = path.join(outDir, `${tableName}.ts`);
186
+ if (fs.existsSync(filePath) && !force) {
187
+ skippedFiles.push(tableName);
188
+ return;
189
+ }
190
+
191
+ let sampleData: Record<string, unknown>[] | undefined = undefined;
192
+ if (runDataInference) {
193
+ try {
194
+ const { rows } = await client.query(`SELECT * FROM "${pgSchema}"."${tableName}" LIMIT 100`);
195
+ sampleData = rows;
196
+ } catch (err) {
197
+ console.error(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
198
+ }
199
+ }
200
+
201
+ const fileContent = generateCollectionFile(
202
+ tableName,
203
+ meta,
204
+ fks,
205
+ joinTables,
206
+ tablesMap,
207
+ enumMap,
208
+ sampleData,
209
+ );
210
+
211
+ fs.writeFileSync(filePath, fileContent, "utf-8");
212
+ generatedFiles.push(tableName);
213
+ console.log(chalk.green(` ✓ ${filePath}`));
214
+ }));
172
215
  }
173
216
 
174
217
  // Generate index.ts (sorted alphabetically for deterministic output)
@@ -617,6 +617,10 @@ export class EntityFetchService {
617
617
 
618
618
  return entity;
619
619
  } catch (e) {
620
+ if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
621
+ console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
622
+ console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
623
+ }
620
624
  console.warn(`[EntityFetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select:`, e);
621
625
  }
622
626
  }
@@ -733,6 +737,10 @@ export class EntityFetchService {
733
737
 
734
738
  return entities;
735
739
  } catch (e) {
740
+ if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
741
+ console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
742
+ console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
743
+ }
736
744
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
737
745
  }
738
746
  }
@@ -1195,6 +1203,10 @@ export class EntityFetchService {
1195
1203
 
1196
1204
  return restRows;
1197
1205
  } catch (e) {
1206
+ if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
1207
+ console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1208
+ console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
1209
+ }
1198
1210
  console.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back:`, e);
1199
1211
  }
1200
1212
  }
@@ -1305,6 +1317,10 @@ export class EntityFetchService {
1305
1317
 
1306
1318
  return restRow;
1307
1319
  } catch (e) {
1320
+ if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
1321
+ console.error(`[EntityFetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
1322
+ console.error(`Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.`);
1323
+ }
1308
1324
  console.warn(`[fetchEntityForRest] db.query.findFirst failed for ${collectionPath}, falling back:`, e);
1309
1325
  }
1310
1326
  }
@@ -298,51 +298,127 @@ export class EntityPersistService {
298
298
 
299
299
  if (pgError) {
300
300
  const detail = pgError.detail as string | undefined;
301
+ const hint = pgError.hint as string | undefined;
301
302
  const constraint = pgError.constraint as string | undefined;
302
303
  const column = pgError.column as string | undefined;
303
304
  const table = pgError.table as string | undefined;
305
+ const dataType = pgError.dataType as string | undefined;
306
+ const pgMessage = pgError.message || "Unknown database error";
307
+
308
+ const suffix = hint ? ` Hint: ${hint}` : "";
309
+ const tableRef = table ?? collectionSlug;
304
310
 
305
311
  switch (pgError.code) {
306
312
  case "23503": // foreign_key_violation
307
313
  return new Error(
308
314
  detail
309
- ? `Foreign key constraint violated: ${detail}`
310
- : `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
315
+ ? `Foreign key constraint violated: ${detail}${suffix}`
316
+ : `Cannot save: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
311
317
  );
312
318
  case "23505": // unique_violation
313
319
  return new Error(
314
320
  detail
315
- ? `Duplicate value: ${detail}`
316
- : `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
321
+ ? `Duplicate value: ${detail}${suffix}`
322
+ : `Cannot save: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
317
323
  );
318
324
  case "23502": // not_null_violation
319
325
  return new Error(
320
- `Missing required field: "${column ?? "unknown"}" in "${table ?? collectionSlug}" cannot be empty.`
326
+ `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`
321
327
  );
322
328
  case "23514": // check_violation
323
329
  return new Error(
324
- `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".`
330
+ `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${collectionSlug}".${suffix}`
331
+ );
332
+ case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
333
+ return new Error(
334
+ `Invalid data format in "${collectionSlug}": ${pgMessage}${suffix}`
335
+ );
336
+ case "22001": // string_data_right_truncation (value too long)
337
+ return new Error(
338
+ `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
339
+ );
340
+ case "22003": // numeric_value_out_of_range
341
+ return new Error(
342
+ `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`
325
343
  );
344
+ case "42703": // undefined_column
345
+ return new Error(
346
+ `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
347
+ );
348
+ case "42P01": // undefined_table
349
+ return new Error(
350
+ `Table not found for "${collectionSlug}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`
351
+ );
352
+ default: {
353
+ // Unhandled PG code — still surface the actual database message
354
+ const parts = [`Database error in "${collectionSlug}" [${pgError.code}]: ${pgMessage}`];
355
+ if (detail) parts.push(`Detail: ${detail}`);
356
+ if (column) parts.push(`Column: ${column}`);
357
+ if (dataType) parts.push(`Data type: ${dataType}`);
358
+ if (constraint) parts.push(`Constraint: ${constraint}`);
359
+ if (hint) parts.push(`Hint: ${hint}`);
360
+ return new Error(parts.join(". "));
361
+ }
362
+ }
363
+ }
364
+
365
+ // No PG error found — try to extract a useful message from the
366
+ // Drizzle wrapper instead of leaking the raw SQL query + params.
367
+ const causeMessage = this.extractCauseMessage(error);
368
+ if (causeMessage) {
369
+ return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
370
+ }
371
+
372
+ // Last resort: use the original error message but strip the SQL query
373
+ if (error instanceof Error) {
374
+ const cleaned = this.stripSqlFromMessage(error.message, collectionSlug);
375
+ return new Error(cleaned);
376
+ }
377
+ return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
378
+ }
379
+
380
+ /**
381
+ * Walk the error cause chain and return the deepest meaningful message.
382
+ */
383
+ private extractCauseMessage(error: unknown): string | null {
384
+ if (!error || typeof error !== "object") return null;
385
+ const err = error as Error & { cause?: unknown };
386
+
387
+ if (err.cause && typeof err.cause === "object") {
388
+ const deeper = this.extractCauseMessage(err.cause);
389
+ if (deeper) return deeper;
390
+ // The cause itself has a message
391
+ if (err.cause instanceof Error && err.cause.message) {
392
+ return err.cause.message;
326
393
  }
327
394
  }
395
+ return null;
396
+ }
328
397
 
329
- // Fall through: re-throw original
330
- if (error instanceof Error) return error;
331
- return new Error(String(error));
398
+ /**
399
+ * Strip the raw SQL query from a Drizzle "Failed query: ..." message,
400
+ * keeping only the error description.
401
+ */
402
+ private stripSqlFromMessage(message: string, collectionSlug: string): string {
403
+ // Drizzle format: "Failed query: <SQL>\nparams: <params>"
404
+ if (message.startsWith("Failed query:")) {
405
+ return `Failed to save entity in "${collectionSlug}". Check server logs for details.`;
406
+ }
407
+ return message;
332
408
  }
333
409
 
334
410
  /**
335
411
  * Extract the underlying PostgreSQL error from a Drizzle wrapper.
336
412
  * Drizzle wraps PG errors in a `cause` property.
337
413
  */
338
- private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown }) | null {
414
+ private extractPgError(error: unknown): (Error & { code?: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown }) | null {
339
415
  if (!error || typeof error !== "object") return null;
340
416
 
341
417
  const err = error as Error & { code?: string; cause?: unknown; detail?: unknown };
342
418
 
343
419
  // Check if the error itself has a PG error code
344
- if (err.code && /^[0-9]{5}$/.test(err.code)) {
345
- return err as Error & { code: string; detail?: unknown; constraint?: unknown; column?: unknown; table?: unknown };
420
+ if (err.code && /^[0-9A-Z]{5}$/.test(err.code)) {
421
+ return err as Error & { code: string; detail?: unknown; hint?: unknown; constraint?: unknown; column?: unknown; table?: unknown; dataType?: unknown };
346
422
  }
347
423
 
348
424
  // Check the cause chain (Drizzle wraps PG errors)
@@ -1033,3 +1033,298 @@ isId: true }
1033
1033
  expect(cleanResult).toContain("user_name: varchar(\"user_name\").primaryKey()");
1034
1034
  });
1035
1035
  });
1036
+
1037
+ // ── columnName tests ────────────────────────────────────────────────────
1038
+ describe("generateDrizzleSchema columnName support", () => {
1039
+ const cleanSchema = (schema: string) => {
1040
+ return schema
1041
+ .replace(/\/\/.*$/gm, "")
1042
+ .replace(/\/\*[\s\S]*?\*\//g, "")
1043
+ .replace(/\n{2,}/g, "\n")
1044
+ .replace(/\s+/g, " ")
1045
+ .trim();
1046
+ };
1047
+
1048
+ it("should use explicit columnName as the SQL column name instead of deriving from the property key", async () => {
1049
+ const collections: EntityCollection[] = [{
1050
+ slug: "billing",
1051
+ table: "company_billing_config",
1052
+ name: "Billing",
1053
+ properties: {
1054
+ employee_number_140a: {
1055
+ type: "string",
1056
+ name: "Employee Number 140a",
1057
+ columnName: "employee_number_140a",
1058
+ },
1059
+ contract_number_140a: {
1060
+ type: "string",
1061
+ name: "Contract Number 140a",
1062
+ columnName: "contract_number_140a",
1063
+ },
1064
+ },
1065
+ }];
1066
+
1067
+ const result = await generateSchema(collections);
1068
+ const cleanResult = cleanSchema(result);
1069
+
1070
+ // Must use the exact columnName, NOT toSnakeCase(propKey) which would produce "employee_number_140_a"
1071
+ expect(cleanResult).toContain('employee_number_140a: varchar("employee_number_140a")');
1072
+ expect(cleanResult).toContain('contract_number_140a: varchar("contract_number_140a")');
1073
+
1074
+ // Must NOT contain the broken snake_case version
1075
+ expect(cleanResult).not.toContain("employee_number_140_a");
1076
+ expect(cleanResult).not.toContain("contract_number_140_a");
1077
+ });
1078
+
1079
+ it("should fall back to toSnakeCase when columnName is not set (manually-authored collections)", async () => {
1080
+ const collections: EntityCollection[] = [{
1081
+ slug: "products",
1082
+ table: "products",
1083
+ name: "Products",
1084
+ properties: {
1085
+ productName: {
1086
+ type: "string",
1087
+ name: "Product Name",
1088
+ // No columnName — should derive from key
1089
+ },
1090
+ },
1091
+ }];
1092
+
1093
+ const result = await generateSchema(collections);
1094
+ const cleanResult = cleanSchema(result);
1095
+
1096
+ // JS key stays camelCase, SQL column name gets snake_cased
1097
+ expect(cleanResult).toContain('productName: varchar("product_name")');
1098
+ });
1099
+
1100
+ it("should handle mixed properties — some with columnName, some without", async () => {
1101
+ const collections: EntityCollection[] = [{
1102
+ slug: "config",
1103
+ table: "app_config",
1104
+ name: "Config",
1105
+ properties: {
1106
+ // Introspected: has explicit columnName
1107
+ fee_number_140a: {
1108
+ type: "string",
1109
+ name: "Fee Number",
1110
+ columnName: "fee_number_140a",
1111
+ },
1112
+ // Manually added: no columnName
1113
+ displayName: {
1114
+ type: "string",
1115
+ name: "Display Name",
1116
+ },
1117
+ },
1118
+ }];
1119
+
1120
+ const result = await generateSchema(collections);
1121
+ const cleanResult = cleanSchema(result);
1122
+
1123
+ // Introspected prop uses exact columnName
1124
+ expect(cleanResult).toContain('fee_number_140a: varchar("fee_number_140a")');
1125
+ // Manual prop: JS key stays camelCase, SQL column gets snake_cased
1126
+ expect(cleanResult).toContain('displayName: varchar("display_name")');
1127
+ });
1128
+
1129
+ it("should use columnName for all property types, not just strings", async () => {
1130
+ const collections: EntityCollection[] = [{
1131
+ slug: "metrics",
1132
+ table: "metrics",
1133
+ name: "Metrics",
1134
+ properties: {
1135
+ count_v2: {
1136
+ type: "number",
1137
+ name: "Count V2",
1138
+ columnName: "count_v2",
1139
+ },
1140
+ is_active_v2: {
1141
+ type: "boolean",
1142
+ name: "Is Active V2",
1143
+ columnName: "is_active_v2",
1144
+ },
1145
+ created_at_v2: {
1146
+ type: "date",
1147
+ name: "Created At V2",
1148
+ columnName: "created_at_v2",
1149
+ },
1150
+ metadata_v2: {
1151
+ type: "map",
1152
+ name: "Metadata V2",
1153
+ columnName: "metadata_v2",
1154
+ },
1155
+ },
1156
+ }];
1157
+
1158
+ const result = await generateSchema(collections);
1159
+ const cleanResult = cleanSchema(result);
1160
+
1161
+ expect(cleanResult).toContain('count_v2: numeric("count_v2")');
1162
+ expect(cleanResult).toContain('is_active_v2: boolean("is_active_v2")');
1163
+ expect(cleanResult).toContain('created_at_v2: timestamp("created_at_v2"');
1164
+ expect(cleanResult).toContain('metadata_v2: jsonb("metadata_v2")');
1165
+ });
1166
+
1167
+ it("should reproduce and prevent the medmot bug: digit+letter column names", async () => {
1168
+ // This is the exact scenario from the medmot project that caused the production failure
1169
+ const collections: EntityCollection[] = [{
1170
+ slug: "company_billing_config",
1171
+ table: "company_billing_config",
1172
+ name: "Company Billing Config",
1173
+ properties: {
1174
+ employee_number_140a: { type: "string", name: "Employee Number", columnName: "employee_number_140a" },
1175
+ contract_number_140a: { type: "string", name: "Contract Number", columnName: "contract_number_140a" },
1176
+ amount: { type: "number", name: "Amount" },
1177
+ id: { type: "number", name: "ID", isId: "increment" },
1178
+ service_provider_140a: { type: "string", name: "Service Provider", columnName: "service_provider_140a" },
1179
+ internal_area_code_140a: { type: "string", name: "Internal Area Code", columnName: "internal_area_code_140a" },
1180
+ fee_number_140a: { type: "string", name: "Fee Number", columnName: "fee_number_140a" },
1181
+ receiver_market_participant_140a: { type: "string", name: "Receiver Market Participant", columnName: "receiver_market_participant_140a" },
1182
+ employee_value_number_140a: { type: "string", name: "Employee Value Number", columnName: "employee_value_number_140a" },
1183
+ sender_market_participant_140a: { type: "string", name: "Sender Market Participant", columnName: "sender_market_participant_140a" },
1184
+ processing_indicator_140a: { type: "string", name: "Processing Indicator", columnName: "processing_indicator_140a" },
1185
+ insurance_id_140a: { type: "string", name: "Insurance ID", columnName: "insurance_id_140a" },
1186
+ company_id: { type: "number", name: "Company ID" },
1187
+ },
1188
+ }];
1189
+
1190
+ const result = await generateSchema(collections);
1191
+
1192
+ // Every _140a column must stay _140a, not become _140_a
1193
+ const brokenPattern = /_140_a/;
1194
+ expect(result).not.toMatch(brokenPattern);
1195
+
1196
+ // Spot-check a few exact columns
1197
+ expect(result).toContain('"employee_number_140a"');
1198
+ expect(result).toContain('"contract_number_140a"');
1199
+ expect(result).toContain('"service_provider_140a"');
1200
+ expect(result).toContain('"insurance_id_140a"');
1201
+ });
1202
+
1203
+ describe("generateDrizzleSchema autoValue date properties", () => {
1204
+
1205
+ it("should add .default(sql`now()`) for on_create autoValue", async () => {
1206
+ const collections: EntityCollection[] = [
1207
+ {
1208
+ slug: "articles",
1209
+ table: "articles",
1210
+ name: "Articles",
1211
+ properties: {
1212
+ title: { type: "string" },
1213
+ created_at: {
1214
+ type: "date",
1215
+ autoValue: "on_create"
1216
+ }
1217
+ }
1218
+ }
1219
+ ];
1220
+
1221
+ const result = await generateSchema(collections, true);
1222
+
1223
+ // on_create should produce .default(sql`now()`)
1224
+ expect(result).toContain(".default(sql`now()`)");
1225
+ // No $onUpdate or triggers — on_update logic lives in the backend driver
1226
+ expect(result).not.toContain(".$onUpdate");
1227
+ expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
1228
+ expect(result).not.toContain("CREATE OR REPLACE FUNCTION");
1229
+ });
1230
+
1231
+ it("should add .default(sql`now()`) for on_update autoValue (INSERT default only)", async () => {
1232
+ const collections: EntityCollection[] = [
1233
+ {
1234
+ slug: "articles",
1235
+ table: "articles",
1236
+ name: "Articles",
1237
+ properties: {
1238
+ title: { type: "string" },
1239
+ updated_at: {
1240
+ type: "date",
1241
+ autoValue: "on_update"
1242
+ }
1243
+ }
1244
+ }
1245
+ ];
1246
+
1247
+ const result = await generateSchema(collections, true);
1248
+
1249
+ // on_update should produce .default(sql`now()`) for initial INSERT value
1250
+ expect(result).toContain(".default(sql`now()`)");
1251
+ // No $onUpdate or triggers — update logic is handled by the backend driver
1252
+ expect(result).not.toContain(".$onUpdate");
1253
+ expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
1254
+ });
1255
+
1256
+ it("should not modify date columns without autoValue", async () => {
1257
+ const collections: EntityCollection[] = [
1258
+ {
1259
+ slug: "events",
1260
+ table: "events",
1261
+ name: "Events",
1262
+ properties: {
1263
+ name: { type: "string" },
1264
+ event_date: { type: "date" }
1265
+ }
1266
+ }
1267
+ ];
1268
+
1269
+ const result = await generateSchema(collections, true);
1270
+
1271
+ // A plain date should NOT have any autoValue-related modifiers
1272
+ expect(result).not.toContain(".default(sql`now()`)");
1273
+ expect(result).not.toContain(".$onUpdate");
1274
+ });
1275
+
1276
+ it("should handle both on_create and on_update in the same collection", async () => {
1277
+ const collections: EntityCollection[] = [
1278
+ {
1279
+ slug: "posts",
1280
+ table: "posts",
1281
+ name: "Posts",
1282
+ properties: {
1283
+ title: { type: "string" },
1284
+ created_at: {
1285
+ type: "date",
1286
+ autoValue: "on_create"
1287
+ },
1288
+ updated_at: {
1289
+ type: "date",
1290
+ autoValue: "on_update"
1291
+ }
1292
+ }
1293
+ }
1294
+ ];
1295
+
1296
+ const result = await generateSchema(collections, true);
1297
+
1298
+ // Both should get .default(sql`now()`) for INSERT defaults
1299
+ expect(result).toMatch(/created_at:.*\.default\(sql`now\(\)`\)/);
1300
+ expect(result).toMatch(/updated_at:.*\.default\(sql`now\(\)`\)/);
1301
+ // No $onUpdate or triggers
1302
+ expect(result).not.toContain(".$onUpdate");
1303
+ expect(result).not.toContain("CREATE OR REPLACE TRIGGER");
1304
+ });
1305
+
1306
+ it("should handle on_create with date columnType", async () => {
1307
+ const collections: EntityCollection[] = [
1308
+ {
1309
+ slug: "logs",
1310
+ table: "logs",
1311
+ name: "Logs",
1312
+ properties: {
1313
+ message: { type: "string" },
1314
+ log_date: {
1315
+ type: "date",
1316
+ columnType: "date",
1317
+ autoValue: "on_create"
1318
+ }
1319
+ }
1320
+ }
1321
+ ];
1322
+
1323
+ const result = await generateSchema(collections, true);
1324
+
1325
+ // Should use date() column with the default
1326
+ expect(result).toContain("date(\"log_date\"");
1327
+ expect(result).toContain(".default(sql`now()`)");
1328
+ });
1329
+ });
1330
+ });