postgresdk 0.6.7 → 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
@@ -1005,7 +998,7 @@ function emitZod(table, opts) {
1005
998
  const Type = pascal(table.name);
1006
999
  const zFor = (pg) => {
1007
1000
  if (pg === "uuid")
1008
- return `z.string().uuid()`;
1001
+ return `z.string()`;
1009
1002
  if (pg === "bool" || pg === "boolean")
1010
1003
  return `z.boolean()`;
1011
1004
  if (pg === "int2" || pg === "int4" || pg === "int8")
@@ -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) {
2697
+ try {
2698
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2699
+ } catch (e) {
2700
+ // Parent might already be deleted due to cascading
2701
+ }
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) {
2656
2714
  try {
2657
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2715
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2658
2716
  } catch (e) {
2659
2717
  // Parent might already be deleted due to cascading
2660
2718
  }
2661
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":
@@ -2732,31 +2767,22 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2732
2767
  }
2733
2768
  }
2734
2769
  for (const col of table.columns) {
2735
- if (col.hasDefault) {
2736
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2737
- if (autoGenerated.includes(col.name.toLowerCase())) {
2738
- continue;
2739
- }
2770
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2771
+ const isPrimaryKey = table.pk.includes(col.name);
2772
+ if (isPrimaryKey && col.hasDefault) {
2773
+ continue;
2740
2774
  }
2741
- if (col.name === "deleted_at" || col.name === "deleted") {
2775
+ if (isAutoGenerated) {
2742
2776
  continue;
2743
2777
  }
2744
- if (!col.nullable) {
2745
- const foreignTable = foreignKeyColumns.get(col.name);
2778
+ const foreignTable = foreignKeyColumns.get(col.name);
2779
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2746
2780
  if (foreignTable) {
2747
2781
  fields.push(` ${col.name}: ${foreignTable}Id`);
2748
2782
  } else {
2749
2783
  const value = generateValueForColumn(col);
2750
2784
  fields.push(` ${col.name}: ${value}`);
2751
2785
  }
2752
- } else {
2753
- const foreignTable = foreignKeyColumns.get(col.name);
2754
- if (foreignTable) {
2755
- fields.push(` ${col.name}: ${foreignTable}Id`);
2756
- } else if (shouldIncludeNullableColumn(col)) {
2757
- const value = generateValueForColumn(col);
2758
- fields.push(` ${col.name}: ${value}`);
2759
- }
2760
2786
  }
2761
2787
  }
2762
2788
  return fields.length > 0 ? `{
@@ -2767,126 +2793,50 @@ ${fields.join(`,
2767
2793
  function generateUpdateDataFromSchema(table) {
2768
2794
  const fields = [];
2769
2795
  for (const col of table.columns) {
2770
- if (table.pk.includes(col.name) || col.hasDefault) {
2771
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2772
- if (autoGenerated.includes(col.name.toLowerCase())) {
2773
- continue;
2774
- }
2796
+ if (table.pk.includes(col.name)) {
2797
+ continue;
2775
2798
  }
2776
- 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) {
2777
2801
  continue;
2778
2802
  }
2779
- if (col.name === "deleted_at" || col.name === "deleted") {
2803
+ if (col.name.endsWith("_id")) {
2780
2804
  continue;
2781
2805
  }
2782
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2806
+ if (!col.nullable) {
2783
2807
  const value = generateValueForColumn(col, true);
2784
2808
  fields.push(` ${col.name}: ${value}`);
2785
2809
  break;
2786
2810
  }
2787
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
+ }
2788
2825
  return fields.length > 0 ? `{
2789
2826
  ${fields.join(`,
2790
2827
  `)}
2791
2828
  }` : "{}";
2792
2829
  }
2793
- function shouldIncludeNullableColumn(col) {
2794
- const importantPatterns = [
2795
- "_id",
2796
- "_by",
2797
- "email",
2798
- "name",
2799
- "title",
2800
- "description",
2801
- "phone",
2802
- "address",
2803
- "status",
2804
- "type",
2805
- "category",
2806
- "price",
2807
- "amount",
2808
- "quantity",
2809
- "url",
2810
- "slug"
2811
- ];
2812
- const name = col.name.toLowerCase();
2813
- return importantPatterns.some((pattern) => name.includes(pattern));
2814
- }
2815
2830
  function generateValueForColumn(col, isUpdate = false) {
2816
- const name = col.name.toLowerCase();
2817
2831
  const type = col.pgType.toLowerCase();
2818
- if (name.includes("email")) {
2819
- return `'test${isUpdate ? ".updated" : ""}@example.com'`;
2820
- }
2821
- if (name.includes("phone")) {
2822
- return `'555-${isUpdate ? "0200" : "0100"}'`;
2823
- }
2824
- if (name.includes("url") || name.includes("website")) {
2825
- return `'https://example.com${isUpdate ? "/updated" : ""}'`;
2826
- }
2827
- if (name.includes("password")) {
2828
- return `'hashedPassword123'`;
2829
- }
2830
- if (name === "name" || name.includes("_name") || name.includes("name_")) {
2831
- return `'Test ${pascal(col.name)}${isUpdate ? " Updated" : ""}'`;
2832
- }
2833
- if (name === "title" || name.includes("title")) {
2834
- return `'Test Title${isUpdate ? " Updated" : ""}'`;
2835
- }
2836
- if (name.includes("description") || name === "bio" || name === "about") {
2837
- return `'Test description${isUpdate ? " updated" : ""}'`;
2838
- }
2839
- if (name === "slug") {
2840
- return `'test-slug${isUpdate ? "-updated" : ""}'`;
2841
- }
2842
- if (name === "status") {
2843
- return `'${isUpdate ? "updated" : "active"}'`;
2844
- }
2845
- if (name === "type" || name === "kind" || name === "category") {
2846
- return `'${isUpdate ? "type2" : "type1"}'`;
2847
- }
2848
- if (name === "color" || name === "colour") {
2849
- return `'${isUpdate ? "#FF0000" : "#0000FF"}'`;
2850
- }
2851
- if (name === "gender") {
2852
- return `'${isUpdate ? "F" : "M"}'`;
2853
- }
2854
- if (name.includes("price") || name === "cost" || name === "amount") {
2855
- return isUpdate ? "99.99" : "10.50";
2856
- }
2857
- if (name === "quantity" || name === "count" || name.includes("qty")) {
2858
- return isUpdate ? "5" : "1";
2859
- }
2860
- if (name === "age") {
2861
- return isUpdate ? "30" : "25";
2862
- }
2863
- if (name.includes("percent") || name === "rate" || name === "ratio") {
2864
- return isUpdate ? "0.75" : "0.5";
2865
- }
2866
- if (name.includes("latitude") || name === "lat") {
2867
- return "40.7128";
2868
- }
2869
- if (name.includes("longitude") || name === "lng" || name === "lon") {
2870
- return "-74.0060";
2871
- }
2872
- if (type.includes("date") || type.includes("timestamp")) {
2873
- if (name.includes("birth") || name === "dob") {
2874
- return `new Date('1990-01-01')`;
2875
- }
2876
- if (name.includes("end") || name.includes("expire")) {
2877
- return `new Date('2025-12-31')`;
2878
- }
2879
- if (name.includes("start") || name.includes("begin")) {
2880
- return `new Date('2024-01-01')`;
2881
- }
2882
- return `new Date()`;
2883
- }
2884
2832
  switch (type) {
2885
2833
  case "text":
2886
2834
  case "varchar":
2887
2835
  case "char":
2888
2836
  case "character varying":
2889
- return `'test_value${isUpdate ? "_updated" : ""}'`;
2837
+ case "bpchar":
2838
+ case "name":
2839
+ return `'str_${Math.random().toString(36).substring(7)}'`;
2890
2840
  case "int":
2891
2841
  case "int2":
2892
2842
  case "int4":
@@ -2894,6 +2844,8 @@ function generateValueForColumn(col, isUpdate = false) {
2894
2844
  case "integer":
2895
2845
  case "smallint":
2896
2846
  case "bigint":
2847
+ case "serial":
2848
+ case "bigserial":
2897
2849
  return isUpdate ? "42" : "1";
2898
2850
  case "decimal":
2899
2851
  case "numeric":
@@ -2902,60 +2854,98 @@ function generateValueForColumn(col, isUpdate = false) {
2902
2854
  case "float":
2903
2855
  case "float4":
2904
2856
  case "float8":
2857
+ case "money":
2905
2858
  return isUpdate ? "99.99" : "10.50";
2906
2859
  case "boolean":
2907
2860
  case "bool":
2908
2861
  return isUpdate ? "false" : "true";
2862
+ case "date":
2863
+ return `'2024-01-01'`;
2864
+ case "timestamp":
2865
+ case "timestamptz":
2866
+ case "timestamp without time zone":
2867
+ case "timestamp with time zone":
2868
+ return `'2024-01-01T00:00:00.000Z'`;
2869
+ case "time":
2870
+ case "timetz":
2871
+ case "time without time zone":
2872
+ case "time with time zone":
2873
+ return `'12:00:00'`;
2874
+ case "interval":
2875
+ return `'1 day'`;
2909
2876
  case "json":
2910
2877
  case "jsonb":
2911
- return `{ key: '${isUpdate ? "updated" : "value"}' }`;
2878
+ return `{}`;
2912
2879
  case "uuid":
2913
- return `'${isUpdate ? "550e8400-e29b-41d4-a716-446655440001" : "550e8400-e29b-41d4-a716-446655440000"}'`;
2880
+ return `'${generateUUID()}'`;
2914
2881
  case "inet":
2915
- return `'${isUpdate ? "192.168.1.2" : "192.168.1.1"}'`;
2882
+ return `'192.168.1.1'`;
2916
2883
  case "cidr":
2917
2884
  return `'192.168.1.0/24'`;
2918
2885
  case "macaddr":
2919
- return `'08:00:2b:01:02:0${isUpdate ? "4" : "3"}'`;
2886
+ case "macaddr8":
2887
+ return `'08:00:2b:01:02:03'`;
2888
+ case "point":
2889
+ return `'(1,2)'`;
2890
+ case "line":
2891
+ return `'{1,2,3}'`;
2892
+ case "lseg":
2893
+ return `'[(0,0),(1,1)]'`;
2894
+ case "box":
2895
+ return `'((0,0),(1,1))'`;
2896
+ case "path":
2897
+ return `'[(0,0),(1,1),(2,0)]'`;
2898
+ case "polygon":
2899
+ return `'((0,0),(1,1),(1,0))'`;
2900
+ case "circle":
2901
+ return `'<(0,0),1>'`;
2902
+ case "bit":
2903
+ case "bit varying":
2904
+ case "varbit":
2905
+ return `'101'`;
2906
+ case "bytea":
2907
+ return `'\\\\x0102'`;
2920
2908
  case "xml":
2921
- return `'<root>${isUpdate ? "updated" : "value"}</root>'`;
2909
+ return `'<root/>'`;
2910
+ case "tsvector":
2911
+ return `'a fat cat'`;
2912
+ case "tsquery":
2913
+ return `'fat & cat'`;
2914
+ case "oid":
2915
+ case "regproc":
2916
+ case "regprocedure":
2917
+ case "regoper":
2918
+ case "regoperator":
2919
+ case "regclass":
2920
+ case "regtype":
2921
+ case "regconfig":
2922
+ case "regdictionary":
2923
+ return "1";
2922
2924
  default:
2923
- if (type.endsWith("[]")) {
2924
- const baseType = type.slice(0, -2);
2925
- if (baseType === "text" || baseType === "varchar") {
2926
- return `['item1', 'item2${isUpdate ? "_updated" : ""}']`;
2927
- }
2928
- if (baseType === "int" || baseType === "integer") {
2929
- return `[1, 2, ${isUpdate ? "3" : ""}]`;
2930
- }
2925
+ if (type.endsWith("[]") || type.startsWith("_")) {
2931
2926
  return `[]`;
2932
2927
  }
2933
- return `'test${isUpdate ? "_updated" : ""}'`;
2928
+ return `'value1'`;
2934
2929
  }
2935
2930
  }
2931
+ function generateUUID() {
2932
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
2933
+ const r = Math.random() * 16 | 0;
2934
+ const v = c === "x" ? r : r & 3 | 8;
2935
+ return v.toString(16);
2936
+ });
2937
+ }
2936
2938
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2937
2939
  const Type = pascal(table.name);
2938
2940
  const hasData = sampleData !== "{}";
2939
- const isJunctionTable = table.pk.length > 1 && table.columns.every((col) => table.pk.includes(col.name) || col.name.endsWith("_id"));
2940
- if (isJunctionTable) {
2941
- return `it('should create a ${table.name} relationship', async () => {
2942
- // This is a junction table for M:N relationships
2943
- // Test data depends on parent records created in other tests
2944
- expect(true).toBe(true);
2945
- });
2946
-
2947
- it('should list ${table.name} relationships', async () => {
2948
- const list = await sdk.${table.name}.list({ limit: 10 });
2949
- expect(Array.isArray(list)).toBe(true);
2950
- });`;
2951
- }
2941
+ const hasSinglePK = table.pk.length === 1;
2952
2942
  return `it('should create a ${table.name}', async () => {
2953
2943
  const data: Insert${Type} = ${sampleData};
2954
2944
  ${hasData ? `
2955
2945
  const created = await sdk.${table.name}.create(data);
2956
2946
  expect(created).toBeDefined();
2957
- expect(created.id).toBeDefined();
2958
- createdId = created.id;
2947
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2948
+ createdId = created.${table.pk[0]};` : ""}
2959
2949
  ` : `
2960
2950
  // Table has only auto-generated columns
2961
2951
  // Skip create test or add your own test data
@@ -2968,7 +2958,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2968
2958
  expect(Array.isArray(list)).toBe(true);
2969
2959
  });
2970
2960
 
2971
- ${hasData ? `it('should get ${table.name} by id', async () => {
2961
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2972
2962
  if (!createdId) {
2973
2963
  console.warn('No ID from create test, skipping get test');
2974
2964
  return;
@@ -2976,7 +2966,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2976
2966
 
2977
2967
  const item = await sdk.${table.name}.getByPk(createdId);
2978
2968
  expect(item).toBeDefined();
2979
- expect(item?.id).toBe(createdId);
2969
+ expect(item?.${table.pk[0]}).toBe(createdId);
2980
2970
  });
2981
2971
 
2982
2972
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3334,7 +3324,6 @@ async function generate(configPath) {
3334
3324
  if (sameDirectory) {
3335
3325
  clientDir = join(originalClientDir, "sdk");
3336
3326
  }
3337
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3338
3327
  const serverFramework = cfg.serverFramework || "hono";
3339
3328
  const generateTests = cfg.tests?.generate ?? false;
3340
3329
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3378,12 +3367,12 @@ async function generate(configPath) {
3378
3367
  content: emitCoreOperations()
3379
3368
  });
3380
3369
  for (const table of Object.values(model.tables)) {
3381
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3370
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3382
3371
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3383
3372
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3384
3373
  files.push({
3385
3374
  path: join(serverDir, "zod", `${table.name}.ts`),
3386
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3375
+ content: emitZod(table, { numericMode: "string" })
3387
3376
  });
3388
3377
  let routeContent;
3389
3378
  if (serverFramework === "hono") {
@@ -3454,7 +3443,7 @@ async function generate(configPath) {
3454
3443
  for (const table of Object.values(model.tables)) {
3455
3444
  files.push({
3456
3445
  path: join(testDir, `${table.name}.test.ts`),
3457
- content: emitTableTest(table, relativeClientPath, testFramework)
3446
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3458
3447
  });
3459
3448
  }
3460
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
@@ -735,7 +735,7 @@ function emitZod(table, opts) {
735
735
  const Type = pascal(table.name);
736
736
  const zFor = (pg) => {
737
737
  if (pg === "uuid")
738
- return `z.string().uuid()`;
738
+ return `z.string()`;
739
739
  if (pg === "bool" || pg === "boolean")
740
740
  return `z.boolean()`;
741
741
  if (pg === "int2" || pg === "int4" || pg === "int8")
@@ -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) {
2434
+ try {
2435
+ await sdk.${foreignTableName}.delete(${foreignTableName}Id);
2436
+ } catch (e) {
2437
+ // Parent might already be deleted due to cascading
2438
+ }
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) {
2386
2451
  try {
2387
- await sdk.${foreignTable}.delete(${foreignTable}Id);
2452
+ await sdk.${foreignTableName}.delete(${foreignTableName}Key);
2388
2453
  } catch (e) {
2389
2454
  // Parent might already be deleted due to cascading
2390
2455
  }
2391
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":
@@ -2462,31 +2504,22 @@ function generateSampleDataFromSchema(table, hasForeignKeys = false) {
2462
2504
  }
2463
2505
  }
2464
2506
  for (const col of table.columns) {
2465
- if (col.hasDefault) {
2466
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2467
- if (autoGenerated.includes(col.name.toLowerCase())) {
2468
- continue;
2469
- }
2507
+ const isAutoGenerated = col.hasDefault && ["id", "created_at", "updated_at", "created", "updated", "modified_at"].includes(col.name.toLowerCase());
2508
+ const isPrimaryKey = table.pk.includes(col.name);
2509
+ if (isPrimaryKey && col.hasDefault) {
2510
+ continue;
2470
2511
  }
2471
- if (col.name === "deleted_at" || col.name === "deleted") {
2512
+ if (isAutoGenerated) {
2472
2513
  continue;
2473
2514
  }
2474
- if (!col.nullable) {
2475
- const foreignTable = foreignKeyColumns.get(col.name);
2515
+ const foreignTable = foreignKeyColumns.get(col.name);
2516
+ if (!col.nullable || foreignTable || isPrimaryKey) {
2476
2517
  if (foreignTable) {
2477
2518
  fields.push(` ${col.name}: ${foreignTable}Id`);
2478
2519
  } else {
2479
2520
  const value = generateValueForColumn(col);
2480
2521
  fields.push(` ${col.name}: ${value}`);
2481
2522
  }
2482
- } else {
2483
- const foreignTable = foreignKeyColumns.get(col.name);
2484
- if (foreignTable) {
2485
- fields.push(` ${col.name}: ${foreignTable}Id`);
2486
- } else if (shouldIncludeNullableColumn(col)) {
2487
- const value = generateValueForColumn(col);
2488
- fields.push(` ${col.name}: ${value}`);
2489
- }
2490
2523
  }
2491
2524
  }
2492
2525
  return fields.length > 0 ? `{
@@ -2497,126 +2530,50 @@ ${fields.join(`,
2497
2530
  function generateUpdateDataFromSchema(table) {
2498
2531
  const fields = [];
2499
2532
  for (const col of table.columns) {
2500
- if (table.pk.includes(col.name) || col.hasDefault) {
2501
- const autoGenerated = ["id", "created_at", "updated_at", "created", "updated", "modified_at"];
2502
- if (autoGenerated.includes(col.name.toLowerCase())) {
2503
- continue;
2504
- }
2533
+ if (table.pk.includes(col.name)) {
2534
+ continue;
2505
2535
  }
2506
- 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) {
2507
2538
  continue;
2508
2539
  }
2509
- if (col.name === "deleted_at" || col.name === "deleted") {
2540
+ if (col.name.endsWith("_id")) {
2510
2541
  continue;
2511
2542
  }
2512
- if (!col.nullable || shouldIncludeNullableColumn(col)) {
2543
+ if (!col.nullable) {
2513
2544
  const value = generateValueForColumn(col, true);
2514
2545
  fields.push(` ${col.name}: ${value}`);
2515
2546
  break;
2516
2547
  }
2517
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
+ }
2518
2562
  return fields.length > 0 ? `{
2519
2563
  ${fields.join(`,
2520
2564
  `)}
2521
2565
  }` : "{}";
2522
2566
  }
2523
- function shouldIncludeNullableColumn(col) {
2524
- const importantPatterns = [
2525
- "_id",
2526
- "_by",
2527
- "email",
2528
- "name",
2529
- "title",
2530
- "description",
2531
- "phone",
2532
- "address",
2533
- "status",
2534
- "type",
2535
- "category",
2536
- "price",
2537
- "amount",
2538
- "quantity",
2539
- "url",
2540
- "slug"
2541
- ];
2542
- const name = col.name.toLowerCase();
2543
- return importantPatterns.some((pattern) => name.includes(pattern));
2544
- }
2545
2567
  function generateValueForColumn(col, isUpdate = false) {
2546
- const name = col.name.toLowerCase();
2547
2568
  const type = col.pgType.toLowerCase();
2548
- if (name.includes("email")) {
2549
- return `'test${isUpdate ? ".updated" : ""}@example.com'`;
2550
- }
2551
- if (name.includes("phone")) {
2552
- return `'555-${isUpdate ? "0200" : "0100"}'`;
2553
- }
2554
- if (name.includes("url") || name.includes("website")) {
2555
- return `'https://example.com${isUpdate ? "/updated" : ""}'`;
2556
- }
2557
- if (name.includes("password")) {
2558
- return `'hashedPassword123'`;
2559
- }
2560
- if (name === "name" || name.includes("_name") || name.includes("name_")) {
2561
- return `'Test ${pascal(col.name)}${isUpdate ? " Updated" : ""}'`;
2562
- }
2563
- if (name === "title" || name.includes("title")) {
2564
- return `'Test Title${isUpdate ? " Updated" : ""}'`;
2565
- }
2566
- if (name.includes("description") || name === "bio" || name === "about") {
2567
- return `'Test description${isUpdate ? " updated" : ""}'`;
2568
- }
2569
- if (name === "slug") {
2570
- return `'test-slug${isUpdate ? "-updated" : ""}'`;
2571
- }
2572
- if (name === "status") {
2573
- return `'${isUpdate ? "updated" : "active"}'`;
2574
- }
2575
- if (name === "type" || name === "kind" || name === "category") {
2576
- return `'${isUpdate ? "type2" : "type1"}'`;
2577
- }
2578
- if (name === "color" || name === "colour") {
2579
- return `'${isUpdate ? "#FF0000" : "#0000FF"}'`;
2580
- }
2581
- if (name === "gender") {
2582
- return `'${isUpdate ? "F" : "M"}'`;
2583
- }
2584
- if (name.includes("price") || name === "cost" || name === "amount") {
2585
- return isUpdate ? "99.99" : "10.50";
2586
- }
2587
- if (name === "quantity" || name === "count" || name.includes("qty")) {
2588
- return isUpdate ? "5" : "1";
2589
- }
2590
- if (name === "age") {
2591
- return isUpdate ? "30" : "25";
2592
- }
2593
- if (name.includes("percent") || name === "rate" || name === "ratio") {
2594
- return isUpdate ? "0.75" : "0.5";
2595
- }
2596
- if (name.includes("latitude") || name === "lat") {
2597
- return "40.7128";
2598
- }
2599
- if (name.includes("longitude") || name === "lng" || name === "lon") {
2600
- return "-74.0060";
2601
- }
2602
- if (type.includes("date") || type.includes("timestamp")) {
2603
- if (name.includes("birth") || name === "dob") {
2604
- return `new Date('1990-01-01')`;
2605
- }
2606
- if (name.includes("end") || name.includes("expire")) {
2607
- return `new Date('2025-12-31')`;
2608
- }
2609
- if (name.includes("start") || name.includes("begin")) {
2610
- return `new Date('2024-01-01')`;
2611
- }
2612
- return `new Date()`;
2613
- }
2614
2569
  switch (type) {
2615
2570
  case "text":
2616
2571
  case "varchar":
2617
2572
  case "char":
2618
2573
  case "character varying":
2619
- return `'test_value${isUpdate ? "_updated" : ""}'`;
2574
+ case "bpchar":
2575
+ case "name":
2576
+ return `'str_${Math.random().toString(36).substring(7)}'`;
2620
2577
  case "int":
2621
2578
  case "int2":
2622
2579
  case "int4":
@@ -2624,6 +2581,8 @@ function generateValueForColumn(col, isUpdate = false) {
2624
2581
  case "integer":
2625
2582
  case "smallint":
2626
2583
  case "bigint":
2584
+ case "serial":
2585
+ case "bigserial":
2627
2586
  return isUpdate ? "42" : "1";
2628
2587
  case "decimal":
2629
2588
  case "numeric":
@@ -2632,60 +2591,98 @@ function generateValueForColumn(col, isUpdate = false) {
2632
2591
  case "float":
2633
2592
  case "float4":
2634
2593
  case "float8":
2594
+ case "money":
2635
2595
  return isUpdate ? "99.99" : "10.50";
2636
2596
  case "boolean":
2637
2597
  case "bool":
2638
2598
  return isUpdate ? "false" : "true";
2599
+ case "date":
2600
+ return `'2024-01-01'`;
2601
+ case "timestamp":
2602
+ case "timestamptz":
2603
+ case "timestamp without time zone":
2604
+ case "timestamp with time zone":
2605
+ return `'2024-01-01T00:00:00.000Z'`;
2606
+ case "time":
2607
+ case "timetz":
2608
+ case "time without time zone":
2609
+ case "time with time zone":
2610
+ return `'12:00:00'`;
2611
+ case "interval":
2612
+ return `'1 day'`;
2639
2613
  case "json":
2640
2614
  case "jsonb":
2641
- return `{ key: '${isUpdate ? "updated" : "value"}' }`;
2615
+ return `{}`;
2642
2616
  case "uuid":
2643
- return `'${isUpdate ? "550e8400-e29b-41d4-a716-446655440001" : "550e8400-e29b-41d4-a716-446655440000"}'`;
2617
+ return `'${generateUUID()}'`;
2644
2618
  case "inet":
2645
- return `'${isUpdate ? "192.168.1.2" : "192.168.1.1"}'`;
2619
+ return `'192.168.1.1'`;
2646
2620
  case "cidr":
2647
2621
  return `'192.168.1.0/24'`;
2648
2622
  case "macaddr":
2649
- return `'08:00:2b:01:02:0${isUpdate ? "4" : "3"}'`;
2623
+ case "macaddr8":
2624
+ return `'08:00:2b:01:02:03'`;
2625
+ case "point":
2626
+ return `'(1,2)'`;
2627
+ case "line":
2628
+ return `'{1,2,3}'`;
2629
+ case "lseg":
2630
+ return `'[(0,0),(1,1)]'`;
2631
+ case "box":
2632
+ return `'((0,0),(1,1))'`;
2633
+ case "path":
2634
+ return `'[(0,0),(1,1),(2,0)]'`;
2635
+ case "polygon":
2636
+ return `'((0,0),(1,1),(1,0))'`;
2637
+ case "circle":
2638
+ return `'<(0,0),1>'`;
2639
+ case "bit":
2640
+ case "bit varying":
2641
+ case "varbit":
2642
+ return `'101'`;
2643
+ case "bytea":
2644
+ return `'\\\\x0102'`;
2650
2645
  case "xml":
2651
- return `'<root>${isUpdate ? "updated" : "value"}</root>'`;
2646
+ return `'<root/>'`;
2647
+ case "tsvector":
2648
+ return `'a fat cat'`;
2649
+ case "tsquery":
2650
+ return `'fat & cat'`;
2651
+ case "oid":
2652
+ case "regproc":
2653
+ case "regprocedure":
2654
+ case "regoper":
2655
+ case "regoperator":
2656
+ case "regclass":
2657
+ case "regtype":
2658
+ case "regconfig":
2659
+ case "regdictionary":
2660
+ return "1";
2652
2661
  default:
2653
- if (type.endsWith("[]")) {
2654
- const baseType = type.slice(0, -2);
2655
- if (baseType === "text" || baseType === "varchar") {
2656
- return `['item1', 'item2${isUpdate ? "_updated" : ""}']`;
2657
- }
2658
- if (baseType === "int" || baseType === "integer") {
2659
- return `[1, 2, ${isUpdate ? "3" : ""}]`;
2660
- }
2662
+ if (type.endsWith("[]") || type.startsWith("_")) {
2661
2663
  return `[]`;
2662
2664
  }
2663
- return `'test${isUpdate ? "_updated" : ""}'`;
2665
+ return `'value1'`;
2664
2666
  }
2665
2667
  }
2668
+ function generateUUID() {
2669
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
2670
+ const r = Math.random() * 16 | 0;
2671
+ const v = c === "x" ? r : r & 3 | 8;
2672
+ return v.toString(16);
2673
+ });
2674
+ }
2666
2675
  function generateTestCases(table, sampleData, updateData, hasForeignKeys = false) {
2667
2676
  const Type = pascal(table.name);
2668
2677
  const hasData = sampleData !== "{}";
2669
- const isJunctionTable = table.pk.length > 1 && table.columns.every((col) => table.pk.includes(col.name) || col.name.endsWith("_id"));
2670
- if (isJunctionTable) {
2671
- return `it('should create a ${table.name} relationship', async () => {
2672
- // This is a junction table for M:N relationships
2673
- // Test data depends on parent records created in other tests
2674
- expect(true).toBe(true);
2675
- });
2676
-
2677
- it('should list ${table.name} relationships', async () => {
2678
- const list = await sdk.${table.name}.list({ limit: 10 });
2679
- expect(Array.isArray(list)).toBe(true);
2680
- });`;
2681
- }
2678
+ const hasSinglePK = table.pk.length === 1;
2682
2679
  return `it('should create a ${table.name}', async () => {
2683
2680
  const data: Insert${Type} = ${sampleData};
2684
2681
  ${hasData ? `
2685
2682
  const created = await sdk.${table.name}.create(data);
2686
2683
  expect(created).toBeDefined();
2687
- expect(created.id).toBeDefined();
2688
- createdId = created.id;
2684
+ ${hasSinglePK ? `expect(created.${table.pk[0]}).toBeDefined();
2685
+ createdId = created.${table.pk[0]};` : ""}
2689
2686
  ` : `
2690
2687
  // Table has only auto-generated columns
2691
2688
  // Skip create test or add your own test data
@@ -2698,7 +2695,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2698
2695
  expect(Array.isArray(list)).toBe(true);
2699
2696
  });
2700
2697
 
2701
- ${hasData ? `it('should get ${table.name} by id', async () => {
2698
+ ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
2702
2699
  if (!createdId) {
2703
2700
  console.warn('No ID from create test, skipping get test');
2704
2701
  return;
@@ -2706,7 +2703,7 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
2706
2703
 
2707
2704
  const item = await sdk.${table.name}.getByPk(createdId);
2708
2705
  expect(item).toBeDefined();
2709
- expect(item?.id).toBe(createdId);
2706
+ expect(item?.${table.pk[0]}).toBe(createdId);
2710
2707
  });
2711
2708
 
2712
2709
  ${updateData !== "{}" ? `it('should update ${table.name}', async () => {
@@ -3064,7 +3061,6 @@ async function generate(configPath) {
3064
3061
  if (sameDirectory) {
3065
3062
  clientDir = join(originalClientDir, "sdk");
3066
3063
  }
3067
- const normDateType = cfg.dateType === "string" ? "string" : "date";
3068
3064
  const serverFramework = cfg.serverFramework || "hono";
3069
3065
  const generateTests = cfg.tests?.generate ?? false;
3070
3066
  const originalTestDir = cfg.tests?.output || "./api/tests";
@@ -3108,12 +3104,12 @@ async function generate(configPath) {
3108
3104
  content: emitCoreOperations()
3109
3105
  });
3110
3106
  for (const table of Object.values(model.tables)) {
3111
- const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
3107
+ const typesSrc = emitTypes(table, { numericMode: "string" });
3112
3108
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
3113
3109
  files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
3114
3110
  files.push({
3115
3111
  path: join(serverDir, "zod", `${table.name}.ts`),
3116
- content: emitZod(table, { dateType: normDateType, numericMode: "string" })
3112
+ content: emitZod(table, { numericMode: "string" })
3117
3113
  });
3118
3114
  let routeContent;
3119
3115
  if (serverFramework === "hono") {
@@ -3184,7 +3180,7 @@ async function generate(configPath) {
3184
3180
  for (const table of Object.values(model.tables)) {
3185
3181
  files.push({
3186
3182
  path: join(testDir, `${table.name}.test.ts`),
3187
- content: emitTableTest(table, relativeClientPath, testFramework)
3183
+ content: emitTableTest(table, model, relativeClientPath, testFramework)
3188
3184
  });
3189
3185
  }
3190
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.7",
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": {