postgresdk 0.11.0 → 0.12.1
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/README.md +8 -2
- package/dist/cli.js +175 -39
- package/dist/core/operations.d.ts +2 -0
- package/dist/emit-types.d.ts +1 -1
- package/dist/emit-zod.d.ts +1 -1
- package/dist/index.js +175 -39
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Generate a typed server/client SDK from your PostgreSQL database schema.
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- 🚀 **Instant SDK Generation** - Point at your PostgreSQL database and get a complete SDK
|
|
10
|
-
- 🔒 **Type Safety** - Full TypeScript types derived from your database schema
|
|
10
|
+
- 🔒 **Type Safety** - Full TypeScript types derived from your database schema (including enum types)
|
|
11
11
|
- ✅ **Runtime Validation** - Zod schemas for request/response validation
|
|
12
12
|
- 🔗 **Smart Relationships** - Automatic handling of 1:N and M:N relationships with eager loading
|
|
13
13
|
- 🔐 **Built-in Auth** - API key and JWT authentication
|
|
@@ -160,7 +160,7 @@ const authors = await sdk.authors.list({
|
|
|
160
160
|
### Filtering & Pagination
|
|
161
161
|
|
|
162
162
|
```typescript
|
|
163
|
-
// Simple equality filtering
|
|
163
|
+
// Simple equality filtering with single-column sorting
|
|
164
164
|
const users = await sdk.users.list({
|
|
165
165
|
where: { status: "active" },
|
|
166
166
|
orderBy: "created_at",
|
|
@@ -169,6 +169,12 @@ const users = await sdk.users.list({
|
|
|
169
169
|
offset: 40
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
// Multi-column sorting
|
|
173
|
+
const sorted = await sdk.users.list({
|
|
174
|
+
orderBy: ["status", "created_at"],
|
|
175
|
+
order: ["asc", "desc"] // or use single direction: order: "asc"
|
|
176
|
+
});
|
|
177
|
+
|
|
172
178
|
// Advanced WHERE operators
|
|
173
179
|
const filtered = await sdk.users.list({
|
|
174
180
|
where: {
|
package/dist/cli.js
CHANGED
|
@@ -780,6 +780,7 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
780
780
|
const basePath = `/v1/${tableName}`;
|
|
781
781
|
const hasSinglePK = table.pk.length === 1;
|
|
782
782
|
const pkField = hasSinglePK ? table.pk[0] : "id";
|
|
783
|
+
const enums = model.enums || {};
|
|
783
784
|
const sdkMethods = [];
|
|
784
785
|
const endpoints = [];
|
|
785
786
|
sdkMethods.push({
|
|
@@ -793,9 +794,9 @@ const items = await sdk.${tableName}.list();
|
|
|
793
794
|
const filtered = await sdk.${tableName}.list({
|
|
794
795
|
limit: 20,
|
|
795
796
|
offset: 0,
|
|
796
|
-
${table.columns[0]?.name || "field"}
|
|
797
|
-
|
|
798
|
-
|
|
797
|
+
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
798
|
+
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
799
|
+
order: 'desc'
|
|
799
800
|
});`,
|
|
800
801
|
correspondsTo: `GET ${basePath}`
|
|
801
802
|
});
|
|
@@ -803,7 +804,7 @@ const filtered = await sdk.${tableName}.list({
|
|
|
803
804
|
method: "GET",
|
|
804
805
|
path: basePath,
|
|
805
806
|
description: `List all ${tableName} records`,
|
|
806
|
-
queryParameters: generateQueryParams(table),
|
|
807
|
+
queryParameters: generateQueryParams(table, enums),
|
|
807
808
|
responseBody: `${Type}[]`
|
|
808
809
|
});
|
|
809
810
|
if (hasSinglePK) {
|
|
@@ -911,7 +912,7 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
911
912
|
});
|
|
912
913
|
}
|
|
913
914
|
}
|
|
914
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
915
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table, enums));
|
|
915
916
|
return {
|
|
916
917
|
name: Type,
|
|
917
918
|
tableName,
|
|
@@ -926,11 +927,11 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
926
927
|
fields
|
|
927
928
|
};
|
|
928
929
|
}
|
|
929
|
-
function generateFieldContract(column, table) {
|
|
930
|
+
function generateFieldContract(column, table, enums) {
|
|
930
931
|
const field = {
|
|
931
932
|
name: column.name,
|
|
932
|
-
type: postgresTypeToJsonType(column.pgType),
|
|
933
|
-
tsType: postgresTypeToTsType(column),
|
|
933
|
+
type: postgresTypeToJsonType(column.pgType, enums),
|
|
934
|
+
tsType: postgresTypeToTsType(column, enums),
|
|
934
935
|
required: !column.nullable && !column.hasDefault,
|
|
935
936
|
description: generateFieldDescription(column, table)
|
|
936
937
|
};
|
|
@@ -943,16 +944,41 @@ function generateFieldContract(column, table) {
|
|
|
943
944
|
}
|
|
944
945
|
return field;
|
|
945
946
|
}
|
|
946
|
-
function postgresTypeToTsType(column) {
|
|
947
|
+
function postgresTypeToTsType(column, enums) {
|
|
948
|
+
const pgType = column.pgType.toLowerCase();
|
|
949
|
+
if (enums[pgType]) {
|
|
950
|
+
const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
|
|
951
|
+
if (column.nullable) {
|
|
952
|
+
return `${enumType} | null`;
|
|
953
|
+
}
|
|
954
|
+
return enumType;
|
|
955
|
+
}
|
|
956
|
+
if (pgType.startsWith("_")) {
|
|
957
|
+
const enumName = pgType.slice(1);
|
|
958
|
+
const enumValues = enums[enumName];
|
|
959
|
+
if (enumValues) {
|
|
960
|
+
const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
|
|
961
|
+
const arrayType = `(${enumType})[]`;
|
|
962
|
+
if (column.nullable) {
|
|
963
|
+
return `${arrayType} | null`;
|
|
964
|
+
}
|
|
965
|
+
return arrayType;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
947
968
|
const baseType = (() => {
|
|
948
|
-
switch (
|
|
969
|
+
switch (pgType) {
|
|
949
970
|
case "int":
|
|
971
|
+
case "int2":
|
|
972
|
+
case "int4":
|
|
973
|
+
case "int8":
|
|
950
974
|
case "integer":
|
|
951
975
|
case "smallint":
|
|
952
976
|
case "bigint":
|
|
953
977
|
case "decimal":
|
|
954
978
|
case "numeric":
|
|
955
979
|
case "real":
|
|
980
|
+
case "float4":
|
|
981
|
+
case "float8":
|
|
956
982
|
case "double precision":
|
|
957
983
|
case "float":
|
|
958
984
|
return "number";
|
|
@@ -970,9 +996,16 @@ function postgresTypeToTsType(column) {
|
|
|
970
996
|
return "string";
|
|
971
997
|
case "text[]":
|
|
972
998
|
case "varchar[]":
|
|
999
|
+
case "_text":
|
|
1000
|
+
case "_varchar":
|
|
973
1001
|
return "string[]";
|
|
974
1002
|
case "int[]":
|
|
975
1003
|
case "integer[]":
|
|
1004
|
+
case "_int":
|
|
1005
|
+
case "_int2":
|
|
1006
|
+
case "_int4":
|
|
1007
|
+
case "_int8":
|
|
1008
|
+
case "_integer":
|
|
976
1009
|
return "number[]";
|
|
977
1010
|
default:
|
|
978
1011
|
return "string";
|
|
@@ -1054,18 +1087,18 @@ function generateExampleValue(column) {
|
|
|
1054
1087
|
return `'example value'`;
|
|
1055
1088
|
}
|
|
1056
1089
|
}
|
|
1057
|
-
function generateQueryParams(table) {
|
|
1090
|
+
function generateQueryParams(table, enums) {
|
|
1058
1091
|
const params = {
|
|
1059
1092
|
limit: "number - Max records to return (default: 50)",
|
|
1060
1093
|
offset: "number - Records to skip",
|
|
1061
|
-
|
|
1062
|
-
|
|
1094
|
+
orderBy: "string | string[] - Field(s) to sort by",
|
|
1095
|
+
order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
|
|
1063
1096
|
};
|
|
1064
1097
|
let filterCount = 0;
|
|
1065
1098
|
for (const col of table.columns) {
|
|
1066
1099
|
if (filterCount >= 3)
|
|
1067
1100
|
break;
|
|
1068
|
-
const type = postgresTypeToJsonType(col.pgType);
|
|
1101
|
+
const type = postgresTypeToJsonType(col.pgType, enums);
|
|
1069
1102
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
|
1070
1103
|
if (type === "string") {
|
|
1071
1104
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
|
@@ -1078,15 +1111,27 @@ function generateQueryParams(table) {
|
|
|
1078
1111
|
params["..."] = "Additional filters for all fields";
|
|
1079
1112
|
return params;
|
|
1080
1113
|
}
|
|
1081
|
-
function postgresTypeToJsonType(pgType) {
|
|
1082
|
-
|
|
1114
|
+
function postgresTypeToJsonType(pgType, enums) {
|
|
1115
|
+
const t = pgType.toLowerCase();
|
|
1116
|
+
if (enums[t]) {
|
|
1117
|
+
return t;
|
|
1118
|
+
}
|
|
1119
|
+
if (t.startsWith("_") && enums[t.slice(1)]) {
|
|
1120
|
+
return `${t.slice(1)}[]`;
|
|
1121
|
+
}
|
|
1122
|
+
switch (t) {
|
|
1083
1123
|
case "int":
|
|
1124
|
+
case "int2":
|
|
1125
|
+
case "int4":
|
|
1126
|
+
case "int8":
|
|
1084
1127
|
case "integer":
|
|
1085
1128
|
case "smallint":
|
|
1086
1129
|
case "bigint":
|
|
1087
1130
|
case "decimal":
|
|
1088
1131
|
case "numeric":
|
|
1089
1132
|
case "real":
|
|
1133
|
+
case "float4":
|
|
1134
|
+
case "float8":
|
|
1090
1135
|
case "double precision":
|
|
1091
1136
|
case "float":
|
|
1092
1137
|
return "number";
|
|
@@ -1104,9 +1149,16 @@ function postgresTypeToJsonType(pgType) {
|
|
|
1104
1149
|
return "uuid";
|
|
1105
1150
|
case "text[]":
|
|
1106
1151
|
case "varchar[]":
|
|
1152
|
+
case "_text":
|
|
1153
|
+
case "_varchar":
|
|
1107
1154
|
return "string[]";
|
|
1108
1155
|
case "int[]":
|
|
1109
1156
|
case "integer[]":
|
|
1157
|
+
case "_int":
|
|
1158
|
+
case "_int2":
|
|
1159
|
+
case "_int4":
|
|
1160
|
+
case "_int8":
|
|
1161
|
+
case "_integer":
|
|
1110
1162
|
return "number[]";
|
|
1111
1163
|
default:
|
|
1112
1164
|
return "string";
|
|
@@ -1351,6 +1403,64 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1351
1403
|
lines.push("");
|
|
1352
1404
|
lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
|
|
1353
1405
|
lines.push("");
|
|
1406
|
+
lines.push("## Sorting");
|
|
1407
|
+
lines.push("");
|
|
1408
|
+
lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
|
|
1409
|
+
lines.push("");
|
|
1410
|
+
lines.push("### Single Column Sorting");
|
|
1411
|
+
lines.push("");
|
|
1412
|
+
lines.push("```typescript");
|
|
1413
|
+
lines.push("// Sort by one column ascending");
|
|
1414
|
+
lines.push("const users = await sdk.users.list({");
|
|
1415
|
+
lines.push(" orderBy: 'created_at',");
|
|
1416
|
+
lines.push(" order: 'asc'");
|
|
1417
|
+
lines.push("});");
|
|
1418
|
+
lines.push("");
|
|
1419
|
+
lines.push("// Sort descending");
|
|
1420
|
+
lines.push("const latest = await sdk.users.list({");
|
|
1421
|
+
lines.push(" orderBy: 'created_at',");
|
|
1422
|
+
lines.push(" order: 'desc'");
|
|
1423
|
+
lines.push("});");
|
|
1424
|
+
lines.push("");
|
|
1425
|
+
lines.push("// Order defaults to 'asc' if not specified");
|
|
1426
|
+
lines.push("const sorted = await sdk.users.list({");
|
|
1427
|
+
lines.push(" orderBy: 'name'");
|
|
1428
|
+
lines.push("});");
|
|
1429
|
+
lines.push("```");
|
|
1430
|
+
lines.push("");
|
|
1431
|
+
lines.push("### Multi-Column Sorting");
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push("```typescript");
|
|
1434
|
+
lines.push("// Sort by multiple columns (all same direction)");
|
|
1435
|
+
lines.push("const users = await sdk.users.list({");
|
|
1436
|
+
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1437
|
+
lines.push(" order: 'desc'");
|
|
1438
|
+
lines.push("});");
|
|
1439
|
+
lines.push("");
|
|
1440
|
+
lines.push("// Different direction per column");
|
|
1441
|
+
lines.push("const sorted = await sdk.users.list({");
|
|
1442
|
+
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1443
|
+
lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
|
|
1444
|
+
lines.push("});");
|
|
1445
|
+
lines.push("```");
|
|
1446
|
+
lines.push("");
|
|
1447
|
+
lines.push("### Combining Sorting with Filters");
|
|
1448
|
+
lines.push("");
|
|
1449
|
+
lines.push("```typescript");
|
|
1450
|
+
lines.push("const results = await sdk.users.list({");
|
|
1451
|
+
lines.push(" where: {");
|
|
1452
|
+
lines.push(" status: 'active',");
|
|
1453
|
+
lines.push(" age: { $gte: 18 }");
|
|
1454
|
+
lines.push(" },");
|
|
1455
|
+
lines.push(" orderBy: 'created_at',");
|
|
1456
|
+
lines.push(" order: 'desc',");
|
|
1457
|
+
lines.push(" limit: 50,");
|
|
1458
|
+
lines.push(" offset: 0");
|
|
1459
|
+
lines.push("});");
|
|
1460
|
+
lines.push("```");
|
|
1461
|
+
lines.push("");
|
|
1462
|
+
lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
|
|
1463
|
+
lines.push("");
|
|
1354
1464
|
lines.push("## Resources");
|
|
1355
1465
|
lines.push("");
|
|
1356
1466
|
for (const resource of contract.resources) {
|
|
@@ -2508,23 +2618,28 @@ export const buildWithFor = (t: TableName) =>
|
|
|
2508
2618
|
|
|
2509
2619
|
// src/emit-zod.ts
|
|
2510
2620
|
init_utils();
|
|
2511
|
-
function emitZod(table, opts) {
|
|
2621
|
+
function emitZod(table, opts, enums) {
|
|
2512
2622
|
const Type = pascal(table.name);
|
|
2513
2623
|
const zFor = (pg) => {
|
|
2514
|
-
|
|
2624
|
+
const t = pg.toLowerCase();
|
|
2625
|
+
if (enums[t]) {
|
|
2626
|
+
const values = enums[t].map((v) => `"${v}"`).join(", ");
|
|
2627
|
+
return `z.enum([${values}])`;
|
|
2628
|
+
}
|
|
2629
|
+
if (t === "uuid")
|
|
2515
2630
|
return `z.string()`;
|
|
2516
|
-
if (
|
|
2631
|
+
if (t === "bool" || t === "boolean")
|
|
2517
2632
|
return `z.boolean()`;
|
|
2518
|
-
if (
|
|
2633
|
+
if (t === "int2" || t === "int4" || t === "int8")
|
|
2519
2634
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
2520
|
-
if (
|
|
2635
|
+
if (t === "numeric" || t === "float4" || t === "float8")
|
|
2521
2636
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
2522
|
-
if (
|
|
2637
|
+
if (t === "jsonb" || t === "json")
|
|
2523
2638
|
return `z.unknown()`;
|
|
2524
|
-
if (
|
|
2639
|
+
if (t === "date" || t.startsWith("timestamp"))
|
|
2525
2640
|
return `z.string()`;
|
|
2526
|
-
if (
|
|
2527
|
-
return `z.array(${zFor(
|
|
2641
|
+
if (t.startsWith("_"))
|
|
2642
|
+
return `z.array(${zFor(t.slice(1))})`;
|
|
2528
2643
|
return `z.string()`;
|
|
2529
2644
|
};
|
|
2530
2645
|
const selectFields = table.columns.map((c) => {
|
|
@@ -2625,6 +2740,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
2625
2740
|
const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
|
|
2626
2741
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
2627
2742
|
const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
|
|
2743
|
+
const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
|
|
2628
2744
|
return `/**
|
|
2629
2745
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
2630
2746
|
*
|
|
@@ -2641,12 +2757,15 @@ import { loadIncludes } from "../include-loader${ext}";
|
|
|
2641
2757
|
import * as coreOps from "../core/operations${ext}";
|
|
2642
2758
|
${authImport}
|
|
2643
2759
|
|
|
2760
|
+
const columnEnum = z.enum([${columnNames}]);
|
|
2761
|
+
|
|
2644
2762
|
const listSchema = z.object({
|
|
2645
2763
|
where: z.any().optional(),
|
|
2646
2764
|
include: z.any().optional(),
|
|
2647
2765
|
limit: z.number().int().positive().max(100).optional(),
|
|
2648
2766
|
offset: z.number().int().min(0).optional(),
|
|
2649
|
-
orderBy: z.
|
|
2767
|
+
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
2768
|
+
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
2650
2769
|
});
|
|
2651
2770
|
|
|
2652
2771
|
export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }, onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void> }) {
|
|
@@ -2835,7 +2954,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2835
2954
|
let includeMethodsCode = "";
|
|
2836
2955
|
for (const method of includeMethods) {
|
|
2837
2956
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2838
|
-
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
|
|
2957
|
+
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
2839
2958
|
if (isGetByPk) {
|
|
2840
2959
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2841
2960
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
@@ -2891,8 +3010,8 @@ export class ${Type}Client extends BaseClient {
|
|
|
2891
3010
|
limit?: number;
|
|
2892
3011
|
offset?: number;
|
|
2893
3012
|
where?: Where<Select${Type}>;
|
|
2894
|
-
orderBy?: string;
|
|
2895
|
-
order?: "asc" | "desc";
|
|
3013
|
+
orderBy?: string | string[];
|
|
3014
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2896
3015
|
}): Promise<Select${Type}[]> {
|
|
2897
3016
|
return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
|
|
2898
3017
|
}
|
|
@@ -3448,10 +3567,13 @@ export async function loadIncludes(
|
|
|
3448
3567
|
}
|
|
3449
3568
|
|
|
3450
3569
|
// src/emit-types.ts
|
|
3451
|
-
function tsTypeFor(pgType, opts) {
|
|
3570
|
+
function tsTypeFor(pgType, opts, enums) {
|
|
3452
3571
|
const t = pgType.toLowerCase();
|
|
3572
|
+
if (enums[t]) {
|
|
3573
|
+
return enums[t].map((v) => `"${v}"`).join(" | ");
|
|
3574
|
+
}
|
|
3453
3575
|
if (t.startsWith("_"))
|
|
3454
|
-
return
|
|
3576
|
+
return `(${tsTypeFor(t.slice(1), opts, enums)})[]`;
|
|
3455
3577
|
if (t === "uuid")
|
|
3456
3578
|
return "string";
|
|
3457
3579
|
if (t === "bool" || t === "boolean")
|
|
@@ -3466,17 +3588,17 @@ function tsTypeFor(pgType, opts) {
|
|
|
3466
3588
|
return "string";
|
|
3467
3589
|
}
|
|
3468
3590
|
var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
3469
|
-
function emitTypes(table, opts) {
|
|
3591
|
+
function emitTypes(table, opts, enums) {
|
|
3470
3592
|
const Type = pascal2(table.name);
|
|
3471
3593
|
const insertFields = table.columns.map((col) => {
|
|
3472
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
3594
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
3473
3595
|
const optional = col.hasDefault || col.nullable ? "?" : "";
|
|
3474
3596
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
3475
3597
|
return ` ${col.name}${optional}: ${valueType};`;
|
|
3476
3598
|
}).join(`
|
|
3477
3599
|
`);
|
|
3478
3600
|
const selectFields = table.columns.map((col) => {
|
|
3479
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
3601
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
3480
3602
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
3481
3603
|
return ` ${col.name}: ${valueType};`;
|
|
3482
3604
|
}).join(`
|
|
@@ -4226,10 +4348,10 @@ function buildWhereClause(
|
|
|
4226
4348
|
*/
|
|
4227
4349
|
export async function listRecords(
|
|
4228
4350
|
ctx: OperationContext,
|
|
4229
|
-
params: { where?: any; limit?: number; offset?: number; include?: any }
|
|
4351
|
+
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
4230
4352
|
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4231
4353
|
try {
|
|
4232
|
-
const { where: whereClause, limit = 50, offset = 0, include } = params;
|
|
4354
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
4233
4355
|
|
|
4234
4356
|
// Build WHERE clause
|
|
4235
4357
|
let paramIndex = 1;
|
|
@@ -4253,12 +4375,26 @@ export async function listRecords(
|
|
|
4253
4375
|
|
|
4254
4376
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
4255
4377
|
|
|
4378
|
+
// Build ORDER BY clause
|
|
4379
|
+
let orderBySQL = "";
|
|
4380
|
+
if (orderBy) {
|
|
4381
|
+
const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
4382
|
+
const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
|
|
4383
|
+
|
|
4384
|
+
const orderParts = columns.map((col, i) => {
|
|
4385
|
+
const dir = (directions[i] || "asc").toUpperCase();
|
|
4386
|
+
return \`"\${col}" \${dir}\`;
|
|
4387
|
+
});
|
|
4388
|
+
|
|
4389
|
+
orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4256
4392
|
// Add limit and offset params
|
|
4257
4393
|
const limitParam = \`$\${paramIndex}\`;
|
|
4258
4394
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
4259
4395
|
const allParams = [...whereParams, limit, offset];
|
|
4260
4396
|
|
|
4261
|
-
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
4397
|
+
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
4262
4398
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
4263
4399
|
|
|
4264
4400
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
@@ -5200,10 +5336,10 @@ async function generate(configPath) {
|
|
|
5200
5336
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
5201
5337
|
}
|
|
5202
5338
|
for (const table of Object.values(model.tables)) {
|
|
5203
|
-
const typesSrc = emitTypes(table, { numericMode: "string" });
|
|
5339
|
+
const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
|
|
5204
5340
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5205
5341
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5206
|
-
const zodSrc = emitZod(table, { numericMode: "string" });
|
|
5342
|
+
const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
|
|
5207
5343
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5208
5344
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5209
5345
|
const paramsZodSrc = emitParamsZod(table, graph);
|
package/dist/emit-types.d.ts
CHANGED
package/dist/emit-zod.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -779,6 +779,7 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
779
779
|
const basePath = `/v1/${tableName}`;
|
|
780
780
|
const hasSinglePK = table.pk.length === 1;
|
|
781
781
|
const pkField = hasSinglePK ? table.pk[0] : "id";
|
|
782
|
+
const enums = model.enums || {};
|
|
782
783
|
const sdkMethods = [];
|
|
783
784
|
const endpoints = [];
|
|
784
785
|
sdkMethods.push({
|
|
@@ -792,9 +793,9 @@ const items = await sdk.${tableName}.list();
|
|
|
792
793
|
const filtered = await sdk.${tableName}.list({
|
|
793
794
|
limit: 20,
|
|
794
795
|
offset: 0,
|
|
795
|
-
${table.columns[0]?.name || "field"}
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
797
|
+
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
798
|
+
order: 'desc'
|
|
798
799
|
});`,
|
|
799
800
|
correspondsTo: `GET ${basePath}`
|
|
800
801
|
});
|
|
@@ -802,7 +803,7 @@ const filtered = await sdk.${tableName}.list({
|
|
|
802
803
|
method: "GET",
|
|
803
804
|
path: basePath,
|
|
804
805
|
description: `List all ${tableName} records`,
|
|
805
|
-
queryParameters: generateQueryParams(table),
|
|
806
|
+
queryParameters: generateQueryParams(table, enums),
|
|
806
807
|
responseBody: `${Type}[]`
|
|
807
808
|
});
|
|
808
809
|
if (hasSinglePK) {
|
|
@@ -910,7 +911,7 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
910
911
|
});
|
|
911
912
|
}
|
|
912
913
|
}
|
|
913
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
914
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table, enums));
|
|
914
915
|
return {
|
|
915
916
|
name: Type,
|
|
916
917
|
tableName,
|
|
@@ -925,11 +926,11 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
925
926
|
fields
|
|
926
927
|
};
|
|
927
928
|
}
|
|
928
|
-
function generateFieldContract(column, table) {
|
|
929
|
+
function generateFieldContract(column, table, enums) {
|
|
929
930
|
const field = {
|
|
930
931
|
name: column.name,
|
|
931
|
-
type: postgresTypeToJsonType(column.pgType),
|
|
932
|
-
tsType: postgresTypeToTsType(column),
|
|
932
|
+
type: postgresTypeToJsonType(column.pgType, enums),
|
|
933
|
+
tsType: postgresTypeToTsType(column, enums),
|
|
933
934
|
required: !column.nullable && !column.hasDefault,
|
|
934
935
|
description: generateFieldDescription(column, table)
|
|
935
936
|
};
|
|
@@ -942,16 +943,41 @@ function generateFieldContract(column, table) {
|
|
|
942
943
|
}
|
|
943
944
|
return field;
|
|
944
945
|
}
|
|
945
|
-
function postgresTypeToTsType(column) {
|
|
946
|
+
function postgresTypeToTsType(column, enums) {
|
|
947
|
+
const pgType = column.pgType.toLowerCase();
|
|
948
|
+
if (enums[pgType]) {
|
|
949
|
+
const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
|
|
950
|
+
if (column.nullable) {
|
|
951
|
+
return `${enumType} | null`;
|
|
952
|
+
}
|
|
953
|
+
return enumType;
|
|
954
|
+
}
|
|
955
|
+
if (pgType.startsWith("_")) {
|
|
956
|
+
const enumName = pgType.slice(1);
|
|
957
|
+
const enumValues = enums[enumName];
|
|
958
|
+
if (enumValues) {
|
|
959
|
+
const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
|
|
960
|
+
const arrayType = `(${enumType})[]`;
|
|
961
|
+
if (column.nullable) {
|
|
962
|
+
return `${arrayType} | null`;
|
|
963
|
+
}
|
|
964
|
+
return arrayType;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
946
967
|
const baseType = (() => {
|
|
947
|
-
switch (
|
|
968
|
+
switch (pgType) {
|
|
948
969
|
case "int":
|
|
970
|
+
case "int2":
|
|
971
|
+
case "int4":
|
|
972
|
+
case "int8":
|
|
949
973
|
case "integer":
|
|
950
974
|
case "smallint":
|
|
951
975
|
case "bigint":
|
|
952
976
|
case "decimal":
|
|
953
977
|
case "numeric":
|
|
954
978
|
case "real":
|
|
979
|
+
case "float4":
|
|
980
|
+
case "float8":
|
|
955
981
|
case "double precision":
|
|
956
982
|
case "float":
|
|
957
983
|
return "number";
|
|
@@ -969,9 +995,16 @@ function postgresTypeToTsType(column) {
|
|
|
969
995
|
return "string";
|
|
970
996
|
case "text[]":
|
|
971
997
|
case "varchar[]":
|
|
998
|
+
case "_text":
|
|
999
|
+
case "_varchar":
|
|
972
1000
|
return "string[]";
|
|
973
1001
|
case "int[]":
|
|
974
1002
|
case "integer[]":
|
|
1003
|
+
case "_int":
|
|
1004
|
+
case "_int2":
|
|
1005
|
+
case "_int4":
|
|
1006
|
+
case "_int8":
|
|
1007
|
+
case "_integer":
|
|
975
1008
|
return "number[]";
|
|
976
1009
|
default:
|
|
977
1010
|
return "string";
|
|
@@ -1053,18 +1086,18 @@ function generateExampleValue(column) {
|
|
|
1053
1086
|
return `'example value'`;
|
|
1054
1087
|
}
|
|
1055
1088
|
}
|
|
1056
|
-
function generateQueryParams(table) {
|
|
1089
|
+
function generateQueryParams(table, enums) {
|
|
1057
1090
|
const params = {
|
|
1058
1091
|
limit: "number - Max records to return (default: 50)",
|
|
1059
1092
|
offset: "number - Records to skip",
|
|
1060
|
-
|
|
1061
|
-
|
|
1093
|
+
orderBy: "string | string[] - Field(s) to sort by",
|
|
1094
|
+
order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
|
|
1062
1095
|
};
|
|
1063
1096
|
let filterCount = 0;
|
|
1064
1097
|
for (const col of table.columns) {
|
|
1065
1098
|
if (filterCount >= 3)
|
|
1066
1099
|
break;
|
|
1067
|
-
const type = postgresTypeToJsonType(col.pgType);
|
|
1100
|
+
const type = postgresTypeToJsonType(col.pgType, enums);
|
|
1068
1101
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
|
1069
1102
|
if (type === "string") {
|
|
1070
1103
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
|
@@ -1077,15 +1110,27 @@ function generateQueryParams(table) {
|
|
|
1077
1110
|
params["..."] = "Additional filters for all fields";
|
|
1078
1111
|
return params;
|
|
1079
1112
|
}
|
|
1080
|
-
function postgresTypeToJsonType(pgType) {
|
|
1081
|
-
|
|
1113
|
+
function postgresTypeToJsonType(pgType, enums) {
|
|
1114
|
+
const t = pgType.toLowerCase();
|
|
1115
|
+
if (enums[t]) {
|
|
1116
|
+
return t;
|
|
1117
|
+
}
|
|
1118
|
+
if (t.startsWith("_") && enums[t.slice(1)]) {
|
|
1119
|
+
return `${t.slice(1)}[]`;
|
|
1120
|
+
}
|
|
1121
|
+
switch (t) {
|
|
1082
1122
|
case "int":
|
|
1123
|
+
case "int2":
|
|
1124
|
+
case "int4":
|
|
1125
|
+
case "int8":
|
|
1083
1126
|
case "integer":
|
|
1084
1127
|
case "smallint":
|
|
1085
1128
|
case "bigint":
|
|
1086
1129
|
case "decimal":
|
|
1087
1130
|
case "numeric":
|
|
1088
1131
|
case "real":
|
|
1132
|
+
case "float4":
|
|
1133
|
+
case "float8":
|
|
1089
1134
|
case "double precision":
|
|
1090
1135
|
case "float":
|
|
1091
1136
|
return "number";
|
|
@@ -1103,9 +1148,16 @@ function postgresTypeToJsonType(pgType) {
|
|
|
1103
1148
|
return "uuid";
|
|
1104
1149
|
case "text[]":
|
|
1105
1150
|
case "varchar[]":
|
|
1151
|
+
case "_text":
|
|
1152
|
+
case "_varchar":
|
|
1106
1153
|
return "string[]";
|
|
1107
1154
|
case "int[]":
|
|
1108
1155
|
case "integer[]":
|
|
1156
|
+
case "_int":
|
|
1157
|
+
case "_int2":
|
|
1158
|
+
case "_int4":
|
|
1159
|
+
case "_int8":
|
|
1160
|
+
case "_integer":
|
|
1109
1161
|
return "number[]";
|
|
1110
1162
|
default:
|
|
1111
1163
|
return "string";
|
|
@@ -1350,6 +1402,64 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1350
1402
|
lines.push("");
|
|
1351
1403
|
lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
|
|
1352
1404
|
lines.push("");
|
|
1405
|
+
lines.push("## Sorting");
|
|
1406
|
+
lines.push("");
|
|
1407
|
+
lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
lines.push("### Single Column Sorting");
|
|
1410
|
+
lines.push("");
|
|
1411
|
+
lines.push("```typescript");
|
|
1412
|
+
lines.push("// Sort by one column ascending");
|
|
1413
|
+
lines.push("const users = await sdk.users.list({");
|
|
1414
|
+
lines.push(" orderBy: 'created_at',");
|
|
1415
|
+
lines.push(" order: 'asc'");
|
|
1416
|
+
lines.push("});");
|
|
1417
|
+
lines.push("");
|
|
1418
|
+
lines.push("// Sort descending");
|
|
1419
|
+
lines.push("const latest = await sdk.users.list({");
|
|
1420
|
+
lines.push(" orderBy: 'created_at',");
|
|
1421
|
+
lines.push(" order: 'desc'");
|
|
1422
|
+
lines.push("});");
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
lines.push("// Order defaults to 'asc' if not specified");
|
|
1425
|
+
lines.push("const sorted = await sdk.users.list({");
|
|
1426
|
+
lines.push(" orderBy: 'name'");
|
|
1427
|
+
lines.push("});");
|
|
1428
|
+
lines.push("```");
|
|
1429
|
+
lines.push("");
|
|
1430
|
+
lines.push("### Multi-Column Sorting");
|
|
1431
|
+
lines.push("");
|
|
1432
|
+
lines.push("```typescript");
|
|
1433
|
+
lines.push("// Sort by multiple columns (all same direction)");
|
|
1434
|
+
lines.push("const users = await sdk.users.list({");
|
|
1435
|
+
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1436
|
+
lines.push(" order: 'desc'");
|
|
1437
|
+
lines.push("});");
|
|
1438
|
+
lines.push("");
|
|
1439
|
+
lines.push("// Different direction per column");
|
|
1440
|
+
lines.push("const sorted = await sdk.users.list({");
|
|
1441
|
+
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1442
|
+
lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
|
|
1443
|
+
lines.push("});");
|
|
1444
|
+
lines.push("```");
|
|
1445
|
+
lines.push("");
|
|
1446
|
+
lines.push("### Combining Sorting with Filters");
|
|
1447
|
+
lines.push("");
|
|
1448
|
+
lines.push("```typescript");
|
|
1449
|
+
lines.push("const results = await sdk.users.list({");
|
|
1450
|
+
lines.push(" where: {");
|
|
1451
|
+
lines.push(" status: 'active',");
|
|
1452
|
+
lines.push(" age: { $gte: 18 }");
|
|
1453
|
+
lines.push(" },");
|
|
1454
|
+
lines.push(" orderBy: 'created_at',");
|
|
1455
|
+
lines.push(" order: 'desc',");
|
|
1456
|
+
lines.push(" limit: 50,");
|
|
1457
|
+
lines.push(" offset: 0");
|
|
1458
|
+
lines.push("});");
|
|
1459
|
+
lines.push("```");
|
|
1460
|
+
lines.push("");
|
|
1461
|
+
lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
|
|
1462
|
+
lines.push("");
|
|
1353
1463
|
lines.push("## Resources");
|
|
1354
1464
|
lines.push("");
|
|
1355
1465
|
for (const resource of contract.resources) {
|
|
@@ -1748,23 +1858,28 @@ export const buildWithFor = (t: TableName) =>
|
|
|
1748
1858
|
|
|
1749
1859
|
// src/emit-zod.ts
|
|
1750
1860
|
init_utils();
|
|
1751
|
-
function emitZod(table, opts) {
|
|
1861
|
+
function emitZod(table, opts, enums) {
|
|
1752
1862
|
const Type = pascal(table.name);
|
|
1753
1863
|
const zFor = (pg) => {
|
|
1754
|
-
|
|
1864
|
+
const t = pg.toLowerCase();
|
|
1865
|
+
if (enums[t]) {
|
|
1866
|
+
const values = enums[t].map((v) => `"${v}"`).join(", ");
|
|
1867
|
+
return `z.enum([${values}])`;
|
|
1868
|
+
}
|
|
1869
|
+
if (t === "uuid")
|
|
1755
1870
|
return `z.string()`;
|
|
1756
|
-
if (
|
|
1871
|
+
if (t === "bool" || t === "boolean")
|
|
1757
1872
|
return `z.boolean()`;
|
|
1758
|
-
if (
|
|
1873
|
+
if (t === "int2" || t === "int4" || t === "int8")
|
|
1759
1874
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
1760
|
-
if (
|
|
1875
|
+
if (t === "numeric" || t === "float4" || t === "float8")
|
|
1761
1876
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
1762
|
-
if (
|
|
1877
|
+
if (t === "jsonb" || t === "json")
|
|
1763
1878
|
return `z.unknown()`;
|
|
1764
|
-
if (
|
|
1879
|
+
if (t === "date" || t.startsWith("timestamp"))
|
|
1765
1880
|
return `z.string()`;
|
|
1766
|
-
if (
|
|
1767
|
-
return `z.array(${zFor(
|
|
1881
|
+
if (t.startsWith("_"))
|
|
1882
|
+
return `z.array(${zFor(t.slice(1))})`;
|
|
1768
1883
|
return `z.string()`;
|
|
1769
1884
|
};
|
|
1770
1885
|
const selectFields = table.columns.map((c) => {
|
|
@@ -1865,6 +1980,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
1865
1980
|
const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
|
|
1866
1981
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
1867
1982
|
const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
|
|
1983
|
+
const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
|
|
1868
1984
|
return `/**
|
|
1869
1985
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
1870
1986
|
*
|
|
@@ -1881,12 +1997,15 @@ import { loadIncludes } from "../include-loader${ext}";
|
|
|
1881
1997
|
import * as coreOps from "../core/operations${ext}";
|
|
1882
1998
|
${authImport}
|
|
1883
1999
|
|
|
2000
|
+
const columnEnum = z.enum([${columnNames}]);
|
|
2001
|
+
|
|
1884
2002
|
const listSchema = z.object({
|
|
1885
2003
|
where: z.any().optional(),
|
|
1886
2004
|
include: z.any().optional(),
|
|
1887
2005
|
limit: z.number().int().positive().max(100).optional(),
|
|
1888
2006
|
offset: z.number().int().min(0).optional(),
|
|
1889
|
-
orderBy: z.
|
|
2007
|
+
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
2008
|
+
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
1890
2009
|
});
|
|
1891
2010
|
|
|
1892
2011
|
export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }, onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void> }) {
|
|
@@ -2075,7 +2194,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2075
2194
|
let includeMethodsCode = "";
|
|
2076
2195
|
for (const method of includeMethods) {
|
|
2077
2196
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2078
|
-
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
|
|
2197
|
+
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
2079
2198
|
if (isGetByPk) {
|
|
2080
2199
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2081
2200
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
@@ -2131,8 +2250,8 @@ export class ${Type}Client extends BaseClient {
|
|
|
2131
2250
|
limit?: number;
|
|
2132
2251
|
offset?: number;
|
|
2133
2252
|
where?: Where<Select${Type}>;
|
|
2134
|
-
orderBy?: string;
|
|
2135
|
-
order?: "asc" | "desc";
|
|
2253
|
+
orderBy?: string | string[];
|
|
2254
|
+
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2136
2255
|
}): Promise<Select${Type}[]> {
|
|
2137
2256
|
return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
|
|
2138
2257
|
}
|
|
@@ -2688,10 +2807,13 @@ export async function loadIncludes(
|
|
|
2688
2807
|
}
|
|
2689
2808
|
|
|
2690
2809
|
// src/emit-types.ts
|
|
2691
|
-
function tsTypeFor(pgType, opts) {
|
|
2810
|
+
function tsTypeFor(pgType, opts, enums) {
|
|
2692
2811
|
const t = pgType.toLowerCase();
|
|
2812
|
+
if (enums[t]) {
|
|
2813
|
+
return enums[t].map((v) => `"${v}"`).join(" | ");
|
|
2814
|
+
}
|
|
2693
2815
|
if (t.startsWith("_"))
|
|
2694
|
-
return
|
|
2816
|
+
return `(${tsTypeFor(t.slice(1), opts, enums)})[]`;
|
|
2695
2817
|
if (t === "uuid")
|
|
2696
2818
|
return "string";
|
|
2697
2819
|
if (t === "bool" || t === "boolean")
|
|
@@ -2706,17 +2828,17 @@ function tsTypeFor(pgType, opts) {
|
|
|
2706
2828
|
return "string";
|
|
2707
2829
|
}
|
|
2708
2830
|
var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
2709
|
-
function emitTypes(table, opts) {
|
|
2831
|
+
function emitTypes(table, opts, enums) {
|
|
2710
2832
|
const Type = pascal2(table.name);
|
|
2711
2833
|
const insertFields = table.columns.map((col) => {
|
|
2712
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
2834
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
2713
2835
|
const optional = col.hasDefault || col.nullable ? "?" : "";
|
|
2714
2836
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
2715
2837
|
return ` ${col.name}${optional}: ${valueType};`;
|
|
2716
2838
|
}).join(`
|
|
2717
2839
|
`);
|
|
2718
2840
|
const selectFields = table.columns.map((col) => {
|
|
2719
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
2841
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
2720
2842
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
2721
2843
|
return ` ${col.name}: ${valueType};`;
|
|
2722
2844
|
}).join(`
|
|
@@ -3466,10 +3588,10 @@ function buildWhereClause(
|
|
|
3466
3588
|
*/
|
|
3467
3589
|
export async function listRecords(
|
|
3468
3590
|
ctx: OperationContext,
|
|
3469
|
-
params: { where?: any; limit?: number; offset?: number; include?: any }
|
|
3591
|
+
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
3470
3592
|
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3471
3593
|
try {
|
|
3472
|
-
const { where: whereClause, limit = 50, offset = 0, include } = params;
|
|
3594
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
3473
3595
|
|
|
3474
3596
|
// Build WHERE clause
|
|
3475
3597
|
let paramIndex = 1;
|
|
@@ -3493,12 +3615,26 @@ export async function listRecords(
|
|
|
3493
3615
|
|
|
3494
3616
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
3495
3617
|
|
|
3618
|
+
// Build ORDER BY clause
|
|
3619
|
+
let orderBySQL = "";
|
|
3620
|
+
if (orderBy) {
|
|
3621
|
+
const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
3622
|
+
const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
|
|
3623
|
+
|
|
3624
|
+
const orderParts = columns.map((col, i) => {
|
|
3625
|
+
const dir = (directions[i] || "asc").toUpperCase();
|
|
3626
|
+
return \`"\${col}" \${dir}\`;
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3496
3632
|
// Add limit and offset params
|
|
3497
3633
|
const limitParam = \`$\${paramIndex}\`;
|
|
3498
3634
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
3499
3635
|
const allParams = [...whereParams, limit, offset];
|
|
3500
3636
|
|
|
3501
|
-
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3637
|
+
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3502
3638
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
3503
3639
|
|
|
3504
3640
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
@@ -4440,10 +4576,10 @@ async function generate(configPath) {
|
|
|
4440
4576
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
4441
4577
|
}
|
|
4442
4578
|
for (const table of Object.values(model.tables)) {
|
|
4443
|
-
const typesSrc = emitTypes(table, { numericMode: "string" });
|
|
4579
|
+
const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
|
|
4444
4580
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
4445
4581
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
4446
|
-
const zodSrc = emitZod(table, { numericMode: "string" });
|
|
4582
|
+
const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
|
|
4447
4583
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
4448
4584
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
4449
4585
|
const paramsZodSrc = emitParamsZod(table, graph);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
25
|
-
"test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
|
|
25
|
+
"test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e",
|
|
26
26
|
"test:init": "bun test/test-init.ts",
|
|
27
27
|
"test:gen": "bun test/test-gen.ts",
|
|
28
28
|
"test:gen-with-tests": "bun test/test-gen-with-tests.ts",
|
|
29
29
|
"test:pull": "bun test/test-pull.ts",
|
|
30
|
+
"test:enums": "bun test/test-enums.ts",
|
|
30
31
|
"test:typecheck": "bun test/test-typecheck.ts",
|
|
31
32
|
"test:drizzle-e2e": "bun test/test-drizzle-e2e.ts",
|
|
32
33
|
"typecheck": "tsc --noEmit",
|