postgresdk 0.6.8 → 0.6.11

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/dist/cli.js CHANGED
@@ -561,13 +561,6 @@ export default {
561
561
  */
562
562
  // includeDepthLimit: 3,
563
563
 
564
- /**
565
- * How to handle date/timestamp columns in TypeScript
566
- * - "date": Use JavaScript Date objects
567
- * - "string": Use ISO 8601 strings
568
- * @default "date"
569
- */
570
- // dateType: "date",
571
564
 
572
565
  /**
573
566
  * Server framework for generated API routes
@@ -1015,17 +1008,18 @@ function emitZod(table, opts) {
1015
1008
  if (pg === "jsonb" || pg === "json")
1016
1009
  return `z.unknown()`;
1017
1010
  if (pg === "date" || pg.startsWith("timestamp"))
1018
- return opts.dateType === "date" ? `z.date()` : `z.string()`;
1011
+ return `z.string()`;
1019
1012
  if (pg.startsWith("_"))
1020
1013
  return `z.array(${zFor(pg.slice(1))})`;
1021
1014
  return `z.string()`;
1022
1015
  };
1023
1016
  const fields = table.columns.map((c) => {
1024
1017
  let z = zFor(c.pgType);
1025
- if (c.nullable)
1026
- z += `.nullable()`;
1027
- if (c.hasDefault)
1018
+ if (c.nullable) {
1019
+ z += `.nullish()`;
1020
+ } else if (c.hasDefault) {
1028
1021
  z += `.optional()`;
1022
+ }
1029
1023
  return ` ${c.name}: ${z}`;
1030
1024
  }).join(`,
1031
1025
  `);
@@ -1764,7 +1758,7 @@ function tsTypeFor(pgType, opts) {
1764
1758
  return opts.numericMode === "number" ? "number" : "string";
1765
1759
  }
1766
1760
  if (t === "date" || t.startsWith("timestamp"))
1767
- return opts.dateType === "date" ? "Date" : "string";
1761
+ return "string";
1768
1762
  if (t === "json" || t === "jsonb")
1769
1763
  return "unknown";
1770
1764
  return "string";
@@ -2364,14 +2358,57 @@ export async function deleteRecord(
2364
2358
  }
2365
2359
 
2366
2360
  // src/emit-tests.ts
2367
- function emitTableTest(table, clientPath, framework = "vitest") {
2361
+ function emitTableTest(table, model, clientPath, framework = "vitest") {
2368
2362
  const Type = pascal(table.name);
2369
2363
  const tableName = table.name;
2370
2364
  const imports = getFrameworkImports(framework);
2365
+ const isJunctionTable = table.pk.length > 1;
2371
2366
  const hasForeignKeys = table.fks.length > 0;
2372
- const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, clientPath) : null;
2367
+ const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, model, clientPath) : null;
2373
2368
  const sampleData = generateSampleDataFromSchema(table, hasForeignKeys);
2374
2369
  const updateData = generateUpdateDataFromSchema(table);
2370
+ if (isJunctionTable) {
2371
+ return `${imports}
2372
+ import { SDK } from '${clientPath}';
2373
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
2374
+ ${foreignKeySetup?.imports || ""}
2375
+
2376
+ /**
2377
+ * Basic tests for ${tableName} table operations
2378
+ *
2379
+ * This is a junction table with composite primary key.
2380
+ * Tests are simplified since it represents a many-to-many relationship.
2381
+ */
2382
+ describe('${Type} SDK Operations', () => {
2383
+ let sdk: SDK;
2384
+ ${foreignKeySetup?.variables || ""}
2385
+
2386
+ beforeAll(async () => {
2387
+ sdk = new SDK({
2388
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2389
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2390
+ });
2391
+ ${foreignKeySetup?.setup || ""}
2392
+ });
2393
+
2394
+ ${foreignKeySetup?.cleanup ? `afterAll(async () => {
2395
+ ${foreignKeySetup.cleanup}
2396
+ });
2397
+
2398
+ ` : ""}it('should create a ${tableName} relationship', async () => {
2399
+ const data: Insert${Type} = ${sampleData};
2400
+
2401
+ const created = await sdk.${tableName}.create(data);
2402
+ expect(created).toBeDefined();
2403
+ });
2404
+
2405
+ it('should list ${tableName} relationships', async () => {
2406
+ const list = await sdk.${tableName}.list({ limit: 10 });
2407
+ expect(Array.isArray(list)).toBe(true);
2408
+ });
2409
+ });
2410
+ `;
2411
+ }
2375
2412
  return `${imports}
2376
2413
  import { SDK } from '${clientPath}';
2377
2414
  import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
@@ -2632,33 +2669,56 @@ echo " - To reset the database: docker-compose -f $SCRIPT_DIR/docker-compose.ym
2632
2669
  exit $TEST_EXIT_CODE
2633
2670
  `;
2634
2671
  }
2635
- function generateForeignKeySetup(table, clientPath) {
2672
+ function generateForeignKeySetup(table, model, clientPath) {
2636
2673
  const imports = [];
2637
2674
  const variables = [];
2638
2675
  const setupStatements = [];
2639
2676
  const cleanupStatements = [];
2640
- const foreignTables = new Set;
2677
+ const foreignTablesData = new Map;
2641
2678
  for (const fk of table.fks) {
2642
- const foreignTable = fk.toTable;
2643
- if (!foreignTables.has(foreignTable)) {
2644
- foreignTables.add(foreignTable);
2645
- const ForeignType = pascal(foreignTable);
2646
- imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTable}';`);
2647
- variables.push(`let ${foreignTable}Id: string;`);
2648
- setupStatements.push(`
2649
- // Create parent ${foreignTable} record for foreign key reference
2650
- const ${foreignTable}Data: Insert${ForeignType} = ${generateMinimalDataForTable(foreignTable)};
2651
- const created${ForeignType} = await sdk.${foreignTable}.create(${foreignTable}Data);
2652
- ${foreignTable}Id = created${ForeignType}.id;`);
2653
- cleanupStatements.push(`
2654
- // Clean up parent ${foreignTable} record
2655
- if (${foreignTable}Id) {
2679
+ const foreignTableName = fk.toTable;
2680
+ if (!foreignTablesData.has(foreignTableName)) {
2681
+ const foreignTable = model.tables[foreignTableName];
2682
+ if (!foreignTable)
2683
+ continue;
2684
+ foreignTablesData.set(foreignTableName, foreignTable);
2685
+ const ForeignType = pascal(foreignTableName);
2686
+ imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTableName}';`);
2687
+ if (foreignTable.pk.length === 1) {
2688
+ variables.push(`let ${foreignTableName}Id: string;`);
2689
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2690
+ setupStatements.push(`
2691
+ // Create parent ${foreignTableName} record for foreign key reference
2692
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2693
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2694
+ ${foreignTableName}Id = created${ForeignType}.${foreignTable.pk[0]};`);
2695
+ cleanupStatements.push(`
2696
+ // Clean up parent ${foreignTableName} record
2697
+ if (${foreignTableName}Id) {
2698
+ try {
2699
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2700
+ } catch (e) {
2701
+ // Parent might already be deleted due to cascading
2702
+ }
2703
+ }`);
2704
+ } else {
2705
+ variables.push(`let ${foreignTableName}Key: any;`);
2706
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2707
+ setupStatements.push(`
2708
+ // Create parent ${foreignTableName} record for foreign key reference
2709
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2710
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2711
+ ${foreignTableName}Key = { ${foreignTable.pk.map((pk) => `${pk}: created${ForeignType}.${pk}`).join(", ")} };`);
2712
+ cleanupStatements.push(`
2713
+ // Clean up parent ${foreignTableName} record
2714
+ if (${foreignTableName}Key) {
2656
2715
  try {
2657
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2716
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2658
2717
  } catch (e) {
2659
2718
  // Parent might already be deleted due to cascading
2660
2719
  }
2661
2720
  }`);
2721
+ }
2662
2722
  }
2663
2723
  }
2664
2724
  return {
@@ -2670,30 +2730,6 @@ function generateForeignKeySetup(table, clientPath) {
2670
2730
  cleanup: cleanupStatements.join("")
2671
2731
  };
2672
2732
  }
2673
- function generateMinimalDataForTable(tableName) {
2674
- if (tableName.includes("author")) {
2675
- return `{ name: 'Test Author' }`;
2676
- }
2677
- if (tableName.includes("book")) {
2678
- return `{ title: 'Test Book' }`;
2679
- }
2680
- if (tableName.includes("tag")) {
2681
- return `{ name: 'Test Tag' }`;
2682
- }
2683
- if (tableName.includes("user")) {
2684
- return `{ name: 'Test User', email: 'test@example.com' }`;
2685
- }
2686
- if (tableName.includes("category") || tableName.includes("categories")) {
2687
- return `{ name: 'Test Category' }`;
2688
- }
2689
- if (tableName.includes("product")) {
2690
- return `{ name: 'Test Product', price: 10.99 }`;
2691
- }
2692
- if (tableName.includes("order")) {
2693
- return `{ total: 100.00, status: 'pending' }`;
2694
- }
2695
- return `{ name: 'Test ${pascal(tableName)}' }`;
2696
- }
2697
2733
  function getTestCommand(framework, baseCommand) {
2698
2734
  switch (framework) {
2699
2735
  case "vitest":
@@ -2733,20 +2769,15 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2733
2769
  }
2734
2770
  for (const col of table.columns) {
2735
2771
  const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2736
- if (isAutoGenerated) {
2772
+ const isPrimaryKey = table.pk.includes(col.name);
2773
+ if (isPrimaryKey && col.hasDefault) {
2737
2774
  continue;
2738
2775
  }
2739
- if (col.name === "deleted_at" || col.name === "deleted") {
2740
- if (col.nullable) {
2741
- fields.push(` ${col.name}: null`);
2742
- } else {
2743
- const value = generateValueForColumn(col);
2744
- fields.push(` ${col.name}: ${value}`);
2745
- }
2776
+ if (isAutoGenerated) {
2746
2777
  continue;
2747
2778
  }
2748
2779
  const foreignTable = foreignKeyColumns.get(col.name);
2749
- if (!col.nullable || foreignTable || col.hasDefault && !isAutoGenerated || shouldIncludeNullableColumn(col)) {
2780
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2750
2781
  if (foreignTable) {
2751
2782
  fields.push(` ${col.name}: ${foreignTable}Id`);
2752
2783
  } else {
@@ -2763,51 +2794,40 @@ ${fields.join(`,
2763
2794
  function generateUpdateDataFromSchema(table) {
2764
2795
  const fields = [];
2765
2796
  for (const col of table.columns) {
2766
- if (table.pk.includes(col.name) || col.hasDefault) {
2767
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2768
- if (autoGenerated.includes(col.name.toLowerCase())) {
2769
- continue;
2770
- }
2797
+ if (table.pk.includes(col.name)) {
2798
+ continue;
2771
2799
  }
2772
- if (col.name.endsWith("_id")) {
2800
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2801
+ if (isAutoGenerated) {
2773
2802
  continue;
2774
2803
  }
2775
- if (col.name === "deleted_at" || col.name === "deleted") {
2804
+ if (col.name.endsWith("_id")) {
2776
2805
  continue;
2777
2806
  }
2778
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2807
+ if (!col.nullable) {
2779
2808
  const value = generateValueForColumn(col, true);
2780
2809
  fields.push(` ${col.name}: ${value}`);
2781
2810
  break;
2782
2811
  }
2783
2812
  }
2813
+ if (fields.length === 0) {
2814
+ for (const col of table.columns) {
2815
+ if (table.pk.includes(col.name) || col.name.endsWith("_id")) {
2816
+ continue;
2817
+ }
2818
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2819
+ if (!isAutoGenerated) {
2820
+ const value = generateValueForColumn(col, true);
2821
+ fields.push(` ${col.name}: ${value}`);
2822
+ break;
2823
+ }
2824
+ }
2825
+ }
2784
2826
  return fields.length > 0 ? `{
2785
2827
  ${fields.join(`,
2786
2828
  `)}
2787
2829
  }` : "{}";
2788
2830
  }
2789
- function shouldIncludeNullableColumn(col) {
2790
- const importantPatterns = [
2791
- "_id",
2792
- "_by",
2793
- "email",
2794
- "name",
2795
- "title",
2796
- "description",
2797
- "phone",
2798
- "address",
2799
- "status",
2800
- "type",
2801
- "category",
2802
- "price",
2803
- "amount",
2804
- "quantity",
2805
- "url",
2806
- "slug"
2807
- ];
2808
- const name = col.name.toLowerCase();
2809
- return importantPatterns.some((pattern) => name.includes(pattern));
2810
- }
2811
2831
  function generateValueForColumn(col, isUpdate = false) {
2812
2832
  const type = col.pgType.toLowerCase();
2813
2833
  switch (type) {
@@ -2841,15 +2861,17 @@ function generateValueForColumn(col, isUpdate = false) {
2841
2861
  case "bool":
2842
2862
  return isUpdate ? "false" : "true";
2843
2863
  case "date":
2864
+ return `'2024-01-01'`;
2844
2865
  case "timestamp":
2845
2866
  case "timestamptz":
2846
2867
  case "timestamp without time zone":
2847
2868
  case "timestamp with time zone":
2869
+ return `'2024-01-01T00:00:00.000Z'`;
2848
2870
  case "time":
2849
2871
  case "timetz":
2850
2872
  case "time without time zone":
2851
2873
  case "time with time zone":
2852
- return `new Date()`;
2874
+ return `'12:00:00'`;
2853
2875
  case "interval":
2854
2876
  return `'1 day'`;
2855
2877
  case "json":
@@ -2917,26 +2939,14 @@ function generateUUID() {
2917
2939
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2918
2940
  const Type = pascal(table.name);
2919
2941
  const hasData = sampleData !== "{}";
2920
- const isJunctionTable = table.pk.length > 1 && table.columns.every((col) => table.pk.includes(col.name) || col.name.endsWith("_id"));
2921
- if (isJunctionTable) {
2922
- return `it('should create a ${table.name} relationship', async () => {
2923
- // This is a junction table for M:N relationships
2924
- // Test data depends on parent records created in other tests
2925
- expect(true).toBe(true);
2926
- });
2927
-
2928
- it('should list ${table.name} relationships', async () => {
2929
- const list = await sdk.${table.name}.list({ limit: 10 });
2930
- expect(Array.isArray(list)).toBe(true);
2931
- });`;
2932
- }
2942
+ const hasSinglePK = table.pk.length === 1;
2933
2943
  return `it('should create a ${table.name}', async () => {
2934
2944
  const data: Insert${Type} = ${sampleData};
2935
2945
  ${hasData ? `
2936
2946
  const created = await sdk.${table.name}.create(data);
2937
2947
  expect(created).toBeDefined();
2938
- expect(created.id).toBeDefined();
2939
- createdId = created.id;
2948
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2949
+ createdId = created.${table.pk[0]};` : ""}
2940
2950
  ` : `
2941
2951
  // Table has only auto-generated columns
2942
2952
  // Skip create test or add your own test data
@@ -2949,7 +2959,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2949
2959
  expect(Array.isArray(list)).toBe(true);
2950
2960
  });
2951
2961
 
2952
- ${hasData ? `it('should get ${table.name} by id', async () => {
2962
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2953
2963
  if (!createdId) {
2954
2964
  console.warn('No ID from create test, skipping get test');
2955
2965
  return;
@@ -2957,7 +2967,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2957
2967
 
2958
2968
  const item = await sdk.${table.name}.getByPk(createdId);
2959
2969
  expect(item).toBeDefined();
2960
- expect(item?.id).toBe(createdId);
2970
+ expect(item?.${table.pk[0]}).toBe(createdId);
2961
2971
  });
2962
2972
 
2963
2973
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3315,7 +3325,6 @@ async function generate(configPath) {
3315
3325
  if (sameDirectory) {
3316
3326
  clientDir = join(originalClientDir, "sdk");
3317
3327
  }
3318
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3319
3328
  const serverFramework = cfg.serverFramework || "hono";
3320
3329
  const generateTests = cfg.tests?.generate ?? false;
3321
3330
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3359,12 +3368,12 @@ async function generate(configPath) {
3359
3368
  content: emitCoreOperations()
3360
3369
  });
3361
3370
  for (const table of Object.values(model.tables)) {
3362
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3371
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3363
3372
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3364
3373
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3365
3374
  files.push({
3366
3375
  path: join(serverDir, "zod", `${table.name}.ts`),
3367
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3376
+ content: emitZod(table, { numericMode: "string" })
3368
3377
  });
3369
3378
  let routeContent;
3370
3379
  if (serverFramework === "hono") {
@@ -3435,7 +3444,7 @@ async function generate(configPath) {
3435
3444
  for (const table of Object.values(model.tables)) {
3436
3445
  files.push({
3437
3446
  path: join(testDir, `${table.name}.test.ts`),
3438
- content: emitTableTest(table, relativeClientPath, testFramework)
3447
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3439
3448
  });
3440
3449
  }
3441
3450
  }
@@ -1,8 +1,8 @@
1
- import type { Table } from "./introspect";
1
+ import type { Table, Model } from "./introspect";
2
2
  /**
3
3
  * Generate basic SDK tests for a table
4
4
  */
5
- export declare function emitTableTest(table: Table, clientPath: string, framework?: "vitest" | "jest" | "bun"): string;
5
+ export declare function emitTableTest(table: Table, model: Model, clientPath: string, framework?: "vitest" | "jest" | "bun"): string;
6
6
  /**
7
7
  * Generate a test setup file
8
8
  */
@@ -1,5 +1,4 @@
1
1
  import type { Table } from "./introspect";
2
2
  export declare function emitTypes(table: Table, opts: {
3
- dateType: "date" | "string";
4
3
  numericMode: "string" | "number";
5
4
  }): string;
@@ -1,5 +1,4 @@
1
1
  import type { Table } from "./introspect";
2
2
  export declare function emitZod(table: Table, opts: {
3
- dateType: "date" | "string";
4
3
  numericMode: "string" | "number";
5
4
  }): string;
package/dist/index.js CHANGED
@@ -745,17 +745,18 @@ function emitZod(table, opts) {
745
745
  if (pg === "jsonb" || pg === "json")
746
746
  return `z.unknown()`;
747
747
  if (pg === "date" || pg.startsWith("timestamp"))
748
- return opts.dateType === "date" ? `z.date()` : `z.string()`;
748
+ return `z.string()`;
749
749
  if (pg.startsWith("_"))
750
750
  return `z.array(${zFor(pg.slice(1))})`;
751
751
  return `z.string()`;
752
752
  };
753
753
  const fields = table.columns.map((c) => {
754
754
  let z = zFor(c.pgType);
755
- if (c.nullable)
756
- z += `.nullable()`;
757
- if (c.hasDefault)
755
+ if (c.nullable) {
756
+ z += `.nullish()`;
757
+ } else if (c.hasDefault) {
758
758
  z += `.optional()`;
759
+ }
759
760
  return ` ${c.name}: ${z}`;
760
761
  }).join(`,
761
762
  `);
@@ -1494,7 +1495,7 @@ function tsTypeFor(pgType, opts) {
1494
1495
  return opts.numericMode === "number" ? "number" : "string";
1495
1496
  }
1496
1497
  if (t === "date" || t.startsWith("timestamp"))
1497
- return opts.dateType === "date" ? "Date" : "string";
1498
+ return "string";
1498
1499
  if (t === "json" || t === "jsonb")
1499
1500
  return "unknown";
1500
1501
  return "string";
@@ -2094,14 +2095,57 @@ export async function deleteRecord(
2094
2095
  }
2095
2096
 
2096
2097
  // src/emit-tests.ts
2097
- function emitTableTest(table, clientPath, framework = "vitest") {
2098
+ function emitTableTest(table, model, clientPath, framework = "vitest") {
2098
2099
  const Type = pascal(table.name);
2099
2100
  const tableName = table.name;
2100
2101
  const imports = getFrameworkImports(framework);
2102
+ const isJunctionTable = table.pk.length > 1;
2101
2103
  const hasForeignKeys = table.fks.length > 0;
2102
- const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, clientPath) : null;
2104
+ const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, model, clientPath) : null;
2103
2105
  const sampleData = generateSampleDataFromSchema(table, hasForeignKeys);
2104
2106
  const updateData = generateUpdateDataFromSchema(table);
2107
+ if (isJunctionTable) {
2108
+ return `${imports}
2109
+ import { SDK } from '${clientPath}';
2110
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
2111
+ ${foreignKeySetup?.imports || ""}
2112
+
2113
+ /**
2114
+ * Basic tests for ${tableName} table operations
2115
+ *
2116
+ * This is a junction table with composite primary key.
2117
+ * Tests are simplified since it represents a many-to-many relationship.
2118
+ */
2119
+ describe('${Type} SDK Operations', () => {
2120
+ let sdk: SDK;
2121
+ ${foreignKeySetup?.variables || ""}
2122
+
2123
+ beforeAll(async () => {
2124
+ sdk = new SDK({
2125
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2126
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2127
+ });
2128
+ ${foreignKeySetup?.setup || ""}
2129
+ });
2130
+
2131
+ ${foreignKeySetup?.cleanup ? `afterAll(async () => {
2132
+ ${foreignKeySetup.cleanup}
2133
+ });
2134
+
2135
+ ` : ""}it('should create a ${tableName} relationship', async () => {
2136
+ const data: Insert${Type} = ${sampleData};
2137
+
2138
+ const created = await sdk.${tableName}.create(data);
2139
+ expect(created).toBeDefined();
2140
+ });
2141
+
2142
+ it('should list ${tableName} relationships', async () => {
2143
+ const list = await sdk.${tableName}.list({ limit: 10 });
2144
+ expect(Array.isArray(list)).toBe(true);
2145
+ });
2146
+ });
2147
+ `;
2148
+ }
2105
2149
  return `${imports}
2106
2150
  import { SDK } from '${clientPath}';
2107
2151
  import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
@@ -2362,33 +2406,56 @@ echo " - To reset the database: docker-compose -f $SCRIPT_DIR/docker-compose.ym
2362
2406
  exit $TEST_EXIT_CODE
2363
2407
  `;
2364
2408
  }
2365
- function generateForeignKeySetup(table, clientPath) {
2409
+ function generateForeignKeySetup(table, model, clientPath) {
2366
2410
  const imports = [];
2367
2411
  const variables = [];
2368
2412
  const setupStatements = [];
2369
2413
  const cleanupStatements = [];
2370
- const foreignTables = new Set;
2414
+ const foreignTablesData = new Map;
2371
2415
  for (const fk of table.fks) {
2372
- const foreignTable = fk.toTable;
2373
- if (!foreignTables.has(foreignTable)) {
2374
- foreignTables.add(foreignTable);
2375
- const ForeignType = pascal(foreignTable);
2376
- imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTable}';`);
2377
- variables.push(`let ${foreignTable}Id: string;`);
2378
- setupStatements.push(`
2379
- // Create parent ${foreignTable} record for foreign key reference
2380
- const ${foreignTable}Data: Insert${ForeignType} = ${generateMinimalDataForTable(foreignTable)};
2381
- const created${ForeignType} = await sdk.${foreignTable}.create(${foreignTable}Data);
2382
- ${foreignTable}Id = created${ForeignType}.id;`);
2383
- cleanupStatements.push(`
2384
- // Clean up parent ${foreignTable} record
2385
- if (${foreignTable}Id) {
2416
+ const foreignTableName = fk.toTable;
2417
+ if (!foreignTablesData.has(foreignTableName)) {
2418
+ const foreignTable = model.tables[foreignTableName];
2419
+ if (!foreignTable)
2420
+ continue;
2421
+ foreignTablesData.set(foreignTableName, foreignTable);
2422
+ const ForeignType = pascal(foreignTableName);
2423
+ imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTableName}';`);
2424
+ if (foreignTable.pk.length === 1) {
2425
+ variables.push(`let ${foreignTableName}Id: string;`);
2426
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2427
+ setupStatements.push(`
2428
+ // Create parent ${foreignTableName} record for foreign key reference
2429
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2430
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2431
+ ${foreignTableName}Id = created${ForeignType}.${foreignTable.pk[0]};`);
2432
+ cleanupStatements.push(`
2433
+ // Clean up parent ${foreignTableName} record
2434
+ if (${foreignTableName}Id) {
2435
+ try {
2436
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2437
+ } catch (e) {
2438
+ // Parent might already be deleted due to cascading
2439
+ }
2440
+ }`);
2441
+ } else {
2442
+ variables.push(`let ${foreignTableName}Key: any;`);
2443
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2444
+ setupStatements.push(`
2445
+ // Create parent ${foreignTableName} record for foreign key reference
2446
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2447
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2448
+ ${foreignTableName}Key = { ${foreignTable.pk.map((pk) => `${pk}: created${ForeignType}.${pk}`).join(", ")} };`);
2449
+ cleanupStatements.push(`
2450
+ // Clean up parent ${foreignTableName} record
2451
+ if (${foreignTableName}Key) {
2386
2452
  try {
2387
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2453
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2388
2454
  } catch (e) {
2389
2455
  // Parent might already be deleted due to cascading
2390
2456
  }
2391
2457
  }`);
2458
+ }
2392
2459
  }
2393
2460
  }
2394
2461
  return {
@@ -2400,30 +2467,6 @@ function generateForeignKeySetup(table, clientPath) {
2400
2467
  cleanup: cleanupStatements.join("")
2401
2468
  };
2402
2469
  }
2403
- function generateMinimalDataForTable(tableName) {
2404
- if (tableName.includes("author")) {
2405
- return `{ name: 'Test Author' }`;
2406
- }
2407
- if (tableName.includes("book")) {
2408
- return `{ title: 'Test Book' }`;
2409
- }
2410
- if (tableName.includes("tag")) {
2411
- return `{ name: 'Test Tag' }`;
2412
- }
2413
- if (tableName.includes("user")) {
2414
- return `{ name: 'Test User', email: 'test@example.com' }`;
2415
- }
2416
- if (tableName.includes("category") || tableName.includes("categories")) {
2417
- return `{ name: 'Test Category' }`;
2418
- }
2419
- if (tableName.includes("product")) {
2420
- return `{ name: 'Test Product', price: 10.99 }`;
2421
- }
2422
- if (tableName.includes("order")) {
2423
- return `{ total: 100.00, status: 'pending' }`;
2424
- }
2425
- return `{ name: 'Test ${pascal(tableName)}' }`;
2426
- }
2427
2470
  function getTestCommand(framework, baseCommand) {
2428
2471
  switch (framework) {
2429
2472
  case "vitest":
@@ -2463,20 +2506,15 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2463
2506
  }
2464
2507
  for (const col of table.columns) {
2465
2508
  const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2466
- if (isAutoGenerated) {
2509
+ const isPrimaryKey = table.pk.includes(col.name);
2510
+ if (isPrimaryKey && col.hasDefault) {
2467
2511
  continue;
2468
2512
  }
2469
- if (col.name === "deleted_at" || col.name === "deleted") {
2470
- if (col.nullable) {
2471
- fields.push(` ${col.name}: null`);
2472
- } else {
2473
- const value = generateValueForColumn(col);
2474
- fields.push(` ${col.name}: ${value}`);
2475
- }
2513
+ if (isAutoGenerated) {
2476
2514
  continue;
2477
2515
  }
2478
2516
  const foreignTable = foreignKeyColumns.get(col.name);
2479
- if (!col.nullable || foreignTable || col.hasDefault && !isAutoGenerated || shouldIncludeNullableColumn(col)) {
2517
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2480
2518
  if (foreignTable) {
2481
2519
  fields.push(` ${col.name}: ${foreignTable}Id`);
2482
2520
  } else {
@@ -2493,51 +2531,40 @@ ${fields.join(`,
2493
2531
  function generateUpdateDataFromSchema(table) {
2494
2532
  const fields = [];
2495
2533
  for (const col of table.columns) {
2496
- if (table.pk.includes(col.name) || col.hasDefault) {
2497
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2498
- if (autoGenerated.includes(col.name.toLowerCase())) {
2499
- continue;
2500
- }
2534
+ if (table.pk.includes(col.name)) {
2535
+ continue;
2501
2536
  }
2502
- if (col.name.endsWith("_id")) {
2537
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2538
+ if (isAutoGenerated) {
2503
2539
  continue;
2504
2540
  }
2505
- if (col.name === "deleted_at" || col.name === "deleted") {
2541
+ if (col.name.endsWith("_id")) {
2506
2542
  continue;
2507
2543
  }
2508
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2544
+ if (!col.nullable) {
2509
2545
  const value = generateValueForColumn(col, true);
2510
2546
  fields.push(` ${col.name}: ${value}`);
2511
2547
  break;
2512
2548
  }
2513
2549
  }
2550
+ if (fields.length === 0) {
2551
+ for (const col of table.columns) {
2552
+ if (table.pk.includes(col.name) || col.name.endsWith("_id")) {
2553
+ continue;
2554
+ }
2555
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2556
+ if (!isAutoGenerated) {
2557
+ const value = generateValueForColumn(col, true);
2558
+ fields.push(` ${col.name}: ${value}`);
2559
+ break;
2560
+ }
2561
+ }
2562
+ }
2514
2563
  return fields.length > 0 ? `{
2515
2564
  ${fields.join(`,
2516
2565
  `)}
2517
2566
  }` : "{}";
2518
2567
  }
2519
- function shouldIncludeNullableColumn(col) {
2520
- const importantPatterns = [
2521
- "_id",
2522
- "_by",
2523
- "email",
2524
- "name",
2525
- "title",
2526
- "description",
2527
- "phone",
2528
- "address",
2529
- "status",
2530
- "type",
2531
- "category",
2532
- "price",
2533
- "amount",
2534
- "quantity",
2535
- "url",
2536
- "slug"
2537
- ];
2538
- const name = col.name.toLowerCase();
2539
- return importantPatterns.some((pattern) => name.includes(pattern));
2540
- }
2541
2568
  function generateValueForColumn(col, isUpdate = false) {
2542
2569
  const type = col.pgType.toLowerCase();
2543
2570
  switch (type) {
@@ -2571,15 +2598,17 @@ function generateValueForColumn(col, isUpdate = false) {
2571
2598
  case "bool":
2572
2599
  return isUpdate ? "false" : "true";
2573
2600
  case "date":
2601
+ return `'2024-01-01'`;
2574
2602
  case "timestamp":
2575
2603
  case "timestamptz":
2576
2604
  case "timestamp without time zone":
2577
2605
  case "timestamp with time zone":
2606
+ return `'2024-01-01T00:00:00.000Z'`;
2578
2607
  case "time":
2579
2608
  case "timetz":
2580
2609
  case "time without time zone":
2581
2610
  case "time with time zone":
2582
- return `new Date()`;
2611
+ return `'12:00:00'`;
2583
2612
  case "interval":
2584
2613
  return `'1 day'`;
2585
2614
  case "json":
@@ -2647,26 +2676,14 @@ function generateUUID() {
2647
2676
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2648
2677
  const Type = pascal(table.name);
2649
2678
  const hasData = sampleData !== "{}";
2650
- const isJunctionTable = table.pk.length > 1 && table.columns.every((col) => table.pk.includes(col.name) || col.name.endsWith("_id"));
2651
- if (isJunctionTable) {
2652
- return `it('should create a ${table.name} relationship', async () => {
2653
- // This is a junction table for M:N relationships
2654
- // Test data depends on parent records created in other tests
2655
- expect(true).toBe(true);
2656
- });
2657
-
2658
- it('should list ${table.name} relationships', async () => {
2659
- const list = await sdk.${table.name}.list({ limit: 10 });
2660
- expect(Array.isArray(list)).toBe(true);
2661
- });`;
2662
- }
2679
+ const hasSinglePK = table.pk.length === 1;
2663
2680
  return `it('should create a ${table.name}', async () => {
2664
2681
  const data: Insert${Type} = ${sampleData};
2665
2682
  ${hasData ? `
2666
2683
  const created = await sdk.${table.name}.create(data);
2667
2684
  expect(created).toBeDefined();
2668
- expect(created.id).toBeDefined();
2669
- createdId = created.id;
2685
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2686
+ createdId = created.${table.pk[0]};` : ""}
2670
2687
  ` : `
2671
2688
  // Table has only auto-generated columns
2672
2689
  // Skip create test or add your own test data
@@ -2679,7 +2696,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2679
2696
  expect(Array.isArray(list)).toBe(true);
2680
2697
  });
2681
2698
 
2682
- ${hasData ? `it('should get ${table.name} by id', async () => {
2699
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2683
2700
  if (!createdId) {
2684
2701
  console.warn('No ID from create test, skipping get test');
2685
2702
  return;
@@ -2687,7 +2704,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2687
2704
 
2688
2705
  const item = await sdk.${table.name}.getByPk(createdId);
2689
2706
  expect(item).toBeDefined();
2690
- expect(item?.id).toBe(createdId);
2707
+ expect(item?.${table.pk[0]}).toBe(createdId);
2691
2708
  });
2692
2709
 
2693
2710
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3045,7 +3062,6 @@ async function generate(configPath) {
3045
3062
  if (sameDirectory) {
3046
3063
  clientDir = join(originalClientDir, "sdk");
3047
3064
  }
3048
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3049
3065
  const serverFramework = cfg.serverFramework || "hono";
3050
3066
  const generateTests = cfg.tests?.generate ?? false;
3051
3067
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3089,12 +3105,12 @@ async function generate(configPath) {
3089
3105
  content: emitCoreOperations()
3090
3106
  });
3091
3107
  for (const table of Object.values(model.tables)) {
3092
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3108
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3093
3109
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3094
3110
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3095
3111
  files.push({
3096
3112
  path: join(serverDir, "zod", `${table.name}.ts`),
3097
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3113
+ content: emitZod(table, { numericMode: "string" })
3098
3114
  });
3099
3115
  let routeContent;
3100
3116
  if (serverFramework === "hono") {
@@ -3165,7 +3181,7 @@ async function generate(configPath) {
3165
3181
  for (const table of Object.values(model.tables)) {
3166
3182
  files.push({
3167
3183
  path: join(testDir, `${table.name}.test.ts`),
3168
- content: emitTableTest(table, relativeClientPath, testFramework)
3184
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3169
3185
  });
3170
3186
  }
3171
3187
  }
package/dist/types.d.ts CHANGED
@@ -25,7 +25,6 @@ export interface Config {
25
25
  outClient?: string;
26
26
  softDeleteColumn?: string | null;
27
27
  includeDepthLimit?: number;
28
- dateType?: "date" | "string";
29
28
  serverFramework?: "hono" | "express" | "fastify";
30
29
  auth?: AuthConfigInput;
31
30
  pull?: PullConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.6.8",
3
+ "version": "0.6.11",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {