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