postgresdk 0.6.8 → 0.6.10

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,7 +1008,7 @@ 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()`;
@@ -1764,7 +1757,7 @@ function tsTypeFor(pgType, opts) {
1764
1757
  return opts.numericMode === "number" ? "number" : "string";
1765
1758
  }
1766
1759
  if (t === "date" || t.startsWith("timestamp"))
1767
- return opts.dateType === "date" ? "Date" : "string";
1760
+ return "string";
1768
1761
  if (t === "json" || t === "jsonb")
1769
1762
  return "unknown";
1770
1763
  return "string";
@@ -2364,14 +2357,57 @@ export async function deleteRecord(
2364
2357
  }
2365
2358
 
2366
2359
  // src/emit-tests.ts
2367
- function emitTableTest(table, clientPath, framework = "vitest") {
2360
+ function emitTableTest(table, model, clientPath, framework = "vitest") {
2368
2361
  const Type = pascal(table.name);
2369
2362
  const tableName = table.name;
2370
2363
  const imports = getFrameworkImports(framework);
2364
+ const isJunctionTable = table.pk.length > 1;
2371
2365
  const hasForeignKeys = table.fks.length > 0;
2372
- const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, clientPath) : null;
2366
+ const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, model, clientPath) : null;
2373
2367
  const sampleData = generateSampleDataFromSchema(table, hasForeignKeys);
2374
2368
  const updateData = generateUpdateDataFromSchema(table);
2369
+ if (isJunctionTable) {
2370
+ return `${imports}
2371
+ import { SDK } from '${clientPath}';
2372
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
2373
+ ${foreignKeySetup?.imports || ""}
2374
+
2375
+ /**
2376
+ * Basic tests for ${tableName} table operations
2377
+ *
2378
+ * This is a junction table with composite primary key.
2379
+ * Tests are simplified since it represents a many-to-many relationship.
2380
+ */
2381
+ describe('${Type} SDK Operations', () => {
2382
+ let sdk: SDK;
2383
+ ${foreignKeySetup?.variables || ""}
2384
+
2385
+ beforeAll(async () => {
2386
+ sdk = new SDK({
2387
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2388
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2389
+ });
2390
+ ${foreignKeySetup?.setup || ""}
2391
+ });
2392
+
2393
+ ${foreignKeySetup?.cleanup ? `afterAll(async () => {
2394
+ ${foreignKeySetup.cleanup}
2395
+ });
2396
+
2397
+ ` : ""}it('should create a ${tableName} relationship', async () => {
2398
+ const data: Insert${Type} = ${sampleData};
2399
+
2400
+ const created = await sdk.${tableName}.create(data);
2401
+ expect(created).toBeDefined();
2402
+ });
2403
+
2404
+ it('should list ${tableName} relationships', async () => {
2405
+ const list = await sdk.${tableName}.list({ limit: 10 });
2406
+ expect(Array.isArray(list)).toBe(true);
2407
+ });
2408
+ });
2409
+ `;
2410
+ }
2375
2411
  return `${imports}
2376
2412
  import { SDK } from '${clientPath}';
2377
2413
  import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
@@ -2632,33 +2668,56 @@ echo " - To reset the database: docker-compose -f $SCRIPT_DIR/docker-compose.ym
2632
2668
  exit $TEST_EXIT_CODE
2633
2669
  `;
2634
2670
  }
2635
- function generateForeignKeySetup(table, clientPath) {
2671
+ function generateForeignKeySetup(table, model, clientPath) {
2636
2672
  const imports = [];
2637
2673
  const variables = [];
2638
2674
  const setupStatements = [];
2639
2675
  const cleanupStatements = [];
2640
- const foreignTables = new Set;
2676
+ const foreignTablesData = new Map;
2641
2677
  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) {
2678
+ const foreignTableName = fk.toTable;
2679
+ if (!foreignTablesData.has(foreignTableName)) {
2680
+ const foreignTable = model.tables[foreignTableName];
2681
+ if (!foreignTable)
2682
+ continue;
2683
+ foreignTablesData.set(foreignTableName, foreignTable);
2684
+ const ForeignType = pascal(foreignTableName);
2685
+ imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTableName}';`);
2686
+ if (foreignTable.pk.length === 1) {
2687
+ variables.push(`let ${foreignTableName}Id: string;`);
2688
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2689
+ setupStatements.push(`
2690
+ // Create parent ${foreignTableName} record for foreign key reference
2691
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2692
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2693
+ ${foreignTableName}Id = created${ForeignType}.${foreignTable.pk[0]};`);
2694
+ cleanupStatements.push(`
2695
+ // Clean up parent ${foreignTableName} record
2696
+ if (${foreignTableName}Id) {
2656
2697
  try {
2657
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2698
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2658
2699
  } catch (e) {
2659
2700
  // Parent might already be deleted due to cascading
2660
2701
  }
2661
2702
  }`);
2703
+ } else {
2704
+ variables.push(`let ${foreignTableName}Key: any;`);
2705
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2706
+ setupStatements.push(`
2707
+ // Create parent ${foreignTableName} record for foreign key reference
2708
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2709
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2710
+ ${foreignTableName}Key = { ${foreignTable.pk.map((pk) => `${pk}: created${ForeignType}.${pk}`).join(", ")} };`);
2711
+ cleanupStatements.push(`
2712
+ // Clean up parent ${foreignTableName} record
2713
+ if (${foreignTableName}Key) {
2714
+ try {
2715
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2716
+ } catch (e) {
2717
+ // Parent might already be deleted due to cascading
2718
+ }
2719
+ }`);
2720
+ }
2662
2721
  }
2663
2722
  }
2664
2723
  return {
@@ -2670,30 +2729,6 @@ function generateForeignKeySetup(table, clientPath) {
2670
2729
  cleanup: cleanupStatements.join("")
2671
2730
  };
2672
2731
  }
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
2732
  function getTestCommand(framework, baseCommand) {
2698
2733
  switch (framework) {
2699
2734
  case "vitest":
@@ -2733,20 +2768,15 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2733
2768
  }
2734
2769
  for (const col of table.columns) {
2735
2770
  const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2736
- if (isAutoGenerated) {
2771
+ const isPrimaryKey = table.pk.includes(col.name);
2772
+ if (isPrimaryKey && col.hasDefault) {
2737
2773
  continue;
2738
2774
  }
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
- }
2775
+ if (isAutoGenerated) {
2746
2776
  continue;
2747
2777
  }
2748
2778
  const foreignTable = foreignKeyColumns.get(col.name);
2749
- if (!col.nullable || foreignTable || col.hasDefault && !isAutoGenerated || shouldIncludeNullableColumn(col)) {
2779
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2750
2780
  if (foreignTable) {
2751
2781
  fields.push(` ${col.name}: ${foreignTable}Id`);
2752
2782
  } else {
@@ -2763,51 +2793,40 @@ ${fields.join(`,
2763
2793
  function generateUpdateDataFromSchema(table) {
2764
2794
  const fields = [];
2765
2795
  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
- }
2796
+ if (table.pk.includes(col.name)) {
2797
+ continue;
2771
2798
  }
2772
- if (col.name.endsWith("_id")) {
2799
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2800
+ if (isAutoGenerated) {
2773
2801
  continue;
2774
2802
  }
2775
- if (col.name === "deleted_at" || col.name === "deleted") {
2803
+ if (col.name.endsWith("_id")) {
2776
2804
  continue;
2777
2805
  }
2778
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2806
+ if (!col.nullable) {
2779
2807
  const value = generateValueForColumn(col, true);
2780
2808
  fields.push(` ${col.name}: ${value}`);
2781
2809
  break;
2782
2810
  }
2783
2811
  }
2812
+ if (fields.length === 0) {
2813
+ for (const col of table.columns) {
2814
+ if (table.pk.includes(col.name) || col.name.endsWith("_id")) {
2815
+ continue;
2816
+ }
2817
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2818
+ if (!isAutoGenerated) {
2819
+ const value = generateValueForColumn(col, true);
2820
+ fields.push(` ${col.name}: ${value}`);
2821
+ break;
2822
+ }
2823
+ }
2824
+ }
2784
2825
  return fields.length > 0 ? `{
2785
2826
  ${fields.join(`,
2786
2827
  `)}
2787
2828
  }` : "{}";
2788
2829
  }
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
2830
  function generateValueForColumn(col, isUpdate = false) {
2812
2831
  const type = col.pgType.toLowerCase();
2813
2832
  switch (type) {
@@ -2841,15 +2860,17 @@ function generateValueForColumn(col, isUpdate = false) {
2841
2860
  case "bool":
2842
2861
  return isUpdate ? "false" : "true";
2843
2862
  case "date":
2863
+ return `'2024-01-01'`;
2844
2864
  case "timestamp":
2845
2865
  case "timestamptz":
2846
2866
  case "timestamp without time zone":
2847
2867
  case "timestamp with time zone":
2868
+ return `'2024-01-01T00:00:00.000Z'`;
2848
2869
  case "time":
2849
2870
  case "timetz":
2850
2871
  case "time without time zone":
2851
2872
  case "time with time zone":
2852
- return `new Date()`;
2873
+ return `'12:00:00'`;
2853
2874
  case "interval":
2854
2875
  return `'1 day'`;
2855
2876
  case "json":
@@ -2917,26 +2938,14 @@ function generateUUID() {
2917
2938
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2918
2939
  const Type = pascal(table.name);
2919
2940
  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
- }
2941
+ const hasSinglePK = table.pk.length === 1;
2933
2942
  return `it('should create a ${table.name}', async () => {
2934
2943
  const data: Insert${Type} = ${sampleData};
2935
2944
  ${hasData ? `
2936
2945
  const created = await sdk.${table.name}.create(data);
2937
2946
  expect(created).toBeDefined();
2938
- expect(created.id).toBeDefined();
2939
- createdId = created.id;
2947
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2948
+ createdId = created.${table.pk[0]};` : ""}
2940
2949
  ` : `
2941
2950
  // Table has only auto-generated columns
2942
2951
  // Skip create test or add your own test data
@@ -2949,7 +2958,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2949
2958
  expect(Array.isArray(list)).toBe(true);
2950
2959
  });
2951
2960
 
2952
- ${hasData ? `it('should get ${table.name} by id', async () => {
2961
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2953
2962
  if (!createdId) {
2954
2963
  console.warn('No ID from create test, skipping get test');
2955
2964
  return;
@@ -2957,7 +2966,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2957
2966
 
2958
2967
  const item = await sdk.${table.name}.getByPk(createdId);
2959
2968
  expect(item).toBeDefined();
2960
- expect(item?.id).toBe(createdId);
2969
+ expect(item?.${table.pk[0]}).toBe(createdId);
2961
2970
  });
2962
2971
 
2963
2972
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3315,7 +3324,6 @@ async function generate(configPath) {
3315
3324
  if (sameDirectory) {
3316
3325
  clientDir = join(originalClientDir, "sdk");
3317
3326
  }
3318
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3319
3327
  const serverFramework = cfg.serverFramework || "hono";
3320
3328
  const generateTests = cfg.tests?.generate ?? false;
3321
3329
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3359,12 +3367,12 @@ async function generate(configPath) {
3359
3367
  content: emitCoreOperations()
3360
3368
  });
3361
3369
  for (const table of Object.values(model.tables)) {
3362
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3370
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3363
3371
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3364
3372
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3365
3373
  files.push({
3366
3374
  path: join(serverDir, "zod", `${table.name}.ts`),
3367
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3375
+ content: emitZod(table, { numericMode: "string" })
3368
3376
  });
3369
3377
  let routeContent;
3370
3378
  if (serverFramework === "hono") {
@@ -3435,7 +3443,7 @@ async function generate(configPath) {
3435
3443
  for (const table of Object.values(model.tables)) {
3436
3444
  files.push({
3437
3445
  path: join(testDir, `${table.name}.test.ts`),
3438
- content: emitTableTest(table, relativeClientPath, testFramework)
3446
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3439
3447
  });
3440
3448
  }
3441
3449
  }
@@ -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,7 +745,7 @@ 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()`;
@@ -1494,7 +1494,7 @@ function tsTypeFor(pgType, opts) {
1494
1494
  return opts.numericMode === "number" ? "number" : "string";
1495
1495
  }
1496
1496
  if (t === "date" || t.startsWith("timestamp"))
1497
- return opts.dateType === "date" ? "Date" : "string";
1497
+ return "string";
1498
1498
  if (t === "json" || t === "jsonb")
1499
1499
  return "unknown";
1500
1500
  return "string";
@@ -2094,14 +2094,57 @@ export async function deleteRecord(
2094
2094
  }
2095
2095
 
2096
2096
  // src/emit-tests.ts
2097
- function emitTableTest(table, clientPath, framework = "vitest") {
2097
+ function emitTableTest(table, model, clientPath, framework = "vitest") {
2098
2098
  const Type = pascal(table.name);
2099
2099
  const tableName = table.name;
2100
2100
  const imports = getFrameworkImports(framework);
2101
+ const isJunctionTable = table.pk.length > 1;
2101
2102
  const hasForeignKeys = table.fks.length > 0;
2102
- const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, clientPath) : null;
2103
+ const foreignKeySetup = hasForeignKeys ? generateForeignKeySetup(table, model, clientPath) : null;
2103
2104
  const sampleData = generateSampleDataFromSchema(table, hasForeignKeys);
2104
2105
  const updateData = generateUpdateDataFromSchema(table);
2106
+ if (isJunctionTable) {
2107
+ return `${imports}
2108
+ import { SDK } from '${clientPath}';
2109
+ import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
2110
+ ${foreignKeySetup?.imports || ""}
2111
+
2112
+ /**
2113
+ * Basic tests for ${tableName} table operations
2114
+ *
2115
+ * This is a junction table with composite primary key.
2116
+ * Tests are simplified since it represents a many-to-many relationship.
2117
+ */
2118
+ describe('${Type} SDK Operations', () => {
2119
+ let sdk: SDK;
2120
+ ${foreignKeySetup?.variables || ""}
2121
+
2122
+ beforeAll(async () => {
2123
+ sdk = new SDK({
2124
+ baseUrl: process.env.API_URL || 'http://localhost:3000',
2125
+ auth: process.env.API_KEY ? { apiKey: process.env.API_KEY } : undefined
2126
+ });
2127
+ ${foreignKeySetup?.setup || ""}
2128
+ });
2129
+
2130
+ ${foreignKeySetup?.cleanup ? `afterAll(async () => {
2131
+ ${foreignKeySetup.cleanup}
2132
+ });
2133
+
2134
+ ` : ""}it('should create a ${tableName} relationship', async () => {
2135
+ const data: Insert${Type} = ${sampleData};
2136
+
2137
+ const created = await sdk.${tableName}.create(data);
2138
+ expect(created).toBeDefined();
2139
+ });
2140
+
2141
+ it('should list ${tableName} relationships', async () => {
2142
+ const list = await sdk.${tableName}.list({ limit: 10 });
2143
+ expect(Array.isArray(list)).toBe(true);
2144
+ });
2145
+ });
2146
+ `;
2147
+ }
2105
2148
  return `${imports}
2106
2149
  import { SDK } from '${clientPath}';
2107
2150
  import type { Insert${Type}, Update${Type}, Select${Type} } from '${clientPath}/types/${tableName}';
@@ -2362,33 +2405,56 @@ echo " - To reset the database: docker-compose -f $SCRIPT_DIR/docker-compose.ym
2362
2405
  exit $TEST_EXIT_CODE
2363
2406
  `;
2364
2407
  }
2365
- function generateForeignKeySetup(table, clientPath) {
2408
+ function generateForeignKeySetup(table, model, clientPath) {
2366
2409
  const imports = [];
2367
2410
  const variables = [];
2368
2411
  const setupStatements = [];
2369
2412
  const cleanupStatements = [];
2370
- const foreignTables = new Set;
2413
+ const foreignTablesData = new Map;
2371
2414
  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) {
2415
+ const foreignTableName = fk.toTable;
2416
+ if (!foreignTablesData.has(foreignTableName)) {
2417
+ const foreignTable = model.tables[foreignTableName];
2418
+ if (!foreignTable)
2419
+ continue;
2420
+ foreignTablesData.set(foreignTableName, foreignTable);
2421
+ const ForeignType = pascal(foreignTableName);
2422
+ imports.push(`import type { Insert${ForeignType} } from '${clientPath}/types/${foreignTableName}';`);
2423
+ if (foreignTable.pk.length === 1) {
2424
+ variables.push(`let ${foreignTableName}Id: string;`);
2425
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2426
+ setupStatements.push(`
2427
+ // Create parent ${foreignTableName} record for foreign key reference
2428
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2429
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2430
+ ${foreignTableName}Id = created${ForeignType}.${foreignTable.pk[0]};`);
2431
+ cleanupStatements.push(`
2432
+ // Clean up parent ${foreignTableName} record
2433
+ if (${foreignTableName}Id) {
2386
2434
  try {
2387
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2435
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2388
2436
  } catch (e) {
2389
2437
  // Parent might already be deleted due to cascading
2390
2438
  }
2391
2439
  }`);
2440
+ } else {
2441
+ variables.push(`let ${foreignTableName}Key: any;`);
2442
+ const foreignData = generateSampleDataFromSchema(foreignTable, false);
2443
+ setupStatements.push(`
2444
+ // Create parent ${foreignTableName} record for foreign key reference
2445
+ const ${foreignTableName}Data: Insert${ForeignType} = ${foreignData};
2446
+ const created${ForeignType} = await sdk.${foreignTableName}.create(${foreignTableName}Data);
2447
+ ${foreignTableName}Key = { ${foreignTable.pk.map((pk) => `${pk}: created${ForeignType}.${pk}`).join(", ")} };`);
2448
+ cleanupStatements.push(`
2449
+ // Clean up parent ${foreignTableName} record
2450
+ if (${foreignTableName}Key) {
2451
+ try {
2452
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2453
+ } catch (e) {
2454
+ // Parent might already be deleted due to cascading
2455
+ }
2456
+ }`);
2457
+ }
2392
2458
  }
2393
2459
  }
2394
2460
  return {
@@ -2400,30 +2466,6 @@ function generateForeignKeySetup(table, clientPath) {
2400
2466
  cleanup: cleanupStatements.join("")
2401
2467
  };
2402
2468
  }
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
2469
  function getTestCommand(framework, baseCommand) {
2428
2470
  switch (framework) {
2429
2471
  case "vitest":
@@ -2463,20 +2505,15 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2463
2505
  }
2464
2506
  for (const col of table.columns) {
2465
2507
  const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2466
- if (isAutoGenerated) {
2508
+ const isPrimaryKey = table.pk.includes(col.name);
2509
+ if (isPrimaryKey && col.hasDefault) {
2467
2510
  continue;
2468
2511
  }
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
- }
2512
+ if (isAutoGenerated) {
2476
2513
  continue;
2477
2514
  }
2478
2515
  const foreignTable = foreignKeyColumns.get(col.name);
2479
- if (!col.nullable || foreignTable || col.hasDefault && !isAutoGenerated || shouldIncludeNullableColumn(col)) {
2516
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2480
2517
  if (foreignTable) {
2481
2518
  fields.push(` ${col.name}: ${foreignTable}Id`);
2482
2519
  } else {
@@ -2493,51 +2530,40 @@ ${fields.join(`,
2493
2530
  function generateUpdateDataFromSchema(table) {
2494
2531
  const fields = [];
2495
2532
  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
- }
2533
+ if (table.pk.includes(col.name)) {
2534
+ continue;
2501
2535
  }
2502
- if (col.name.endsWith("_id")) {
2536
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2537
+ if (isAutoGenerated) {
2503
2538
  continue;
2504
2539
  }
2505
- if (col.name === "deleted_at" || col.name === "deleted") {
2540
+ if (col.name.endsWith("_id")) {
2506
2541
  continue;
2507
2542
  }
2508
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2543
+ if (!col.nullable) {
2509
2544
  const value = generateValueForColumn(col, true);
2510
2545
  fields.push(` ${col.name}: ${value}`);
2511
2546
  break;
2512
2547
  }
2513
2548
  }
2549
+ if (fields.length === 0) {
2550
+ for (const col of table.columns) {
2551
+ if (table.pk.includes(col.name) || col.name.endsWith("_id")) {
2552
+ continue;
2553
+ }
2554
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2555
+ if (!isAutoGenerated) {
2556
+ const value = generateValueForColumn(col, true);
2557
+ fields.push(` ${col.name}: ${value}`);
2558
+ break;
2559
+ }
2560
+ }
2561
+ }
2514
2562
  return fields.length > 0 ? `{
2515
2563
  ${fields.join(`,
2516
2564
  `)}
2517
2565
  }` : "{}";
2518
2566
  }
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
2567
  function generateValueForColumn(col, isUpdate = false) {
2542
2568
  const type = col.pgType.toLowerCase();
2543
2569
  switch (type) {
@@ -2571,15 +2597,17 @@ function generateValueForColumn(col, isUpdate = false) {
2571
2597
  case "bool":
2572
2598
  return isUpdate ? "false" : "true";
2573
2599
  case "date":
2600
+ return `'2024-01-01'`;
2574
2601
  case "timestamp":
2575
2602
  case "timestamptz":
2576
2603
  case "timestamp without time zone":
2577
2604
  case "timestamp with time zone":
2605
+ return `'2024-01-01T00:00:00.000Z'`;
2578
2606
  case "time":
2579
2607
  case "timetz":
2580
2608
  case "time without time zone":
2581
2609
  case "time with time zone":
2582
- return `new Date()`;
2610
+ return `'12:00:00'`;
2583
2611
  case "interval":
2584
2612
  return `'1 day'`;
2585
2613
  case "json":
@@ -2647,26 +2675,14 @@ function generateUUID() {
2647
2675
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2648
2676
  const Type = pascal(table.name);
2649
2677
  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
- }
2678
+ const hasSinglePK = table.pk.length === 1;
2663
2679
  return `it('should create a ${table.name}', async () => {
2664
2680
  const data: Insert${Type} = ${sampleData};
2665
2681
  ${hasData ? `
2666
2682
  const created = await sdk.${table.name}.create(data);
2667
2683
  expect(created).toBeDefined();
2668
- expect(created.id).toBeDefined();
2669
- createdId = created.id;
2684
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2685
+ createdId = created.${table.pk[0]};` : ""}
2670
2686
  ` : `
2671
2687
  // Table has only auto-generated columns
2672
2688
  // Skip create test or add your own test data
@@ -2679,7 +2695,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2679
2695
  expect(Array.isArray(list)).toBe(true);
2680
2696
  });
2681
2697
 
2682
- ${hasData ? `it('should get ${table.name} by id', async () => {
2698
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2683
2699
  if (!createdId) {
2684
2700
  console.warn('No ID from create test, skipping get test');
2685
2701
  return;
@@ -2687,7 +2703,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2687
2703
 
2688
2704
  const item = await sdk.${table.name}.getByPk(createdId);
2689
2705
  expect(item).toBeDefined();
2690
- expect(item?.id).toBe(createdId);
2706
+ expect(item?.${table.pk[0]}).toBe(createdId);
2691
2707
  });
2692
2708
 
2693
2709
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3045,7 +3061,6 @@ async function generate(configPath) {
3045
3061
  if (sameDirectory) {
3046
3062
  clientDir = join(originalClientDir, "sdk");
3047
3063
  }
3048
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3049
3064
  const serverFramework = cfg.serverFramework || "hono";
3050
3065
  const generateTests = cfg.tests?.generate ?? false;
3051
3066
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3089,12 +3104,12 @@ async function generate(configPath) {
3089
3104
  content: emitCoreOperations()
3090
3105
  });
3091
3106
  for (const table of Object.values(model.tables)) {
3092
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3107
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3093
3108
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3094
3109
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3095
3110
  files.push({
3096
3111
  path: join(serverDir, "zod", `${table.name}.ts`),
3097
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3112
+ content: emitZod(table, { numericMode: "string" })
3098
3113
  });
3099
3114
  let routeContent;
3100
3115
  if (serverFramework === "hono") {
@@ -3165,7 +3180,7 @@ async function generate(configPath) {
3165
3180
  for (const table of Object.values(model.tables)) {
3166
3181
  files.push({
3167
3182
  path: join(testDir, `${table.name}.test.ts`),
3168
- content: emitTableTest(table, relativeClientPath, testFramework)
3183
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3169
3184
  });
3170
3185
  }
3171
3186
  }
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.10",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {