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 +121 -113
- package/dist/emit-tests.d.ts +2 -2
- package/dist/emit-types.d.ts +0 -1
- package/dist/emit-zod.d.ts +0 -1
- package/dist/index.js +121 -106
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
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
|
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
|
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
|
2676
|
+
const foreignTablesData = new Map;
|
2641
2677
|
for (const fk of table.fks) {
|
2642
|
-
const
|
2643
|
-
if (!
|
2644
|
-
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
2648
|
-
|
2649
|
-
|
2650
|
-
|
2651
|
-
|
2652
|
-
|
2653
|
-
|
2654
|
-
//
|
2655
|
-
|
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.${
|
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
|
-
|
2771
|
+
const isPrimaryKey = table.pk.includes(col.name);
|
2772
|
+
if (isPrimaryKey && col.hasDefault) {
|
2737
2773
|
continue;
|
2738
2774
|
}
|
2739
|
-
if (
|
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 ||
|
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)
|
2767
|
-
|
2768
|
-
if (autoGenerated.includes(col.name.toLowerCase())) {
|
2769
|
-
continue;
|
2770
|
-
}
|
2796
|
+
if (table.pk.includes(col.name)) {
|
2797
|
+
continue;
|
2771
2798
|
}
|
2772
|
-
|
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
|
2803
|
+
if (col.name.endsWith("_id")) {
|
2776
2804
|
continue;
|
2777
2805
|
}
|
2778
|
-
if (!col.nullable
|
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 `
|
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
|
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.
|
2939
|
-
createdId = created.
|
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
|
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, {
|
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, {
|
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
|
}
|
package/dist/emit-tests.d.ts
CHANGED
@@ -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
|
*/
|
package/dist/emit-types.d.ts
CHANGED
package/dist/emit-zod.d.ts
CHANGED
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
|
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
|
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
|
2413
|
+
const foreignTablesData = new Map;
|
2371
2414
|
for (const fk of table.fks) {
|
2372
|
-
const
|
2373
|
-
if (!
|
2374
|
-
|
2375
|
-
|
2376
|
-
|
2377
|
-
|
2378
|
-
|
2379
|
-
|
2380
|
-
|
2381
|
-
|
2382
|
-
|
2383
|
-
|
2384
|
-
//
|
2385
|
-
|
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.${
|
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
|
-
|
2508
|
+
const isPrimaryKey = table.pk.includes(col.name);
|
2509
|
+
if (isPrimaryKey && col.hasDefault) {
|
2467
2510
|
continue;
|
2468
2511
|
}
|
2469
|
-
if (
|
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 ||
|
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)
|
2497
|
-
|
2498
|
-
if (autoGenerated.includes(col.name.toLowerCase())) {
|
2499
|
-
continue;
|
2500
|
-
}
|
2533
|
+
if (table.pk.includes(col.name)) {
|
2534
|
+
continue;
|
2501
2535
|
}
|
2502
|
-
|
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
|
2540
|
+
if (col.name.endsWith("_id")) {
|
2506
2541
|
continue;
|
2507
2542
|
}
|
2508
|
-
if (!col.nullable
|
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 `
|
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
|
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.
|
2669
|
-
createdId = created.
|
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
|
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, {
|
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, {
|
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