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 +191 -202
- 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 +191 -195
- 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
|
@@ -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()
|
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
|
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) {
|
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.${
|
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
|
-
|
2736
|
-
|
2737
|
-
|
2738
|
-
|
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 (
|
2775
|
+
if (isAutoGenerated) {
|
2742
2776
|
continue;
|
2743
2777
|
}
|
2744
|
-
|
2745
|
-
|
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)
|
2771
|
-
|
2772
|
-
if (autoGenerated.includes(col.name.toLowerCase())) {
|
2773
|
-
continue;
|
2774
|
-
}
|
2796
|
+
if (table.pk.includes(col.name)) {
|
2797
|
+
continue;
|
2775
2798
|
}
|
2776
|
-
|
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
|
2803
|
+
if (col.name.endsWith("_id")) {
|
2780
2804
|
continue;
|
2781
2805
|
}
|
2782
|
-
if (!col.nullable
|
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
|
-
|
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 `{
|
2878
|
+
return `{}`;
|
2912
2879
|
case "uuid":
|
2913
|
-
return `'${
|
2880
|
+
return `'${generateUUID()}'`;
|
2914
2881
|
case "inet":
|
2915
|
-
return `'
|
2882
|
+
return `'192.168.1.1'`;
|
2916
2883
|
case "cidr":
|
2917
2884
|
return `'192.168.1.0/24'`;
|
2918
2885
|
case "macaddr":
|
2919
|
-
|
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
|
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 `'
|
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
|
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.
|
2958
|
-
createdId = created.
|
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
|
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, {
|
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, {
|
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
|
}
|
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
@@ -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()
|
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
|
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) {
|
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.${
|
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
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
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 (
|
2512
|
+
if (isAutoGenerated) {
|
2472
2513
|
continue;
|
2473
2514
|
}
|
2474
|
-
|
2475
|
-
|
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)
|
2501
|
-
|
2502
|
-
if (autoGenerated.includes(col.name.toLowerCase())) {
|
2503
|
-
continue;
|
2504
|
-
}
|
2533
|
+
if (table.pk.includes(col.name)) {
|
2534
|
+
continue;
|
2505
2535
|
}
|
2506
|
-
|
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
|
2540
|
+
if (col.name.endsWith("_id")) {
|
2510
2541
|
continue;
|
2511
2542
|
}
|
2512
|
-
if (!col.nullable
|
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
|
-
|
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 `{
|
2615
|
+
return `{}`;
|
2642
2616
|
case "uuid":
|
2643
|
-
return `'${
|
2617
|
+
return `'${generateUUID()}'`;
|
2644
2618
|
case "inet":
|
2645
|
-
return `'
|
2619
|
+
return `'192.168.1.1'`;
|
2646
2620
|
case "cidr":
|
2647
2621
|
return `'192.168.1.0/24'`;
|
2648
2622
|
case "macaddr":
|
2649
|
-
|
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
|
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 `'
|
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
|
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.
|
2688
|
-
createdId = created.
|
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
|
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, {
|
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, {
|
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