postgresdk 0.12.0 → 0.13.0
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 +35 -7
- package/dist/cli.js +251 -66
- package/dist/emit-types.d.ts +1 -1
- package/dist/emit-zod.d.ts +1 -1
- package/dist/index.js +251 -66
- 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
|
|
@@ -121,7 +121,8 @@ const user = await sdk.users.create({ name: "Bob", email: "bob@example.com" });
|
|
|
121
121
|
|
|
122
122
|
// Read
|
|
123
123
|
const user = await sdk.users.getByPk(123);
|
|
124
|
-
const
|
|
124
|
+
const result = await sdk.users.list();
|
|
125
|
+
const users = result.data; // Array of users
|
|
125
126
|
|
|
126
127
|
// Update
|
|
127
128
|
const updated = await sdk.users.update(123, { name: "Robert" });
|
|
@@ -136,17 +137,19 @@ Automatically handles relationships with the `include` parameter:
|
|
|
136
137
|
|
|
137
138
|
```typescript
|
|
138
139
|
// 1:N relationship - Get authors with their books
|
|
139
|
-
const
|
|
140
|
+
const authorsResult = await sdk.authors.list({
|
|
140
141
|
include: { books: true }
|
|
141
142
|
});
|
|
143
|
+
const authors = authorsResult.data;
|
|
142
144
|
|
|
143
145
|
// M:N relationship - Get books with their tags
|
|
144
|
-
const
|
|
146
|
+
const booksResult = await sdk.books.list({
|
|
145
147
|
include: { tags: true }
|
|
146
148
|
});
|
|
149
|
+
const books = booksResult.data;
|
|
147
150
|
|
|
148
151
|
// Nested includes - Get authors with books and their tags
|
|
149
|
-
const
|
|
152
|
+
const nestedResult = await sdk.authors.list({
|
|
150
153
|
include: {
|
|
151
154
|
books: {
|
|
152
155
|
include: {
|
|
@@ -155,13 +158,15 @@ const authors = await sdk.authors.list({
|
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
});
|
|
161
|
+
const authorsWithBooksAndTags = nestedResult.data;
|
|
158
162
|
```
|
|
159
163
|
|
|
160
164
|
### Filtering & Pagination
|
|
161
165
|
|
|
166
|
+
All `list()` methods return pagination metadata:
|
|
167
|
+
|
|
162
168
|
```typescript
|
|
163
|
-
|
|
164
|
-
const users = await sdk.users.list({
|
|
169
|
+
const result = await sdk.users.list({
|
|
165
170
|
where: { status: "active" },
|
|
166
171
|
orderBy: "created_at",
|
|
167
172
|
order: "desc",
|
|
@@ -169,6 +174,17 @@ const users = await sdk.users.list({
|
|
|
169
174
|
offset: 40
|
|
170
175
|
});
|
|
171
176
|
|
|
177
|
+
// Access results
|
|
178
|
+
result.data; // User[] - array of records
|
|
179
|
+
result.total; // number - total matching records
|
|
180
|
+
result.limit; // number - page size used
|
|
181
|
+
result.offset; // number - offset used
|
|
182
|
+
result.hasMore; // boolean - more pages available
|
|
183
|
+
|
|
184
|
+
// Calculate pagination info
|
|
185
|
+
const totalPages = Math.ceil(result.total / result.limit);
|
|
186
|
+
const currentPage = Math.floor(result.offset / result.limit) + 1;
|
|
187
|
+
|
|
172
188
|
// Multi-column sorting
|
|
173
189
|
const sorted = await sdk.users.list({
|
|
174
190
|
orderBy: ["status", "created_at"],
|
|
@@ -184,6 +200,7 @@ const filtered = await sdk.users.list({
|
|
|
184
200
|
deleted_at: { $is: null } // NULL checks
|
|
185
201
|
}
|
|
186
202
|
});
|
|
203
|
+
// filtered.total respects WHERE clause for accurate counts
|
|
187
204
|
|
|
188
205
|
// OR logic - match any condition
|
|
189
206
|
const results = await sdk.users.list({
|
|
@@ -221,6 +238,17 @@ const nested = await sdk.users.list({
|
|
|
221
238
|
]
|
|
222
239
|
}
|
|
223
240
|
});
|
|
241
|
+
|
|
242
|
+
// Pagination with filtered results
|
|
243
|
+
let allResults = [];
|
|
244
|
+
let offset = 0;
|
|
245
|
+
const limit = 50;
|
|
246
|
+
do {
|
|
247
|
+
const page = await sdk.users.list({ where: { status: 'active' }, limit, offset });
|
|
248
|
+
allResults = allResults.concat(page.data);
|
|
249
|
+
offset += limit;
|
|
250
|
+
if (!page.hasMore) break;
|
|
251
|
+
} while (true);
|
|
224
252
|
```
|
|
225
253
|
|
|
226
254
|
See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`.
|
package/dist/cli.js
CHANGED
|
@@ -578,7 +578,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
578
578
|
path: newPath,
|
|
579
579
|
isMany: newIsMany,
|
|
580
580
|
targets: newTargets,
|
|
581
|
-
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
581
|
+
returnType: `{ data: (${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]; total: number; limit: number; offset: number; hasMore: boolean; }`,
|
|
582
582
|
includeSpec: buildIncludeSpec(newPath)
|
|
583
583
|
});
|
|
584
584
|
methods.push({
|
|
@@ -618,7 +618,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
618
618
|
path: combinedPath,
|
|
619
619
|
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
620
620
|
targets: [edge1.target, edge2.target],
|
|
621
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
621
|
+
returnType: `{ data: (Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]; total: number; limit: number; offset: number; hasMore: boolean; }`,
|
|
622
622
|
includeSpec: { [key1]: true, [key2]: true }
|
|
623
623
|
});
|
|
624
624
|
methods.push({
|
|
@@ -780,14 +780,18 @@ 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({
|
|
786
787
|
name: "list",
|
|
787
|
-
signature: `list(params?: ListParams): Promise
|
|
788
|
-
description: `List ${tableName} with filtering, sorting, and pagination
|
|
788
|
+
signature: `list(params?: ListParams): Promise<{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }>`,
|
|
789
|
+
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
789
790
|
example: `// Get all ${tableName}
|
|
790
|
-
const
|
|
791
|
+
const result = await sdk.${tableName}.list();
|
|
792
|
+
console.log(result.data); // array of records
|
|
793
|
+
console.log(result.total); // total matching records
|
|
794
|
+
console.log(result.hasMore); // true if more pages available
|
|
791
795
|
|
|
792
796
|
// With filters and pagination
|
|
793
797
|
const filtered = await sdk.${tableName}.list({
|
|
@@ -796,15 +800,19 @@ const filtered = await sdk.${tableName}.list({
|
|
|
796
800
|
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
797
801
|
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
798
802
|
order: 'desc'
|
|
799
|
-
})
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Calculate total pages
|
|
806
|
+
const totalPages = Math.ceil(filtered.total / filtered.limit);
|
|
807
|
+
const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
800
808
|
correspondsTo: `GET ${basePath}`
|
|
801
809
|
});
|
|
802
810
|
endpoints.push({
|
|
803
811
|
method: "GET",
|
|
804
812
|
path: basePath,
|
|
805
|
-
description: `List all ${tableName} records`,
|
|
806
|
-
queryParameters: generateQueryParams(table),
|
|
807
|
-
responseBody:
|
|
813
|
+
description: `List all ${tableName} records with pagination metadata`,
|
|
814
|
+
queryParameters: generateQueryParams(table, enums),
|
|
815
|
+
responseBody: `{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }`
|
|
808
816
|
});
|
|
809
817
|
if (hasSinglePK) {
|
|
810
818
|
sdkMethods.push({
|
|
@@ -894,7 +902,10 @@ console.log('Deleted:', deleted);`,
|
|
|
894
902
|
}, allTables);
|
|
895
903
|
for (const method of includeMethods) {
|
|
896
904
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
897
|
-
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const
|
|
905
|
+
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const result = await sdk.${tableName}.${method.name}();
|
|
906
|
+
console.log(result.data); // array of records with includes
|
|
907
|
+
console.log(result.total); // total count
|
|
908
|
+
console.log(result.hasMore); // more pages available
|
|
898
909
|
|
|
899
910
|
// With filters and pagination
|
|
900
911
|
const filtered = await sdk.${tableName}.${method.name}({
|
|
@@ -911,7 +922,7 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
911
922
|
});
|
|
912
923
|
}
|
|
913
924
|
}
|
|
914
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
925
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table, enums));
|
|
915
926
|
return {
|
|
916
927
|
name: Type,
|
|
917
928
|
tableName,
|
|
@@ -926,11 +937,11 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
926
937
|
fields
|
|
927
938
|
};
|
|
928
939
|
}
|
|
929
|
-
function generateFieldContract(column, table) {
|
|
940
|
+
function generateFieldContract(column, table, enums) {
|
|
930
941
|
const field = {
|
|
931
942
|
name: column.name,
|
|
932
|
-
type: postgresTypeToJsonType(column.pgType),
|
|
933
|
-
tsType: postgresTypeToTsType(column),
|
|
943
|
+
type: postgresTypeToJsonType(column.pgType, enums),
|
|
944
|
+
tsType: postgresTypeToTsType(column, enums),
|
|
934
945
|
required: !column.nullable && !column.hasDefault,
|
|
935
946
|
description: generateFieldDescription(column, table)
|
|
936
947
|
};
|
|
@@ -943,16 +954,41 @@ function generateFieldContract(column, table) {
|
|
|
943
954
|
}
|
|
944
955
|
return field;
|
|
945
956
|
}
|
|
946
|
-
function postgresTypeToTsType(column) {
|
|
957
|
+
function postgresTypeToTsType(column, enums) {
|
|
958
|
+
const pgType = column.pgType.toLowerCase();
|
|
959
|
+
if (enums[pgType]) {
|
|
960
|
+
const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
|
|
961
|
+
if (column.nullable) {
|
|
962
|
+
return `${enumType} | null`;
|
|
963
|
+
}
|
|
964
|
+
return enumType;
|
|
965
|
+
}
|
|
966
|
+
if (pgType.startsWith("_")) {
|
|
967
|
+
const enumName = pgType.slice(1);
|
|
968
|
+
const enumValues = enums[enumName];
|
|
969
|
+
if (enumValues) {
|
|
970
|
+
const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
|
|
971
|
+
const arrayType = `(${enumType})[]`;
|
|
972
|
+
if (column.nullable) {
|
|
973
|
+
return `${arrayType} | null`;
|
|
974
|
+
}
|
|
975
|
+
return arrayType;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
947
978
|
const baseType = (() => {
|
|
948
|
-
switch (
|
|
979
|
+
switch (pgType) {
|
|
949
980
|
case "int":
|
|
981
|
+
case "int2":
|
|
982
|
+
case "int4":
|
|
983
|
+
case "int8":
|
|
950
984
|
case "integer":
|
|
951
985
|
case "smallint":
|
|
952
986
|
case "bigint":
|
|
953
987
|
case "decimal":
|
|
954
988
|
case "numeric":
|
|
955
989
|
case "real":
|
|
990
|
+
case "float4":
|
|
991
|
+
case "float8":
|
|
956
992
|
case "double precision":
|
|
957
993
|
case "float":
|
|
958
994
|
return "number";
|
|
@@ -970,9 +1006,16 @@ function postgresTypeToTsType(column) {
|
|
|
970
1006
|
return "string";
|
|
971
1007
|
case "text[]":
|
|
972
1008
|
case "varchar[]":
|
|
1009
|
+
case "_text":
|
|
1010
|
+
case "_varchar":
|
|
973
1011
|
return "string[]";
|
|
974
1012
|
case "int[]":
|
|
975
1013
|
case "integer[]":
|
|
1014
|
+
case "_int":
|
|
1015
|
+
case "_int2":
|
|
1016
|
+
case "_int4":
|
|
1017
|
+
case "_int8":
|
|
1018
|
+
case "_integer":
|
|
976
1019
|
return "number[]";
|
|
977
1020
|
default:
|
|
978
1021
|
return "string";
|
|
@@ -1054,7 +1097,7 @@ function generateExampleValue(column) {
|
|
|
1054
1097
|
return `'example value'`;
|
|
1055
1098
|
}
|
|
1056
1099
|
}
|
|
1057
|
-
function generateQueryParams(table) {
|
|
1100
|
+
function generateQueryParams(table, enums) {
|
|
1058
1101
|
const params = {
|
|
1059
1102
|
limit: "number - Max records to return (default: 50)",
|
|
1060
1103
|
offset: "number - Records to skip",
|
|
@@ -1065,7 +1108,7 @@ function generateQueryParams(table) {
|
|
|
1065
1108
|
for (const col of table.columns) {
|
|
1066
1109
|
if (filterCount >= 3)
|
|
1067
1110
|
break;
|
|
1068
|
-
const type = postgresTypeToJsonType(col.pgType);
|
|
1111
|
+
const type = postgresTypeToJsonType(col.pgType, enums);
|
|
1069
1112
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
|
1070
1113
|
if (type === "string") {
|
|
1071
1114
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
|
@@ -1078,15 +1121,27 @@ function generateQueryParams(table) {
|
|
|
1078
1121
|
params["..."] = "Additional filters for all fields";
|
|
1079
1122
|
return params;
|
|
1080
1123
|
}
|
|
1081
|
-
function postgresTypeToJsonType(pgType) {
|
|
1082
|
-
|
|
1124
|
+
function postgresTypeToJsonType(pgType, enums) {
|
|
1125
|
+
const t = pgType.toLowerCase();
|
|
1126
|
+
if (enums[t]) {
|
|
1127
|
+
return t;
|
|
1128
|
+
}
|
|
1129
|
+
if (t.startsWith("_") && enums[t.slice(1)]) {
|
|
1130
|
+
return `${t.slice(1)}[]`;
|
|
1131
|
+
}
|
|
1132
|
+
switch (t) {
|
|
1083
1133
|
case "int":
|
|
1134
|
+
case "int2":
|
|
1135
|
+
case "int4":
|
|
1136
|
+
case "int8":
|
|
1084
1137
|
case "integer":
|
|
1085
1138
|
case "smallint":
|
|
1086
1139
|
case "bigint":
|
|
1087
1140
|
case "decimal":
|
|
1088
1141
|
case "numeric":
|
|
1089
1142
|
case "real":
|
|
1143
|
+
case "float4":
|
|
1144
|
+
case "float8":
|
|
1090
1145
|
case "double precision":
|
|
1091
1146
|
case "float":
|
|
1092
1147
|
return "number";
|
|
@@ -1104,9 +1159,16 @@ function postgresTypeToJsonType(pgType) {
|
|
|
1104
1159
|
return "uuid";
|
|
1105
1160
|
case "text[]":
|
|
1106
1161
|
case "varchar[]":
|
|
1162
|
+
case "_text":
|
|
1163
|
+
case "_varchar":
|
|
1107
1164
|
return "string[]";
|
|
1108
1165
|
case "int[]":
|
|
1109
1166
|
case "integer[]":
|
|
1167
|
+
case "_int":
|
|
1168
|
+
case "_int2":
|
|
1169
|
+
case "_int4":
|
|
1170
|
+
case "_int8":
|
|
1171
|
+
case "_integer":
|
|
1110
1172
|
return "number[]";
|
|
1111
1173
|
default:
|
|
1112
1174
|
return "string";
|
|
@@ -2566,23 +2628,28 @@ export const buildWithFor = (t: TableName) =>
|
|
|
2566
2628
|
|
|
2567
2629
|
// src/emit-zod.ts
|
|
2568
2630
|
init_utils();
|
|
2569
|
-
function emitZod(table, opts) {
|
|
2631
|
+
function emitZod(table, opts, enums) {
|
|
2570
2632
|
const Type = pascal(table.name);
|
|
2571
2633
|
const zFor = (pg) => {
|
|
2572
|
-
|
|
2634
|
+
const t = pg.toLowerCase();
|
|
2635
|
+
if (enums[t]) {
|
|
2636
|
+
const values = enums[t].map((v) => `"${v}"`).join(", ");
|
|
2637
|
+
return `z.enum([${values}])`;
|
|
2638
|
+
}
|
|
2639
|
+
if (t === "uuid")
|
|
2573
2640
|
return `z.string()`;
|
|
2574
|
-
if (
|
|
2641
|
+
if (t === "bool" || t === "boolean")
|
|
2575
2642
|
return `z.boolean()`;
|
|
2576
|
-
if (
|
|
2643
|
+
if (t === "int2" || t === "int4" || t === "int8")
|
|
2577
2644
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
2578
|
-
if (
|
|
2645
|
+
if (t === "numeric" || t === "float4" || t === "float8")
|
|
2579
2646
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
2580
|
-
if (
|
|
2647
|
+
if (t === "jsonb" || t === "json")
|
|
2581
2648
|
return `z.unknown()`;
|
|
2582
|
-
if (
|
|
2649
|
+
if (t === "date" || t.startsWith("timestamp"))
|
|
2583
2650
|
return `z.string()`;
|
|
2584
|
-
if (
|
|
2585
|
-
return `z.array(${zFor(
|
|
2651
|
+
if (t.startsWith("_"))
|
|
2652
|
+
return `z.array(${zFor(t.slice(1))})`;
|
|
2586
2653
|
return `z.string()`;
|
|
2587
2654
|
};
|
|
2588
2655
|
const selectFields = table.columns.map((c) => {
|
|
@@ -2711,6 +2778,13 @@ const listSchema = z.object({
|
|
|
2711
2778
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
2712
2779
|
});
|
|
2713
2780
|
|
|
2781
|
+
/**
|
|
2782
|
+
* Register all CRUD routes for the ${fileTableName} table
|
|
2783
|
+
* @param app - Hono application instance
|
|
2784
|
+
* @param deps - Dependencies including database client and optional request hook
|
|
2785
|
+
* @param deps.pg - PostgreSQL client with query method
|
|
2786
|
+
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2787
|
+
*/
|
|
2714
2788
|
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> }) {
|
|
2715
2789
|
const base = "/v1/${fileTableName}";
|
|
2716
2790
|
|
|
@@ -2790,34 +2864,50 @@ ${hasAuth ? `
|
|
|
2790
2864
|
if (result.needsIncludes && result.includeSpec) {
|
|
2791
2865
|
try {
|
|
2792
2866
|
const stitched = await loadIncludes(
|
|
2793
|
-
"${fileTableName}",
|
|
2794
|
-
result.data,
|
|
2795
|
-
result.includeSpec,
|
|
2796
|
-
deps.pg,
|
|
2867
|
+
"${fileTableName}",
|
|
2868
|
+
result.data,
|
|
2869
|
+
result.includeSpec,
|
|
2870
|
+
deps.pg,
|
|
2797
2871
|
${opts.includeMethodsDepth}
|
|
2798
2872
|
);
|
|
2799
|
-
return c.json(
|
|
2873
|
+
return c.json({
|
|
2874
|
+
data: stitched,
|
|
2875
|
+
total: result.total,
|
|
2876
|
+
limit: result.limit,
|
|
2877
|
+
offset: result.offset,
|
|
2878
|
+
hasMore: result.hasMore
|
|
2879
|
+
});
|
|
2800
2880
|
} catch (e: any) {
|
|
2801
2881
|
const strict = process.env.SDK_STRICT_INCLUDE === "1";
|
|
2802
2882
|
if (strict) {
|
|
2803
|
-
return c.json({
|
|
2804
|
-
error: "include-stitch-failed",
|
|
2883
|
+
return c.json({
|
|
2884
|
+
error: "include-stitch-failed",
|
|
2805
2885
|
message: e?.message,
|
|
2806
2886
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2807
2887
|
}, 500);
|
|
2808
2888
|
}
|
|
2809
2889
|
// Non-strict: return base rows with error metadata
|
|
2810
|
-
return c.json({
|
|
2811
|
-
data: result.data,
|
|
2812
|
-
|
|
2890
|
+
return c.json({
|
|
2891
|
+
data: result.data,
|
|
2892
|
+
total: result.total,
|
|
2893
|
+
limit: result.limit,
|
|
2894
|
+
offset: result.offset,
|
|
2895
|
+
hasMore: result.hasMore,
|
|
2896
|
+
includeError: {
|
|
2813
2897
|
message: e?.message,
|
|
2814
2898
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2815
2899
|
}
|
|
2816
2900
|
}, 200);
|
|
2817
2901
|
}
|
|
2818
2902
|
}
|
|
2819
|
-
|
|
2820
|
-
return c.json(
|
|
2903
|
+
|
|
2904
|
+
return c.json({
|
|
2905
|
+
data: result.data,
|
|
2906
|
+
total: result.total,
|
|
2907
|
+
limit: result.limit,
|
|
2908
|
+
offset: result.offset,
|
|
2909
|
+
hasMore: result.hasMore
|
|
2910
|
+
}, result.status as any);
|
|
2821
2911
|
});
|
|
2822
2912
|
|
|
2823
2913
|
// UPDATE
|
|
@@ -2898,21 +2988,36 @@ function emitClient(table, graph, opts, model) {
|
|
|
2898
2988
|
for (const method of includeMethods) {
|
|
2899
2989
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2900
2990
|
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
2991
|
+
const relationshipDesc = method.path.map((p, i) => {
|
|
2992
|
+
const isLast = i === method.path.length - 1;
|
|
2993
|
+
const relation = method.isMany[i] ? "many" : "one";
|
|
2994
|
+
return isLast ? p : `${p} -> `;
|
|
2995
|
+
}).join("");
|
|
2901
2996
|
if (isGetByPk) {
|
|
2902
2997
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2903
2998
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
2904
2999
|
includeMethodsCode += `
|
|
3000
|
+
/**
|
|
3001
|
+
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
3002
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3003
|
+
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
3004
|
+
*/
|
|
2905
3005
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2906
|
-
const results = await this.post
|
|
3006
|
+
const results = await this.post<{ data: ${baseReturnType}[]; total: number; limit: number; offset: number; hasMore: boolean; }>(\`\${this.resource}/list\`, {
|
|
2907
3007
|
where: ${pkWhere},
|
|
2908
3008
|
include: ${JSON.stringify(method.includeSpec)},
|
|
2909
|
-
limit: 1
|
|
3009
|
+
limit: 1
|
|
2910
3010
|
});
|
|
2911
|
-
return (results[0] as ${baseReturnType}) ?? null;
|
|
3011
|
+
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
2912
3012
|
}
|
|
2913
3013
|
`;
|
|
2914
3014
|
} else {
|
|
2915
3015
|
includeMethodsCode += `
|
|
3016
|
+
/**
|
|
3017
|
+
* List ${table.name} records with included related ${relationshipDesc}
|
|
3018
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
3019
|
+
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
3020
|
+
*/
|
|
2916
3021
|
async ${method.name}(${baseParams}): Promise<${method.returnType}> {
|
|
2917
3022
|
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
2918
3023
|
}
|
|
@@ -2939,15 +3044,36 @@ ${otherTableImports.join(`
|
|
|
2939
3044
|
export class ${Type}Client extends BaseClient {
|
|
2940
3045
|
private readonly resource = "/v1/${table.name}";
|
|
2941
3046
|
|
|
3047
|
+
/**
|
|
3048
|
+
* Create a new ${table.name} record
|
|
3049
|
+
* @param data - The data to insert
|
|
3050
|
+
* @returns The created record
|
|
3051
|
+
*/
|
|
2942
3052
|
async create(data: Insert${Type}): Promise<Select${Type}> {
|
|
2943
3053
|
return this.post<Select${Type}>(this.resource, data);
|
|
2944
3054
|
}
|
|
2945
3055
|
|
|
3056
|
+
/**
|
|
3057
|
+
* Get a ${table.name} record by primary key
|
|
3058
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3059
|
+
* @returns The record if found, null otherwise
|
|
3060
|
+
*/
|
|
2946
3061
|
async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2947
3062
|
const path = ${pkPathExpr};
|
|
2948
3063
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
2949
3064
|
}
|
|
2950
3065
|
|
|
3066
|
+
/**
|
|
3067
|
+
* List ${table.name} records with pagination and filtering
|
|
3068
|
+
* @param params - Query parameters
|
|
3069
|
+
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3070
|
+
* @param params.orderBy - Column(s) to sort by
|
|
3071
|
+
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3072
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 100)
|
|
3073
|
+
* @param params.offset - Number of records to skip for pagination
|
|
3074
|
+
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
3075
|
+
* @returns Paginated results with data, total count, and hasMore flag
|
|
3076
|
+
*/
|
|
2951
3077
|
async list(params?: {
|
|
2952
3078
|
include?: any;
|
|
2953
3079
|
limit?: number;
|
|
@@ -2955,15 +3081,38 @@ export class ${Type}Client extends BaseClient {
|
|
|
2955
3081
|
where?: Where<Select${Type}>;
|
|
2956
3082
|
orderBy?: string | string[];
|
|
2957
3083
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2958
|
-
}): Promise<
|
|
2959
|
-
|
|
3084
|
+
}): Promise<{
|
|
3085
|
+
data: Select${Type}[];
|
|
3086
|
+
total: number;
|
|
3087
|
+
limit: number;
|
|
3088
|
+
offset: number;
|
|
3089
|
+
hasMore: boolean;
|
|
3090
|
+
}> {
|
|
3091
|
+
return this.post<{
|
|
3092
|
+
data: Select${Type}[];
|
|
3093
|
+
total: number;
|
|
3094
|
+
limit: number;
|
|
3095
|
+
offset: number;
|
|
3096
|
+
hasMore: boolean;
|
|
3097
|
+
}>(\`\${this.resource}/list\`, params ?? {});
|
|
2960
3098
|
}
|
|
2961
3099
|
|
|
3100
|
+
/**
|
|
3101
|
+
* Update a ${table.name} record by primary key
|
|
3102
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3103
|
+
* @param patch - Partial data to update
|
|
3104
|
+
* @returns The updated record if found, null otherwise
|
|
3105
|
+
*/
|
|
2962
3106
|
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
|
|
2963
3107
|
const path = ${pkPathExpr};
|
|
2964
3108
|
return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
|
|
2965
3109
|
}
|
|
2966
3110
|
|
|
3111
|
+
/**
|
|
3112
|
+
* Delete a ${table.name} record by primary key
|
|
3113
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3114
|
+
* @returns The deleted record if found, null otherwise
|
|
3115
|
+
*/
|
|
2967
3116
|
async delete(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2968
3117
|
const path = ${pkPathExpr};
|
|
2969
3118
|
return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
@@ -3510,10 +3659,13 @@ export async function loadIncludes(
|
|
|
3510
3659
|
}
|
|
3511
3660
|
|
|
3512
3661
|
// src/emit-types.ts
|
|
3513
|
-
function tsTypeFor(pgType, opts) {
|
|
3662
|
+
function tsTypeFor(pgType, opts, enums) {
|
|
3514
3663
|
const t = pgType.toLowerCase();
|
|
3664
|
+
if (enums[t]) {
|
|
3665
|
+
return enums[t].map((v) => `"${v}"`).join(" | ");
|
|
3666
|
+
}
|
|
3515
3667
|
if (t.startsWith("_"))
|
|
3516
|
-
return
|
|
3668
|
+
return `(${tsTypeFor(t.slice(1), opts, enums)})[]`;
|
|
3517
3669
|
if (t === "uuid")
|
|
3518
3670
|
return "string";
|
|
3519
3671
|
if (t === "bool" || t === "boolean")
|
|
@@ -3528,17 +3680,17 @@ function tsTypeFor(pgType, opts) {
|
|
|
3528
3680
|
return "string";
|
|
3529
3681
|
}
|
|
3530
3682
|
var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
3531
|
-
function emitTypes(table, opts) {
|
|
3683
|
+
function emitTypes(table, opts, enums) {
|
|
3532
3684
|
const Type = pascal2(table.name);
|
|
3533
3685
|
const insertFields = table.columns.map((col) => {
|
|
3534
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
3686
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
3535
3687
|
const optional = col.hasDefault || col.nullable ? "?" : "";
|
|
3536
3688
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
3537
3689
|
return ` ${col.name}${optional}: ${valueType};`;
|
|
3538
3690
|
}).join(`
|
|
3539
3691
|
`);
|
|
3540
3692
|
const selectFields = table.columns.map((col) => {
|
|
3541
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
3693
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
3542
3694
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
3543
3695
|
return ` ${col.name}: ${valueType};`;
|
|
3544
3696
|
}).join(`
|
|
@@ -3551,12 +3703,25 @@ function emitTypes(table, opts) {
|
|
|
3551
3703
|
*
|
|
3552
3704
|
* To make changes, modify your schema or configuration and regenerate.
|
|
3553
3705
|
*/
|
|
3706
|
+
|
|
3707
|
+
/**
|
|
3708
|
+
* Type for inserting a new ${table.name} record.
|
|
3709
|
+
* Fields with defaults or nullable columns are optional.
|
|
3710
|
+
*/
|
|
3554
3711
|
export type Insert${Type} = {
|
|
3555
3712
|
${insertFields}
|
|
3556
3713
|
};
|
|
3557
3714
|
|
|
3715
|
+
/**
|
|
3716
|
+
* Type for updating an existing ${table.name} record.
|
|
3717
|
+
* All fields are optional, allowing partial updates.
|
|
3718
|
+
*/
|
|
3558
3719
|
export type Update${Type} = Partial<Insert${Type}>;
|
|
3559
3720
|
|
|
3721
|
+
/**
|
|
3722
|
+
* Type representing a ${table.name} record from the database.
|
|
3723
|
+
* All fields are included as returned by SELECT queries.
|
|
3724
|
+
*/
|
|
3560
3725
|
export type Select${Type} = {
|
|
3561
3726
|
${selectFields}
|
|
3562
3727
|
};
|
|
@@ -4289,7 +4454,7 @@ function buildWhereClause(
|
|
|
4289
4454
|
export async function listRecords(
|
|
4290
4455
|
ctx: OperationContext,
|
|
4291
4456
|
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
4292
|
-
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4457
|
+
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4293
4458
|
try {
|
|
4294
4459
|
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
4295
4460
|
|
|
@@ -4334,20 +4499,34 @@ export async function listRecords(
|
|
|
4334
4499
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
4335
4500
|
const allParams = [...whereParams, limit, offset];
|
|
4336
4501
|
|
|
4502
|
+
// Get total count for pagination
|
|
4503
|
+
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${whereSQL}\`;
|
|
4504
|
+
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
|
|
4505
|
+
const countResult = await ctx.pg.query(countText, whereParams);
|
|
4506
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
4507
|
+
|
|
4508
|
+
// Get paginated data
|
|
4337
4509
|
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
4338
4510
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
4339
4511
|
|
|
4340
4512
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
4341
4513
|
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4514
|
+
// Calculate hasMore
|
|
4515
|
+
const hasMore = offset + limit < total;
|
|
4516
|
+
|
|
4517
|
+
const metadata = {
|
|
4518
|
+
data: rows,
|
|
4519
|
+
total,
|
|
4520
|
+
limit,
|
|
4521
|
+
offset,
|
|
4522
|
+
hasMore,
|
|
4523
|
+
needsIncludes: !!include,
|
|
4524
|
+
includeSpec: include,
|
|
4525
|
+
status: 200
|
|
4526
|
+
};
|
|
4346
4527
|
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
log.debug(\`LIST \${ctx.table} include spec:\`, include);
|
|
4350
|
-
return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
|
|
4528
|
+
log.debug(\`LIST \${ctx.table} result: \${rows.length} rows, \${total} total, hasMore=\${hasMore}\`);
|
|
4529
|
+
return metadata;
|
|
4351
4530
|
} catch (e: any) {
|
|
4352
4531
|
log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
|
|
4353
4532
|
return {
|
|
@@ -4489,8 +4668,11 @@ describe('${Type} SDK Operations', () => {
|
|
|
4489
4668
|
});
|
|
4490
4669
|
|
|
4491
4670
|
it('should list ${tableName} relationships', async () => {
|
|
4492
|
-
const
|
|
4493
|
-
expect(
|
|
4671
|
+
const result = await sdk.${tableName}.list({ limit: 10 });
|
|
4672
|
+
expect(result).toBeDefined();
|
|
4673
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
4674
|
+
expect(typeof result.total).toBe('number');
|
|
4675
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
4494
4676
|
});
|
|
4495
4677
|
});
|
|
4496
4678
|
`;
|
|
@@ -5126,8 +5308,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
|
5126
5308
|
});
|
|
5127
5309
|
|
|
5128
5310
|
it('should list ${table.name}', async () => {
|
|
5129
|
-
const
|
|
5130
|
-
expect(
|
|
5311
|
+
const result = await sdk.${table.name}.list({ limit: 10 });
|
|
5312
|
+
expect(result).toBeDefined();
|
|
5313
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
5314
|
+
expect(typeof result.total).toBe('number');
|
|
5315
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
5131
5316
|
});
|
|
5132
5317
|
|
|
5133
5318
|
${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
|
|
@@ -5276,10 +5461,10 @@ async function generate(configPath) {
|
|
|
5276
5461
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
5277
5462
|
}
|
|
5278
5463
|
for (const table of Object.values(model.tables)) {
|
|
5279
|
-
const typesSrc = emitTypes(table, { numericMode: "string" });
|
|
5464
|
+
const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
|
|
5280
5465
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5281
5466
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
5282
|
-
const zodSrc = emitZod(table, { numericMode: "string" });
|
|
5467
|
+
const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
|
|
5283
5468
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5284
5469
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
5285
5470
|
const paramsZodSrc = emitParamsZod(table, graph);
|
package/dist/emit-types.d.ts
CHANGED
package/dist/emit-zod.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -577,7 +577,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
577
577
|
path: newPath,
|
|
578
578
|
isMany: newIsMany,
|
|
579
579
|
targets: newTargets,
|
|
580
|
-
returnType: `(${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]`,
|
|
580
|
+
returnType: `{ data: (${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)})[]; total: number; limit: number; offset: number; hasMore: boolean; }`,
|
|
581
581
|
includeSpec: buildIncludeSpec(newPath)
|
|
582
582
|
});
|
|
583
583
|
methods.push({
|
|
@@ -617,7 +617,7 @@ function generateIncludeMethods(table, graph, opts, allTables) {
|
|
|
617
617
|
path: combinedPath,
|
|
618
618
|
isMany: [edge1.kind === "many", edge2.kind === "many"],
|
|
619
619
|
targets: [edge1.target, edge2.target],
|
|
620
|
-
returnType: `(Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]`,
|
|
620
|
+
returnType: `{ data: (Select${pascal(baseTableName)} & { ${type1}; ${type2} })[]; total: number; limit: number; offset: number; hasMore: boolean; }`,
|
|
621
621
|
includeSpec: { [key1]: true, [key2]: true }
|
|
622
622
|
});
|
|
623
623
|
methods.push({
|
|
@@ -779,14 +779,18 @@ 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({
|
|
785
786
|
name: "list",
|
|
786
|
-
signature: `list(params?: ListParams): Promise
|
|
787
|
-
description: `List ${tableName} with filtering, sorting, and pagination
|
|
787
|
+
signature: `list(params?: ListParams): Promise<{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }>`,
|
|
788
|
+
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
788
789
|
example: `// Get all ${tableName}
|
|
789
|
-
const
|
|
790
|
+
const result = await sdk.${tableName}.list();
|
|
791
|
+
console.log(result.data); // array of records
|
|
792
|
+
console.log(result.total); // total matching records
|
|
793
|
+
console.log(result.hasMore); // true if more pages available
|
|
790
794
|
|
|
791
795
|
// With filters and pagination
|
|
792
796
|
const filtered = await sdk.${tableName}.list({
|
|
@@ -795,15 +799,19 @@ const filtered = await sdk.${tableName}.list({
|
|
|
795
799
|
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
796
800
|
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
797
801
|
order: 'desc'
|
|
798
|
-
})
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Calculate total pages
|
|
805
|
+
const totalPages = Math.ceil(filtered.total / filtered.limit);
|
|
806
|
+
const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
799
807
|
correspondsTo: `GET ${basePath}`
|
|
800
808
|
});
|
|
801
809
|
endpoints.push({
|
|
802
810
|
method: "GET",
|
|
803
811
|
path: basePath,
|
|
804
|
-
description: `List all ${tableName} records`,
|
|
805
|
-
queryParameters: generateQueryParams(table),
|
|
806
|
-
responseBody:
|
|
812
|
+
description: `List all ${tableName} records with pagination metadata`,
|
|
813
|
+
queryParameters: generateQueryParams(table, enums),
|
|
814
|
+
responseBody: `{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }`
|
|
807
815
|
});
|
|
808
816
|
if (hasSinglePK) {
|
|
809
817
|
sdkMethods.push({
|
|
@@ -893,7 +901,10 @@ console.log('Deleted:', deleted);`,
|
|
|
893
901
|
}, allTables);
|
|
894
902
|
for (const method of includeMethods) {
|
|
895
903
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
896
|
-
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const
|
|
904
|
+
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const result = await sdk.${tableName}.${method.name}();
|
|
905
|
+
console.log(result.data); // array of records with includes
|
|
906
|
+
console.log(result.total); // total count
|
|
907
|
+
console.log(result.hasMore); // more pages available
|
|
897
908
|
|
|
898
909
|
// With filters and pagination
|
|
899
910
|
const filtered = await sdk.${tableName}.${method.name}({
|
|
@@ -910,7 +921,7 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
910
921
|
});
|
|
911
922
|
}
|
|
912
923
|
}
|
|
913
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
|
924
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table, enums));
|
|
914
925
|
return {
|
|
915
926
|
name: Type,
|
|
916
927
|
tableName,
|
|
@@ -925,11 +936,11 @@ const filtered = await sdk.${tableName}.${method.name}({
|
|
|
925
936
|
fields
|
|
926
937
|
};
|
|
927
938
|
}
|
|
928
|
-
function generateFieldContract(column, table) {
|
|
939
|
+
function generateFieldContract(column, table, enums) {
|
|
929
940
|
const field = {
|
|
930
941
|
name: column.name,
|
|
931
|
-
type: postgresTypeToJsonType(column.pgType),
|
|
932
|
-
tsType: postgresTypeToTsType(column),
|
|
942
|
+
type: postgresTypeToJsonType(column.pgType, enums),
|
|
943
|
+
tsType: postgresTypeToTsType(column, enums),
|
|
933
944
|
required: !column.nullable && !column.hasDefault,
|
|
934
945
|
description: generateFieldDescription(column, table)
|
|
935
946
|
};
|
|
@@ -942,16 +953,41 @@ function generateFieldContract(column, table) {
|
|
|
942
953
|
}
|
|
943
954
|
return field;
|
|
944
955
|
}
|
|
945
|
-
function postgresTypeToTsType(column) {
|
|
956
|
+
function postgresTypeToTsType(column, enums) {
|
|
957
|
+
const pgType = column.pgType.toLowerCase();
|
|
958
|
+
if (enums[pgType]) {
|
|
959
|
+
const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
|
|
960
|
+
if (column.nullable) {
|
|
961
|
+
return `${enumType} | null`;
|
|
962
|
+
}
|
|
963
|
+
return enumType;
|
|
964
|
+
}
|
|
965
|
+
if (pgType.startsWith("_")) {
|
|
966
|
+
const enumName = pgType.slice(1);
|
|
967
|
+
const enumValues = enums[enumName];
|
|
968
|
+
if (enumValues) {
|
|
969
|
+
const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
|
|
970
|
+
const arrayType = `(${enumType})[]`;
|
|
971
|
+
if (column.nullable) {
|
|
972
|
+
return `${arrayType} | null`;
|
|
973
|
+
}
|
|
974
|
+
return arrayType;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
946
977
|
const baseType = (() => {
|
|
947
|
-
switch (
|
|
978
|
+
switch (pgType) {
|
|
948
979
|
case "int":
|
|
980
|
+
case "int2":
|
|
981
|
+
case "int4":
|
|
982
|
+
case "int8":
|
|
949
983
|
case "integer":
|
|
950
984
|
case "smallint":
|
|
951
985
|
case "bigint":
|
|
952
986
|
case "decimal":
|
|
953
987
|
case "numeric":
|
|
954
988
|
case "real":
|
|
989
|
+
case "float4":
|
|
990
|
+
case "float8":
|
|
955
991
|
case "double precision":
|
|
956
992
|
case "float":
|
|
957
993
|
return "number";
|
|
@@ -969,9 +1005,16 @@ function postgresTypeToTsType(column) {
|
|
|
969
1005
|
return "string";
|
|
970
1006
|
case "text[]":
|
|
971
1007
|
case "varchar[]":
|
|
1008
|
+
case "_text":
|
|
1009
|
+
case "_varchar":
|
|
972
1010
|
return "string[]";
|
|
973
1011
|
case "int[]":
|
|
974
1012
|
case "integer[]":
|
|
1013
|
+
case "_int":
|
|
1014
|
+
case "_int2":
|
|
1015
|
+
case "_int4":
|
|
1016
|
+
case "_int8":
|
|
1017
|
+
case "_integer":
|
|
975
1018
|
return "number[]";
|
|
976
1019
|
default:
|
|
977
1020
|
return "string";
|
|
@@ -1053,7 +1096,7 @@ function generateExampleValue(column) {
|
|
|
1053
1096
|
return `'example value'`;
|
|
1054
1097
|
}
|
|
1055
1098
|
}
|
|
1056
|
-
function generateQueryParams(table) {
|
|
1099
|
+
function generateQueryParams(table, enums) {
|
|
1057
1100
|
const params = {
|
|
1058
1101
|
limit: "number - Max records to return (default: 50)",
|
|
1059
1102
|
offset: "number - Records to skip",
|
|
@@ -1064,7 +1107,7 @@ function generateQueryParams(table) {
|
|
|
1064
1107
|
for (const col of table.columns) {
|
|
1065
1108
|
if (filterCount >= 3)
|
|
1066
1109
|
break;
|
|
1067
|
-
const type = postgresTypeToJsonType(col.pgType);
|
|
1110
|
+
const type = postgresTypeToJsonType(col.pgType, enums);
|
|
1068
1111
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
|
1069
1112
|
if (type === "string") {
|
|
1070
1113
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
|
@@ -1077,15 +1120,27 @@ function generateQueryParams(table) {
|
|
|
1077
1120
|
params["..."] = "Additional filters for all fields";
|
|
1078
1121
|
return params;
|
|
1079
1122
|
}
|
|
1080
|
-
function postgresTypeToJsonType(pgType) {
|
|
1081
|
-
|
|
1123
|
+
function postgresTypeToJsonType(pgType, enums) {
|
|
1124
|
+
const t = pgType.toLowerCase();
|
|
1125
|
+
if (enums[t]) {
|
|
1126
|
+
return t;
|
|
1127
|
+
}
|
|
1128
|
+
if (t.startsWith("_") && enums[t.slice(1)]) {
|
|
1129
|
+
return `${t.slice(1)}[]`;
|
|
1130
|
+
}
|
|
1131
|
+
switch (t) {
|
|
1082
1132
|
case "int":
|
|
1133
|
+
case "int2":
|
|
1134
|
+
case "int4":
|
|
1135
|
+
case "int8":
|
|
1083
1136
|
case "integer":
|
|
1084
1137
|
case "smallint":
|
|
1085
1138
|
case "bigint":
|
|
1086
1139
|
case "decimal":
|
|
1087
1140
|
case "numeric":
|
|
1088
1141
|
case "real":
|
|
1142
|
+
case "float4":
|
|
1143
|
+
case "float8":
|
|
1089
1144
|
case "double precision":
|
|
1090
1145
|
case "float":
|
|
1091
1146
|
return "number";
|
|
@@ -1103,9 +1158,16 @@ function postgresTypeToJsonType(pgType) {
|
|
|
1103
1158
|
return "uuid";
|
|
1104
1159
|
case "text[]":
|
|
1105
1160
|
case "varchar[]":
|
|
1161
|
+
case "_text":
|
|
1162
|
+
case "_varchar":
|
|
1106
1163
|
return "string[]";
|
|
1107
1164
|
case "int[]":
|
|
1108
1165
|
case "integer[]":
|
|
1166
|
+
case "_int":
|
|
1167
|
+
case "_int2":
|
|
1168
|
+
case "_int4":
|
|
1169
|
+
case "_int8":
|
|
1170
|
+
case "_integer":
|
|
1109
1171
|
return "number[]";
|
|
1110
1172
|
default:
|
|
1111
1173
|
return "string";
|
|
@@ -1806,23 +1868,28 @@ export const buildWithFor = (t: TableName) =>
|
|
|
1806
1868
|
|
|
1807
1869
|
// src/emit-zod.ts
|
|
1808
1870
|
init_utils();
|
|
1809
|
-
function emitZod(table, opts) {
|
|
1871
|
+
function emitZod(table, opts, enums) {
|
|
1810
1872
|
const Type = pascal(table.name);
|
|
1811
1873
|
const zFor = (pg) => {
|
|
1812
|
-
|
|
1874
|
+
const t = pg.toLowerCase();
|
|
1875
|
+
if (enums[t]) {
|
|
1876
|
+
const values = enums[t].map((v) => `"${v}"`).join(", ");
|
|
1877
|
+
return `z.enum([${values}])`;
|
|
1878
|
+
}
|
|
1879
|
+
if (t === "uuid")
|
|
1813
1880
|
return `z.string()`;
|
|
1814
|
-
if (
|
|
1881
|
+
if (t === "bool" || t === "boolean")
|
|
1815
1882
|
return `z.boolean()`;
|
|
1816
|
-
if (
|
|
1883
|
+
if (t === "int2" || t === "int4" || t === "int8")
|
|
1817
1884
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
1818
|
-
if (
|
|
1885
|
+
if (t === "numeric" || t === "float4" || t === "float8")
|
|
1819
1886
|
return opts.numericMode === "number" ? `z.number()` : `z.string()`;
|
|
1820
|
-
if (
|
|
1887
|
+
if (t === "jsonb" || t === "json")
|
|
1821
1888
|
return `z.unknown()`;
|
|
1822
|
-
if (
|
|
1889
|
+
if (t === "date" || t.startsWith("timestamp"))
|
|
1823
1890
|
return `z.string()`;
|
|
1824
|
-
if (
|
|
1825
|
-
return `z.array(${zFor(
|
|
1891
|
+
if (t.startsWith("_"))
|
|
1892
|
+
return `z.array(${zFor(t.slice(1))})`;
|
|
1826
1893
|
return `z.string()`;
|
|
1827
1894
|
};
|
|
1828
1895
|
const selectFields = table.columns.map((c) => {
|
|
@@ -1951,6 +2018,13 @@ const listSchema = z.object({
|
|
|
1951
2018
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
1952
2019
|
});
|
|
1953
2020
|
|
|
2021
|
+
/**
|
|
2022
|
+
* Register all CRUD routes for the ${fileTableName} table
|
|
2023
|
+
* @param app - Hono application instance
|
|
2024
|
+
* @param deps - Dependencies including database client and optional request hook
|
|
2025
|
+
* @param deps.pg - PostgreSQL client with query method
|
|
2026
|
+
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2027
|
+
*/
|
|
1954
2028
|
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> }) {
|
|
1955
2029
|
const base = "/v1/${fileTableName}";
|
|
1956
2030
|
|
|
@@ -2030,34 +2104,50 @@ ${hasAuth ? `
|
|
|
2030
2104
|
if (result.needsIncludes && result.includeSpec) {
|
|
2031
2105
|
try {
|
|
2032
2106
|
const stitched = await loadIncludes(
|
|
2033
|
-
"${fileTableName}",
|
|
2034
|
-
result.data,
|
|
2035
|
-
result.includeSpec,
|
|
2036
|
-
deps.pg,
|
|
2107
|
+
"${fileTableName}",
|
|
2108
|
+
result.data,
|
|
2109
|
+
result.includeSpec,
|
|
2110
|
+
deps.pg,
|
|
2037
2111
|
${opts.includeMethodsDepth}
|
|
2038
2112
|
);
|
|
2039
|
-
return c.json(
|
|
2113
|
+
return c.json({
|
|
2114
|
+
data: stitched,
|
|
2115
|
+
total: result.total,
|
|
2116
|
+
limit: result.limit,
|
|
2117
|
+
offset: result.offset,
|
|
2118
|
+
hasMore: result.hasMore
|
|
2119
|
+
});
|
|
2040
2120
|
} catch (e: any) {
|
|
2041
2121
|
const strict = process.env.SDK_STRICT_INCLUDE === "1";
|
|
2042
2122
|
if (strict) {
|
|
2043
|
-
return c.json({
|
|
2044
|
-
error: "include-stitch-failed",
|
|
2123
|
+
return c.json({
|
|
2124
|
+
error: "include-stitch-failed",
|
|
2045
2125
|
message: e?.message,
|
|
2046
2126
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2047
2127
|
}, 500);
|
|
2048
2128
|
}
|
|
2049
2129
|
// Non-strict: return base rows with error metadata
|
|
2050
|
-
return c.json({
|
|
2051
|
-
data: result.data,
|
|
2052
|
-
|
|
2130
|
+
return c.json({
|
|
2131
|
+
data: result.data,
|
|
2132
|
+
total: result.total,
|
|
2133
|
+
limit: result.limit,
|
|
2134
|
+
offset: result.offset,
|
|
2135
|
+
hasMore: result.hasMore,
|
|
2136
|
+
includeError: {
|
|
2053
2137
|
message: e?.message,
|
|
2054
2138
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2055
2139
|
}
|
|
2056
2140
|
}, 200);
|
|
2057
2141
|
}
|
|
2058
2142
|
}
|
|
2059
|
-
|
|
2060
|
-
return c.json(
|
|
2143
|
+
|
|
2144
|
+
return c.json({
|
|
2145
|
+
data: result.data,
|
|
2146
|
+
total: result.total,
|
|
2147
|
+
limit: result.limit,
|
|
2148
|
+
offset: result.offset,
|
|
2149
|
+
hasMore: result.hasMore
|
|
2150
|
+
}, result.status as any);
|
|
2061
2151
|
});
|
|
2062
2152
|
|
|
2063
2153
|
// UPDATE
|
|
@@ -2138,21 +2228,36 @@ function emitClient(table, graph, opts, model) {
|
|
|
2138
2228
|
for (const method of includeMethods) {
|
|
2139
2229
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2140
2230
|
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
2231
|
+
const relationshipDesc = method.path.map((p, i) => {
|
|
2232
|
+
const isLast = i === method.path.length - 1;
|
|
2233
|
+
const relation = method.isMany[i] ? "many" : "one";
|
|
2234
|
+
return isLast ? p : `${p} -> `;
|
|
2235
|
+
}).join("");
|
|
2141
2236
|
if (isGetByPk) {
|
|
2142
2237
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2143
2238
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
2144
2239
|
includeMethodsCode += `
|
|
2240
|
+
/**
|
|
2241
|
+
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
2242
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2243
|
+
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
2244
|
+
*/
|
|
2145
2245
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2146
|
-
const results = await this.post
|
|
2246
|
+
const results = await this.post<{ data: ${baseReturnType}[]; total: number; limit: number; offset: number; hasMore: boolean; }>(\`\${this.resource}/list\`, {
|
|
2147
2247
|
where: ${pkWhere},
|
|
2148
2248
|
include: ${JSON.stringify(method.includeSpec)},
|
|
2149
|
-
limit: 1
|
|
2249
|
+
limit: 1
|
|
2150
2250
|
});
|
|
2151
|
-
return (results[0] as ${baseReturnType}) ?? null;
|
|
2251
|
+
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
2152
2252
|
}
|
|
2153
2253
|
`;
|
|
2154
2254
|
} else {
|
|
2155
2255
|
includeMethodsCode += `
|
|
2256
|
+
/**
|
|
2257
|
+
* List ${table.name} records with included related ${relationshipDesc}
|
|
2258
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
2259
|
+
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
2260
|
+
*/
|
|
2156
2261
|
async ${method.name}(${baseParams}): Promise<${method.returnType}> {
|
|
2157
2262
|
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
2158
2263
|
}
|
|
@@ -2179,15 +2284,36 @@ ${otherTableImports.join(`
|
|
|
2179
2284
|
export class ${Type}Client extends BaseClient {
|
|
2180
2285
|
private readonly resource = "/v1/${table.name}";
|
|
2181
2286
|
|
|
2287
|
+
/**
|
|
2288
|
+
* Create a new ${table.name} record
|
|
2289
|
+
* @param data - The data to insert
|
|
2290
|
+
* @returns The created record
|
|
2291
|
+
*/
|
|
2182
2292
|
async create(data: Insert${Type}): Promise<Select${Type}> {
|
|
2183
2293
|
return this.post<Select${Type}>(this.resource, data);
|
|
2184
2294
|
}
|
|
2185
2295
|
|
|
2296
|
+
/**
|
|
2297
|
+
* Get a ${table.name} record by primary key
|
|
2298
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2299
|
+
* @returns The record if found, null otherwise
|
|
2300
|
+
*/
|
|
2186
2301
|
async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2187
2302
|
const path = ${pkPathExpr};
|
|
2188
2303
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
2189
2304
|
}
|
|
2190
2305
|
|
|
2306
|
+
/**
|
|
2307
|
+
* List ${table.name} records with pagination and filtering
|
|
2308
|
+
* @param params - Query parameters
|
|
2309
|
+
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
2310
|
+
* @param params.orderBy - Column(s) to sort by
|
|
2311
|
+
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
2312
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 100)
|
|
2313
|
+
* @param params.offset - Number of records to skip for pagination
|
|
2314
|
+
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
2315
|
+
* @returns Paginated results with data, total count, and hasMore flag
|
|
2316
|
+
*/
|
|
2191
2317
|
async list(params?: {
|
|
2192
2318
|
include?: any;
|
|
2193
2319
|
limit?: number;
|
|
@@ -2195,15 +2321,38 @@ export class ${Type}Client extends BaseClient {
|
|
|
2195
2321
|
where?: Where<Select${Type}>;
|
|
2196
2322
|
orderBy?: string | string[];
|
|
2197
2323
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2198
|
-
}): Promise<
|
|
2199
|
-
|
|
2324
|
+
}): Promise<{
|
|
2325
|
+
data: Select${Type}[];
|
|
2326
|
+
total: number;
|
|
2327
|
+
limit: number;
|
|
2328
|
+
offset: number;
|
|
2329
|
+
hasMore: boolean;
|
|
2330
|
+
}> {
|
|
2331
|
+
return this.post<{
|
|
2332
|
+
data: Select${Type}[];
|
|
2333
|
+
total: number;
|
|
2334
|
+
limit: number;
|
|
2335
|
+
offset: number;
|
|
2336
|
+
hasMore: boolean;
|
|
2337
|
+
}>(\`\${this.resource}/list\`, params ?? {});
|
|
2200
2338
|
}
|
|
2201
2339
|
|
|
2340
|
+
/**
|
|
2341
|
+
* Update a ${table.name} record by primary key
|
|
2342
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2343
|
+
* @param patch - Partial data to update
|
|
2344
|
+
* @returns The updated record if found, null otherwise
|
|
2345
|
+
*/
|
|
2202
2346
|
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
|
|
2203
2347
|
const path = ${pkPathExpr};
|
|
2204
2348
|
return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
|
|
2205
2349
|
}
|
|
2206
2350
|
|
|
2351
|
+
/**
|
|
2352
|
+
* Delete a ${table.name} record by primary key
|
|
2353
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2354
|
+
* @returns The deleted record if found, null otherwise
|
|
2355
|
+
*/
|
|
2207
2356
|
async delete(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2208
2357
|
const path = ${pkPathExpr};
|
|
2209
2358
|
return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
@@ -2750,10 +2899,13 @@ export async function loadIncludes(
|
|
|
2750
2899
|
}
|
|
2751
2900
|
|
|
2752
2901
|
// src/emit-types.ts
|
|
2753
|
-
function tsTypeFor(pgType, opts) {
|
|
2902
|
+
function tsTypeFor(pgType, opts, enums) {
|
|
2754
2903
|
const t = pgType.toLowerCase();
|
|
2904
|
+
if (enums[t]) {
|
|
2905
|
+
return enums[t].map((v) => `"${v}"`).join(" | ");
|
|
2906
|
+
}
|
|
2755
2907
|
if (t.startsWith("_"))
|
|
2756
|
-
return
|
|
2908
|
+
return `(${tsTypeFor(t.slice(1), opts, enums)})[]`;
|
|
2757
2909
|
if (t === "uuid")
|
|
2758
2910
|
return "string";
|
|
2759
2911
|
if (t === "bool" || t === "boolean")
|
|
@@ -2768,17 +2920,17 @@ function tsTypeFor(pgType, opts) {
|
|
|
2768
2920
|
return "string";
|
|
2769
2921
|
}
|
|
2770
2922
|
var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
2771
|
-
function emitTypes(table, opts) {
|
|
2923
|
+
function emitTypes(table, opts, enums) {
|
|
2772
2924
|
const Type = pascal2(table.name);
|
|
2773
2925
|
const insertFields = table.columns.map((col) => {
|
|
2774
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
2926
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
2775
2927
|
const optional = col.hasDefault || col.nullable ? "?" : "";
|
|
2776
2928
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
2777
2929
|
return ` ${col.name}${optional}: ${valueType};`;
|
|
2778
2930
|
}).join(`
|
|
2779
2931
|
`);
|
|
2780
2932
|
const selectFields = table.columns.map((col) => {
|
|
2781
|
-
const base = tsTypeFor(col.pgType, opts);
|
|
2933
|
+
const base = tsTypeFor(col.pgType, opts, enums);
|
|
2782
2934
|
const valueType = col.nullable ? `${base} | null` : base;
|
|
2783
2935
|
return ` ${col.name}: ${valueType};`;
|
|
2784
2936
|
}).join(`
|
|
@@ -2791,12 +2943,25 @@ function emitTypes(table, opts) {
|
|
|
2791
2943
|
*
|
|
2792
2944
|
* To make changes, modify your schema or configuration and regenerate.
|
|
2793
2945
|
*/
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Type for inserting a new ${table.name} record.
|
|
2949
|
+
* Fields with defaults or nullable columns are optional.
|
|
2950
|
+
*/
|
|
2794
2951
|
export type Insert${Type} = {
|
|
2795
2952
|
${insertFields}
|
|
2796
2953
|
};
|
|
2797
2954
|
|
|
2955
|
+
/**
|
|
2956
|
+
* Type for updating an existing ${table.name} record.
|
|
2957
|
+
* All fields are optional, allowing partial updates.
|
|
2958
|
+
*/
|
|
2798
2959
|
export type Update${Type} = Partial<Insert${Type}>;
|
|
2799
2960
|
|
|
2961
|
+
/**
|
|
2962
|
+
* Type representing a ${table.name} record from the database.
|
|
2963
|
+
* All fields are included as returned by SELECT queries.
|
|
2964
|
+
*/
|
|
2800
2965
|
export type Select${Type} = {
|
|
2801
2966
|
${selectFields}
|
|
2802
2967
|
};
|
|
@@ -3529,7 +3694,7 @@ function buildWhereClause(
|
|
|
3529
3694
|
export async function listRecords(
|
|
3530
3695
|
ctx: OperationContext,
|
|
3531
3696
|
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
3532
|
-
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3697
|
+
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3533
3698
|
try {
|
|
3534
3699
|
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
3535
3700
|
|
|
@@ -3574,20 +3739,34 @@ export async function listRecords(
|
|
|
3574
3739
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
3575
3740
|
const allParams = [...whereParams, limit, offset];
|
|
3576
3741
|
|
|
3742
|
+
// Get total count for pagination
|
|
3743
|
+
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${whereSQL}\`;
|
|
3744
|
+
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
|
|
3745
|
+
const countResult = await ctx.pg.query(countText, whereParams);
|
|
3746
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
3747
|
+
|
|
3748
|
+
// Get paginated data
|
|
3577
3749
|
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3578
3750
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
3579
3751
|
|
|
3580
3752
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
3581
3753
|
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3754
|
+
// Calculate hasMore
|
|
3755
|
+
const hasMore = offset + limit < total;
|
|
3756
|
+
|
|
3757
|
+
const metadata = {
|
|
3758
|
+
data: rows,
|
|
3759
|
+
total,
|
|
3760
|
+
limit,
|
|
3761
|
+
offset,
|
|
3762
|
+
hasMore,
|
|
3763
|
+
needsIncludes: !!include,
|
|
3764
|
+
includeSpec: include,
|
|
3765
|
+
status: 200
|
|
3766
|
+
};
|
|
3586
3767
|
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
log.debug(\`LIST \${ctx.table} include spec:\`, include);
|
|
3590
|
-
return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
|
|
3768
|
+
log.debug(\`LIST \${ctx.table} result: \${rows.length} rows, \${total} total, hasMore=\${hasMore}\`);
|
|
3769
|
+
return metadata;
|
|
3591
3770
|
} catch (e: any) {
|
|
3592
3771
|
log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
|
|
3593
3772
|
return {
|
|
@@ -3729,8 +3908,11 @@ describe('${Type} SDK Operations', () => {
|
|
|
3729
3908
|
});
|
|
3730
3909
|
|
|
3731
3910
|
it('should list ${tableName} relationships', async () => {
|
|
3732
|
-
const
|
|
3733
|
-
expect(
|
|
3911
|
+
const result = await sdk.${tableName}.list({ limit: 10 });
|
|
3912
|
+
expect(result).toBeDefined();
|
|
3913
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
3914
|
+
expect(typeof result.total).toBe('number');
|
|
3915
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
3734
3916
|
});
|
|
3735
3917
|
});
|
|
3736
3918
|
`;
|
|
@@ -4366,8 +4548,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
|
4366
4548
|
});
|
|
4367
4549
|
|
|
4368
4550
|
it('should list ${table.name}', async () => {
|
|
4369
|
-
const
|
|
4370
|
-
expect(
|
|
4551
|
+
const result = await sdk.${table.name}.list({ limit: 10 });
|
|
4552
|
+
expect(result).toBeDefined();
|
|
4553
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
4554
|
+
expect(typeof result.total).toBe('number');
|
|
4555
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
4371
4556
|
});
|
|
4372
4557
|
|
|
4373
4558
|
${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
|
|
@@ -4516,10 +4701,10 @@ async function generate(configPath) {
|
|
|
4516
4701
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
4517
4702
|
}
|
|
4518
4703
|
for (const table of Object.values(model.tables)) {
|
|
4519
|
-
const typesSrc = emitTypes(table, { numericMode: "string" });
|
|
4704
|
+
const typesSrc = emitTypes(table, { numericMode: "string" }, model.enums);
|
|
4520
4705
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
4521
4706
|
files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
4522
|
-
const zodSrc = emitZod(table, { numericMode: "string" });
|
|
4707
|
+
const zodSrc = emitZod(table, { numericMode: "string" }, model.enums);
|
|
4523
4708
|
files.push({ path: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
4524
4709
|
files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
4525
4710
|
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.13.0",
|
|
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",
|