postgresdk 0.12.1 → 0.13.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 +34 -6
- package/dist/cli.js +184 -39
- package/dist/emit-shared-types.d.ts +1 -0
- package/dist/index.js +184 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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: `
|
|
581
|
+
returnType: `PaginatedResponse<${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)}>`,
|
|
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: `
|
|
621
|
+
returnType: `PaginatedResponse<Select${pascal(baseTableName)} & { ${type1}; ${type2} }>`,
|
|
622
622
|
includeSpec: { [key1]: true, [key2]: true }
|
|
623
623
|
});
|
|
624
624
|
methods.push({
|
|
@@ -785,10 +785,13 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
785
785
|
const endpoints = [];
|
|
786
786
|
sdkMethods.push({
|
|
787
787
|
name: "list",
|
|
788
|
-
signature: `list(params?: ListParams): Promise<${Type}
|
|
789
|
-
description: `List ${tableName} with filtering, sorting, and pagination
|
|
788
|
+
signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
|
|
789
|
+
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
790
790
|
example: `// Get all ${tableName}
|
|
791
|
-
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
|
|
792
795
|
|
|
793
796
|
// With filters and pagination
|
|
794
797
|
const filtered = await sdk.${tableName}.list({
|
|
@@ -797,15 +800,19 @@ const filtered = await sdk.${tableName}.list({
|
|
|
797
800
|
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
798
801
|
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
799
802
|
order: 'desc'
|
|
800
|
-
})
|
|
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;`,
|
|
801
808
|
correspondsTo: `GET ${basePath}`
|
|
802
809
|
});
|
|
803
810
|
endpoints.push({
|
|
804
811
|
method: "GET",
|
|
805
812
|
path: basePath,
|
|
806
|
-
description: `List all ${tableName} records`,
|
|
813
|
+
description: `List all ${tableName} records with pagination metadata`,
|
|
807
814
|
queryParameters: generateQueryParams(table, enums),
|
|
808
|
-
responseBody:
|
|
815
|
+
responseBody: `PaginatedResponse<${Type}>`
|
|
809
816
|
});
|
|
810
817
|
if (hasSinglePK) {
|
|
811
818
|
sdkMethods.push({
|
|
@@ -895,7 +902,10 @@ console.log('Deleted:', deleted);`,
|
|
|
895
902
|
}, allTables);
|
|
896
903
|
for (const method of includeMethods) {
|
|
897
904
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
898
|
-
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
|
|
899
909
|
|
|
900
910
|
// With filters and pagination
|
|
901
911
|
const filtered = await sdk.${tableName}.${method.name}({
|
|
@@ -2725,6 +2735,31 @@ export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
|
|
|
2725
2735
|
`;
|
|
2726
2736
|
}
|
|
2727
2737
|
|
|
2738
|
+
// src/emit-shared-types.ts
|
|
2739
|
+
function emitSharedTypes() {
|
|
2740
|
+
return `/**
|
|
2741
|
+
* Shared types used across all SDK operations
|
|
2742
|
+
*/
|
|
2743
|
+
|
|
2744
|
+
/**
|
|
2745
|
+
* Paginated response structure returned by list operations
|
|
2746
|
+
* @template T - The type of records in the data array
|
|
2747
|
+
*/
|
|
2748
|
+
export interface PaginatedResponse<T> {
|
|
2749
|
+
/** Array of records for the current page */
|
|
2750
|
+
data: T[];
|
|
2751
|
+
/** Total number of records matching the query (across all pages) */
|
|
2752
|
+
total: number;
|
|
2753
|
+
/** Maximum number of records per page */
|
|
2754
|
+
limit: number;
|
|
2755
|
+
/** Number of records skipped (for pagination) */
|
|
2756
|
+
offset: number;
|
|
2757
|
+
/** Whether there are more records available after this page */
|
|
2758
|
+
hasMore: boolean;
|
|
2759
|
+
}
|
|
2760
|
+
`;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2728
2763
|
// src/emit-routes-hono.ts
|
|
2729
2764
|
init_utils();
|
|
2730
2765
|
function emitHonoRoutes(table, _graph, opts) {
|
|
@@ -2768,6 +2803,13 @@ const listSchema = z.object({
|
|
|
2768
2803
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
2769
2804
|
});
|
|
2770
2805
|
|
|
2806
|
+
/**
|
|
2807
|
+
* Register all CRUD routes for the ${fileTableName} table
|
|
2808
|
+
* @param app - Hono application instance
|
|
2809
|
+
* @param deps - Dependencies including database client and optional request hook
|
|
2810
|
+
* @param deps.pg - PostgreSQL client with query method
|
|
2811
|
+
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2812
|
+
*/
|
|
2771
2813
|
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> }) {
|
|
2772
2814
|
const base = "/v1/${fileTableName}";
|
|
2773
2815
|
|
|
@@ -2847,34 +2889,50 @@ ${hasAuth ? `
|
|
|
2847
2889
|
if (result.needsIncludes && result.includeSpec) {
|
|
2848
2890
|
try {
|
|
2849
2891
|
const stitched = await loadIncludes(
|
|
2850
|
-
"${fileTableName}",
|
|
2851
|
-
result.data,
|
|
2852
|
-
result.includeSpec,
|
|
2853
|
-
deps.pg,
|
|
2892
|
+
"${fileTableName}",
|
|
2893
|
+
result.data,
|
|
2894
|
+
result.includeSpec,
|
|
2895
|
+
deps.pg,
|
|
2854
2896
|
${opts.includeMethodsDepth}
|
|
2855
2897
|
);
|
|
2856
|
-
return c.json(
|
|
2898
|
+
return c.json({
|
|
2899
|
+
data: stitched,
|
|
2900
|
+
total: result.total,
|
|
2901
|
+
limit: result.limit,
|
|
2902
|
+
offset: result.offset,
|
|
2903
|
+
hasMore: result.hasMore
|
|
2904
|
+
});
|
|
2857
2905
|
} catch (e: any) {
|
|
2858
2906
|
const strict = process.env.SDK_STRICT_INCLUDE === "1";
|
|
2859
2907
|
if (strict) {
|
|
2860
|
-
return c.json({
|
|
2861
|
-
error: "include-stitch-failed",
|
|
2908
|
+
return c.json({
|
|
2909
|
+
error: "include-stitch-failed",
|
|
2862
2910
|
message: e?.message,
|
|
2863
2911
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2864
2912
|
}, 500);
|
|
2865
2913
|
}
|
|
2866
2914
|
// Non-strict: return base rows with error metadata
|
|
2867
|
-
return c.json({
|
|
2868
|
-
data: result.data,
|
|
2869
|
-
|
|
2915
|
+
return c.json({
|
|
2916
|
+
data: result.data,
|
|
2917
|
+
total: result.total,
|
|
2918
|
+
limit: result.limit,
|
|
2919
|
+
offset: result.offset,
|
|
2920
|
+
hasMore: result.hasMore,
|
|
2921
|
+
includeError: {
|
|
2870
2922
|
message: e?.message,
|
|
2871
2923
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2872
2924
|
}
|
|
2873
2925
|
}, 200);
|
|
2874
2926
|
}
|
|
2875
2927
|
}
|
|
2876
|
-
|
|
2877
|
-
return c.json(
|
|
2928
|
+
|
|
2929
|
+
return c.json({
|
|
2930
|
+
data: result.data,
|
|
2931
|
+
total: result.total,
|
|
2932
|
+
limit: result.limit,
|
|
2933
|
+
offset: result.offset,
|
|
2934
|
+
hasMore: result.hasMore
|
|
2935
|
+
}, result.status as any);
|
|
2878
2936
|
});
|
|
2879
2937
|
|
|
2880
2938
|
// UPDATE
|
|
@@ -2955,21 +3013,36 @@ function emitClient(table, graph, opts, model) {
|
|
|
2955
3013
|
for (const method of includeMethods) {
|
|
2956
3014
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2957
3015
|
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
3016
|
+
const relationshipDesc = method.path.map((p, i) => {
|
|
3017
|
+
const isLast = i === method.path.length - 1;
|
|
3018
|
+
const relation = method.isMany[i] ? "many" : "one";
|
|
3019
|
+
return isLast ? p : `${p} -> `;
|
|
3020
|
+
}).join("");
|
|
2958
3021
|
if (isGetByPk) {
|
|
2959
3022
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2960
3023
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
2961
3024
|
includeMethodsCode += `
|
|
3025
|
+
/**
|
|
3026
|
+
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
3027
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3028
|
+
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
3029
|
+
*/
|
|
2962
3030
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2963
|
-
const results = await this.post<${baseReturnType}
|
|
3031
|
+
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
2964
3032
|
where: ${pkWhere},
|
|
2965
3033
|
include: ${JSON.stringify(method.includeSpec)},
|
|
2966
|
-
limit: 1
|
|
3034
|
+
limit: 1
|
|
2967
3035
|
});
|
|
2968
|
-
return (results[0] as ${baseReturnType}) ?? null;
|
|
3036
|
+
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
2969
3037
|
}
|
|
2970
3038
|
`;
|
|
2971
3039
|
} else {
|
|
2972
3040
|
includeMethodsCode += `
|
|
3041
|
+
/**
|
|
3042
|
+
* List ${table.name} records with included related ${relationshipDesc}
|
|
3043
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
3044
|
+
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
3045
|
+
*/
|
|
2973
3046
|
async ${method.name}(${baseParams}): Promise<${method.returnType}> {
|
|
2974
3047
|
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
2975
3048
|
}
|
|
@@ -2986,6 +3059,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2986
3059
|
*/
|
|
2987
3060
|
import { BaseClient } from "./base-client${ext}";
|
|
2988
3061
|
import type { Where } from "./where-types${ext}";
|
|
3062
|
+
import type { PaginatedResponse } from "./types/shared${ext}";
|
|
2989
3063
|
${typeImports}
|
|
2990
3064
|
${otherTableImports.join(`
|
|
2991
3065
|
`)}
|
|
@@ -2996,15 +3070,36 @@ ${otherTableImports.join(`
|
|
|
2996
3070
|
export class ${Type}Client extends BaseClient {
|
|
2997
3071
|
private readonly resource = "/v1/${table.name}";
|
|
2998
3072
|
|
|
3073
|
+
/**
|
|
3074
|
+
* Create a new ${table.name} record
|
|
3075
|
+
* @param data - The data to insert
|
|
3076
|
+
* @returns The created record
|
|
3077
|
+
*/
|
|
2999
3078
|
async create(data: Insert${Type}): Promise<Select${Type}> {
|
|
3000
3079
|
return this.post<Select${Type}>(this.resource, data);
|
|
3001
3080
|
}
|
|
3002
3081
|
|
|
3082
|
+
/**
|
|
3083
|
+
* Get a ${table.name} record by primary key
|
|
3084
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3085
|
+
* @returns The record if found, null otherwise
|
|
3086
|
+
*/
|
|
3003
3087
|
async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
3004
3088
|
const path = ${pkPathExpr};
|
|
3005
3089
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
3006
3090
|
}
|
|
3007
3091
|
|
|
3092
|
+
/**
|
|
3093
|
+
* List ${table.name} records with pagination and filtering
|
|
3094
|
+
* @param params - Query parameters
|
|
3095
|
+
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
3096
|
+
* @param params.orderBy - Column(s) to sort by
|
|
3097
|
+
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
3098
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 100)
|
|
3099
|
+
* @param params.offset - Number of records to skip for pagination
|
|
3100
|
+
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
3101
|
+
* @returns Paginated results with data, total count, and hasMore flag
|
|
3102
|
+
*/
|
|
3008
3103
|
async list(params?: {
|
|
3009
3104
|
include?: any;
|
|
3010
3105
|
limit?: number;
|
|
@@ -3012,15 +3107,26 @@ export class ${Type}Client extends BaseClient {
|
|
|
3012
3107
|
where?: Where<Select${Type}>;
|
|
3013
3108
|
orderBy?: string | string[];
|
|
3014
3109
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3015
|
-
}): Promise<Select${Type}
|
|
3016
|
-
return this.post<Select${Type}
|
|
3110
|
+
}): Promise<PaginatedResponse<Select${Type}>> {
|
|
3111
|
+
return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3017
3112
|
}
|
|
3018
3113
|
|
|
3114
|
+
/**
|
|
3115
|
+
* Update a ${table.name} record by primary key
|
|
3116
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3117
|
+
* @param patch - Partial data to update
|
|
3118
|
+
* @returns The updated record if found, null otherwise
|
|
3119
|
+
*/
|
|
3019
3120
|
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
|
|
3020
3121
|
const path = ${pkPathExpr};
|
|
3021
3122
|
return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
|
|
3022
3123
|
}
|
|
3023
3124
|
|
|
3125
|
+
/**
|
|
3126
|
+
* Delete a ${table.name} record by primary key
|
|
3127
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3128
|
+
* @returns The deleted record if found, null otherwise
|
|
3129
|
+
*/
|
|
3024
3130
|
async delete(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
3025
3131
|
const path = ${pkPathExpr};
|
|
3026
3132
|
return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
@@ -3113,6 +3219,11 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
3113
3219
|
out += `export type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${t.name}${ext}";
|
|
3114
3220
|
`;
|
|
3115
3221
|
}
|
|
3222
|
+
out += `
|
|
3223
|
+
// Shared types
|
|
3224
|
+
`;
|
|
3225
|
+
out += `export type { PaginatedResponse } from "./types/shared${ext}";
|
|
3226
|
+
`;
|
|
3116
3227
|
return out;
|
|
3117
3228
|
}
|
|
3118
3229
|
|
|
@@ -3611,12 +3722,25 @@ function emitTypes(table, opts, enums) {
|
|
|
3611
3722
|
*
|
|
3612
3723
|
* To make changes, modify your schema or configuration and regenerate.
|
|
3613
3724
|
*/
|
|
3725
|
+
|
|
3726
|
+
/**
|
|
3727
|
+
* Type for inserting a new ${table.name} record.
|
|
3728
|
+
* Fields with defaults or nullable columns are optional.
|
|
3729
|
+
*/
|
|
3614
3730
|
export type Insert${Type} = {
|
|
3615
3731
|
${insertFields}
|
|
3616
3732
|
};
|
|
3617
3733
|
|
|
3734
|
+
/**
|
|
3735
|
+
* Type for updating an existing ${table.name} record.
|
|
3736
|
+
* All fields are optional, allowing partial updates.
|
|
3737
|
+
*/
|
|
3618
3738
|
export type Update${Type} = Partial<Insert${Type}>;
|
|
3619
3739
|
|
|
3740
|
+
/**
|
|
3741
|
+
* Type representing a ${table.name} record from the database.
|
|
3742
|
+
* All fields are included as returned by SELECT queries.
|
|
3743
|
+
*/
|
|
3620
3744
|
export type Select${Type} = {
|
|
3621
3745
|
${selectFields}
|
|
3622
3746
|
};
|
|
@@ -4349,7 +4473,7 @@ function buildWhereClause(
|
|
|
4349
4473
|
export async function listRecords(
|
|
4350
4474
|
ctx: OperationContext,
|
|
4351
4475
|
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
4352
|
-
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4476
|
+
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
4353
4477
|
try {
|
|
4354
4478
|
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
4355
4479
|
|
|
@@ -4394,20 +4518,34 @@ export async function listRecords(
|
|
|
4394
4518
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
4395
4519
|
const allParams = [...whereParams, limit, offset];
|
|
4396
4520
|
|
|
4521
|
+
// Get total count for pagination
|
|
4522
|
+
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${whereSQL}\`;
|
|
4523
|
+
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
|
|
4524
|
+
const countResult = await ctx.pg.query(countText, whereParams);
|
|
4525
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
4526
|
+
|
|
4527
|
+
// Get paginated data
|
|
4397
4528
|
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
4398
4529
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
4399
4530
|
|
|
4400
4531
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
4401
4532
|
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4533
|
+
// Calculate hasMore
|
|
4534
|
+
const hasMore = offset + limit < total;
|
|
4535
|
+
|
|
4536
|
+
const metadata = {
|
|
4537
|
+
data: rows,
|
|
4538
|
+
total,
|
|
4539
|
+
limit,
|
|
4540
|
+
offset,
|
|
4541
|
+
hasMore,
|
|
4542
|
+
needsIncludes: !!include,
|
|
4543
|
+
includeSpec: include,
|
|
4544
|
+
status: 200
|
|
4545
|
+
};
|
|
4406
4546
|
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
log.debug(\`LIST \${ctx.table} include spec:\`, include);
|
|
4410
|
-
return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
|
|
4547
|
+
log.debug(\`LIST \${ctx.table} result: \${rows.length} rows, \${total} total, hasMore=\${hasMore}\`);
|
|
4548
|
+
return metadata;
|
|
4411
4549
|
} catch (e: any) {
|
|
4412
4550
|
log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
|
|
4413
4551
|
return {
|
|
@@ -4549,8 +4687,11 @@ describe('${Type} SDK Operations', () => {
|
|
|
4549
4687
|
});
|
|
4550
4688
|
|
|
4551
4689
|
it('should list ${tableName} relationships', async () => {
|
|
4552
|
-
const
|
|
4553
|
-
expect(
|
|
4690
|
+
const result = await sdk.${tableName}.list({ limit: 10 });
|
|
4691
|
+
expect(result).toBeDefined();
|
|
4692
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
4693
|
+
expect(typeof result.total).toBe('number');
|
|
4694
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
4554
4695
|
});
|
|
4555
4696
|
});
|
|
4556
4697
|
`;
|
|
@@ -5186,8 +5327,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
|
5186
5327
|
});
|
|
5187
5328
|
|
|
5188
5329
|
it('should list ${table.name}', async () => {
|
|
5189
|
-
const
|
|
5190
|
-
expect(
|
|
5330
|
+
const result = await sdk.${table.name}.list({ limit: 10 });
|
|
5331
|
+
expect(result).toBeDefined();
|
|
5332
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
5333
|
+
expect(typeof result.total).toBe('number');
|
|
5334
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
5191
5335
|
});
|
|
5192
5336
|
|
|
5193
5337
|
${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
|
|
@@ -5314,6 +5458,7 @@ async function generate(configPath) {
|
|
|
5314
5458
|
files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
|
|
5315
5459
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
5316
5460
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
5461
|
+
files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
5317
5462
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
5318
5463
|
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
5319
5464
|
files.push({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function emitSharedTypes(): string;
|
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: `
|
|
580
|
+
returnType: `PaginatedResponse<${buildReturnType(baseTableName, newPath, newIsMany, newTargets, graph)}>`,
|
|
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: `
|
|
620
|
+
returnType: `PaginatedResponse<Select${pascal(baseTableName)} & { ${type1}; ${type2} }>`,
|
|
621
621
|
includeSpec: { [key1]: true, [key2]: true }
|
|
622
622
|
});
|
|
623
623
|
methods.push({
|
|
@@ -784,10 +784,13 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
784
784
|
const endpoints = [];
|
|
785
785
|
sdkMethods.push({
|
|
786
786
|
name: "list",
|
|
787
|
-
signature: `list(params?: ListParams): Promise<${Type}
|
|
788
|
-
description: `List ${tableName} with filtering, sorting, and pagination
|
|
787
|
+
signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
|
|
788
|
+
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
789
789
|
example: `// Get all ${tableName}
|
|
790
|
-
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
|
|
791
794
|
|
|
792
795
|
// With filters and pagination
|
|
793
796
|
const filtered = await sdk.${tableName}.list({
|
|
@@ -796,15 +799,19 @@ const filtered = await sdk.${tableName}.list({
|
|
|
796
799
|
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
797
800
|
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
798
801
|
order: 'desc'
|
|
799
|
-
})
|
|
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;`,
|
|
800
807
|
correspondsTo: `GET ${basePath}`
|
|
801
808
|
});
|
|
802
809
|
endpoints.push({
|
|
803
810
|
method: "GET",
|
|
804
811
|
path: basePath,
|
|
805
|
-
description: `List all ${tableName} records`,
|
|
812
|
+
description: `List all ${tableName} records with pagination metadata`,
|
|
806
813
|
queryParameters: generateQueryParams(table, enums),
|
|
807
|
-
responseBody:
|
|
814
|
+
responseBody: `PaginatedResponse<${Type}>`
|
|
808
815
|
});
|
|
809
816
|
if (hasSinglePK) {
|
|
810
817
|
sdkMethods.push({
|
|
@@ -894,7 +901,10 @@ console.log('Deleted:', deleted);`,
|
|
|
894
901
|
}, allTables);
|
|
895
902
|
for (const method of includeMethods) {
|
|
896
903
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
897
|
-
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
|
|
898
908
|
|
|
899
909
|
// With filters and pagination
|
|
900
910
|
const filtered = await sdk.${tableName}.${method.name}({
|
|
@@ -1965,6 +1975,31 @@ export type PaginationParams = z.infer<typeof PaginationParamsSchema>;
|
|
|
1965
1975
|
`;
|
|
1966
1976
|
}
|
|
1967
1977
|
|
|
1978
|
+
// src/emit-shared-types.ts
|
|
1979
|
+
function emitSharedTypes() {
|
|
1980
|
+
return `/**
|
|
1981
|
+
* Shared types used across all SDK operations
|
|
1982
|
+
*/
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Paginated response structure returned by list operations
|
|
1986
|
+
* @template T - The type of records in the data array
|
|
1987
|
+
*/
|
|
1988
|
+
export interface PaginatedResponse<T> {
|
|
1989
|
+
/** Array of records for the current page */
|
|
1990
|
+
data: T[];
|
|
1991
|
+
/** Total number of records matching the query (across all pages) */
|
|
1992
|
+
total: number;
|
|
1993
|
+
/** Maximum number of records per page */
|
|
1994
|
+
limit: number;
|
|
1995
|
+
/** Number of records skipped (for pagination) */
|
|
1996
|
+
offset: number;
|
|
1997
|
+
/** Whether there are more records available after this page */
|
|
1998
|
+
hasMore: boolean;
|
|
1999
|
+
}
|
|
2000
|
+
`;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1968
2003
|
// src/emit-routes-hono.ts
|
|
1969
2004
|
init_utils();
|
|
1970
2005
|
function emitHonoRoutes(table, _graph, opts) {
|
|
@@ -2008,6 +2043,13 @@ const listSchema = z.object({
|
|
|
2008
2043
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
|
|
2009
2044
|
});
|
|
2010
2045
|
|
|
2046
|
+
/**
|
|
2047
|
+
* Register all CRUD routes for the ${fileTableName} table
|
|
2048
|
+
* @param app - Hono application instance
|
|
2049
|
+
* @param deps - Dependencies including database client and optional request hook
|
|
2050
|
+
* @param deps.pg - PostgreSQL client with query method
|
|
2051
|
+
* @param deps.onRequest - Optional hook that runs before each request (for audit logging, RLS, etc.)
|
|
2052
|
+
*/
|
|
2011
2053
|
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> }) {
|
|
2012
2054
|
const base = "/v1/${fileTableName}";
|
|
2013
2055
|
|
|
@@ -2087,34 +2129,50 @@ ${hasAuth ? `
|
|
|
2087
2129
|
if (result.needsIncludes && result.includeSpec) {
|
|
2088
2130
|
try {
|
|
2089
2131
|
const stitched = await loadIncludes(
|
|
2090
|
-
"${fileTableName}",
|
|
2091
|
-
result.data,
|
|
2092
|
-
result.includeSpec,
|
|
2093
|
-
deps.pg,
|
|
2132
|
+
"${fileTableName}",
|
|
2133
|
+
result.data,
|
|
2134
|
+
result.includeSpec,
|
|
2135
|
+
deps.pg,
|
|
2094
2136
|
${opts.includeMethodsDepth}
|
|
2095
2137
|
);
|
|
2096
|
-
return c.json(
|
|
2138
|
+
return c.json({
|
|
2139
|
+
data: stitched,
|
|
2140
|
+
total: result.total,
|
|
2141
|
+
limit: result.limit,
|
|
2142
|
+
offset: result.offset,
|
|
2143
|
+
hasMore: result.hasMore
|
|
2144
|
+
});
|
|
2097
2145
|
} catch (e: any) {
|
|
2098
2146
|
const strict = process.env.SDK_STRICT_INCLUDE === "1";
|
|
2099
2147
|
if (strict) {
|
|
2100
|
-
return c.json({
|
|
2101
|
-
error: "include-stitch-failed",
|
|
2148
|
+
return c.json({
|
|
2149
|
+
error: "include-stitch-failed",
|
|
2102
2150
|
message: e?.message,
|
|
2103
2151
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2104
2152
|
}, 500);
|
|
2105
2153
|
}
|
|
2106
2154
|
// Non-strict: return base rows with error metadata
|
|
2107
|
-
return c.json({
|
|
2108
|
-
data: result.data,
|
|
2109
|
-
|
|
2155
|
+
return c.json({
|
|
2156
|
+
data: result.data,
|
|
2157
|
+
total: result.total,
|
|
2158
|
+
limit: result.limit,
|
|
2159
|
+
offset: result.offset,
|
|
2160
|
+
hasMore: result.hasMore,
|
|
2161
|
+
includeError: {
|
|
2110
2162
|
message: e?.message,
|
|
2111
2163
|
...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
|
|
2112
2164
|
}
|
|
2113
2165
|
}, 200);
|
|
2114
2166
|
}
|
|
2115
2167
|
}
|
|
2116
|
-
|
|
2117
|
-
return c.json(
|
|
2168
|
+
|
|
2169
|
+
return c.json({
|
|
2170
|
+
data: result.data,
|
|
2171
|
+
total: result.total,
|
|
2172
|
+
limit: result.limit,
|
|
2173
|
+
offset: result.offset,
|
|
2174
|
+
hasMore: result.hasMore
|
|
2175
|
+
}, result.status as any);
|
|
2118
2176
|
});
|
|
2119
2177
|
|
|
2120
2178
|
// UPDATE
|
|
@@ -2195,21 +2253,36 @@ function emitClient(table, graph, opts, model) {
|
|
|
2195
2253
|
for (const method of includeMethods) {
|
|
2196
2254
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2197
2255
|
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
|
|
2256
|
+
const relationshipDesc = method.path.map((p, i) => {
|
|
2257
|
+
const isLast = i === method.path.length - 1;
|
|
2258
|
+
const relation = method.isMany[i] ? "many" : "one";
|
|
2259
|
+
return isLast ? p : `${p} -> `;
|
|
2260
|
+
}).join("");
|
|
2198
2261
|
if (isGetByPk) {
|
|
2199
2262
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2200
2263
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
2201
2264
|
includeMethodsCode += `
|
|
2265
|
+
/**
|
|
2266
|
+
* Get a ${table.name} record by primary key with included related ${relationshipDesc}
|
|
2267
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2268
|
+
* @returns The record with nested ${method.path.join(" and ")} if found, null otherwise
|
|
2269
|
+
*/
|
|
2202
2270
|
async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
|
|
2203
|
-
const results = await this.post<${baseReturnType}
|
|
2271
|
+
const results = await this.post<PaginatedResponse<${baseReturnType}>>(\`\${this.resource}/list\`, {
|
|
2204
2272
|
where: ${pkWhere},
|
|
2205
2273
|
include: ${JSON.stringify(method.includeSpec)},
|
|
2206
|
-
limit: 1
|
|
2274
|
+
limit: 1
|
|
2207
2275
|
});
|
|
2208
|
-
return (results[0] as ${baseReturnType}) ?? null;
|
|
2276
|
+
return (results.data[0] as ${baseReturnType}) ?? null;
|
|
2209
2277
|
}
|
|
2210
2278
|
`;
|
|
2211
2279
|
} else {
|
|
2212
2280
|
includeMethodsCode += `
|
|
2281
|
+
/**
|
|
2282
|
+
* List ${table.name} records with included related ${relationshipDesc}
|
|
2283
|
+
* @param params - Query parameters (where, orderBy, order, limit, offset)
|
|
2284
|
+
* @returns Paginated results with nested ${method.path.join(" and ")} included
|
|
2285
|
+
*/
|
|
2213
2286
|
async ${method.name}(${baseParams}): Promise<${method.returnType}> {
|
|
2214
2287
|
return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
|
|
2215
2288
|
}
|
|
@@ -2226,6 +2299,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2226
2299
|
*/
|
|
2227
2300
|
import { BaseClient } from "./base-client${ext}";
|
|
2228
2301
|
import type { Where } from "./where-types${ext}";
|
|
2302
|
+
import type { PaginatedResponse } from "./types/shared${ext}";
|
|
2229
2303
|
${typeImports}
|
|
2230
2304
|
${otherTableImports.join(`
|
|
2231
2305
|
`)}
|
|
@@ -2236,15 +2310,36 @@ ${otherTableImports.join(`
|
|
|
2236
2310
|
export class ${Type}Client extends BaseClient {
|
|
2237
2311
|
private readonly resource = "/v1/${table.name}";
|
|
2238
2312
|
|
|
2313
|
+
/**
|
|
2314
|
+
* Create a new ${table.name} record
|
|
2315
|
+
* @param data - The data to insert
|
|
2316
|
+
* @returns The created record
|
|
2317
|
+
*/
|
|
2239
2318
|
async create(data: Insert${Type}): Promise<Select${Type}> {
|
|
2240
2319
|
return this.post<Select${Type}>(this.resource, data);
|
|
2241
2320
|
}
|
|
2242
2321
|
|
|
2322
|
+
/**
|
|
2323
|
+
* Get a ${table.name} record by primary key
|
|
2324
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2325
|
+
* @returns The record if found, null otherwise
|
|
2326
|
+
*/
|
|
2243
2327
|
async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2244
2328
|
const path = ${pkPathExpr};
|
|
2245
2329
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
2246
2330
|
}
|
|
2247
2331
|
|
|
2332
|
+
/**
|
|
2333
|
+
* List ${table.name} records with pagination and filtering
|
|
2334
|
+
* @param params - Query parameters
|
|
2335
|
+
* @param params.where - Filter conditions using operators like $eq, $gt, $in, $like, etc.
|
|
2336
|
+
* @param params.orderBy - Column(s) to sort by
|
|
2337
|
+
* @param params.order - Sort direction(s): "asc" or "desc"
|
|
2338
|
+
* @param params.limit - Maximum number of records to return (default: 50, max: 100)
|
|
2339
|
+
* @param params.offset - Number of records to skip for pagination
|
|
2340
|
+
* @param params.include - Related records to include (see listWith* methods for typed includes)
|
|
2341
|
+
* @returns Paginated results with data, total count, and hasMore flag
|
|
2342
|
+
*/
|
|
2248
2343
|
async list(params?: {
|
|
2249
2344
|
include?: any;
|
|
2250
2345
|
limit?: number;
|
|
@@ -2252,15 +2347,26 @@ export class ${Type}Client extends BaseClient {
|
|
|
2252
2347
|
where?: Where<Select${Type}>;
|
|
2253
2348
|
orderBy?: string | string[];
|
|
2254
2349
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2255
|
-
}): Promise<Select${Type}
|
|
2256
|
-
return this.post<Select${Type}
|
|
2350
|
+
}): Promise<PaginatedResponse<Select${Type}>> {
|
|
2351
|
+
return this.post<PaginatedResponse<Select${Type}>>(\`\${this.resource}/list\`, params ?? {});
|
|
2257
2352
|
}
|
|
2258
2353
|
|
|
2354
|
+
/**
|
|
2355
|
+
* Update a ${table.name} record by primary key
|
|
2356
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2357
|
+
* @param patch - Partial data to update
|
|
2358
|
+
* @returns The updated record if found, null otherwise
|
|
2359
|
+
*/
|
|
2259
2360
|
async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
|
|
2260
2361
|
const path = ${pkPathExpr};
|
|
2261
2362
|
return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
|
|
2262
2363
|
}
|
|
2263
2364
|
|
|
2365
|
+
/**
|
|
2366
|
+
* Delete a ${table.name} record by primary key
|
|
2367
|
+
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
2368
|
+
* @returns The deleted record if found, null otherwise
|
|
2369
|
+
*/
|
|
2264
2370
|
async delete(pk: ${pkType}): Promise<Select${Type} | null> {
|
|
2265
2371
|
const path = ${pkPathExpr};
|
|
2266
2372
|
return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
@@ -2353,6 +2459,11 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
2353
2459
|
out += `export type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${t.name}${ext}";
|
|
2354
2460
|
`;
|
|
2355
2461
|
}
|
|
2462
|
+
out += `
|
|
2463
|
+
// Shared types
|
|
2464
|
+
`;
|
|
2465
|
+
out += `export type { PaginatedResponse } from "./types/shared${ext}";
|
|
2466
|
+
`;
|
|
2356
2467
|
return out;
|
|
2357
2468
|
}
|
|
2358
2469
|
|
|
@@ -2851,12 +2962,25 @@ function emitTypes(table, opts, enums) {
|
|
|
2851
2962
|
*
|
|
2852
2963
|
* To make changes, modify your schema or configuration and regenerate.
|
|
2853
2964
|
*/
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* Type for inserting a new ${table.name} record.
|
|
2968
|
+
* Fields with defaults or nullable columns are optional.
|
|
2969
|
+
*/
|
|
2854
2970
|
export type Insert${Type} = {
|
|
2855
2971
|
${insertFields}
|
|
2856
2972
|
};
|
|
2857
2973
|
|
|
2974
|
+
/**
|
|
2975
|
+
* Type for updating an existing ${table.name} record.
|
|
2976
|
+
* All fields are optional, allowing partial updates.
|
|
2977
|
+
*/
|
|
2858
2978
|
export type Update${Type} = Partial<Insert${Type}>;
|
|
2859
2979
|
|
|
2980
|
+
/**
|
|
2981
|
+
* Type representing a ${table.name} record from the database.
|
|
2982
|
+
* All fields are included as returned by SELECT queries.
|
|
2983
|
+
*/
|
|
2860
2984
|
export type Select${Type} = {
|
|
2861
2985
|
${selectFields}
|
|
2862
2986
|
};
|
|
@@ -3589,7 +3713,7 @@ function buildWhereClause(
|
|
|
3589
3713
|
export async function listRecords(
|
|
3590
3714
|
ctx: OperationContext,
|
|
3591
3715
|
params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
|
|
3592
|
-
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3716
|
+
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3593
3717
|
try {
|
|
3594
3718
|
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
|
|
3595
3719
|
|
|
@@ -3634,20 +3758,34 @@ export async function listRecords(
|
|
|
3634
3758
|
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
3635
3759
|
const allParams = [...whereParams, limit, offset];
|
|
3636
3760
|
|
|
3761
|
+
// Get total count for pagination
|
|
3762
|
+
const countText = \`SELECT COUNT(*) FROM "\${ctx.table}" \${whereSQL}\`;
|
|
3763
|
+
log.debug(\`LIST \${ctx.table} COUNT SQL:\`, countText, "params:", whereParams);
|
|
3764
|
+
const countResult = await ctx.pg.query(countText, whereParams);
|
|
3765
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
3766
|
+
|
|
3767
|
+
// Get paginated data
|
|
3637
3768
|
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3638
3769
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
3639
3770
|
|
|
3640
3771
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
3641
3772
|
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3773
|
+
// Calculate hasMore
|
|
3774
|
+
const hasMore = offset + limit < total;
|
|
3775
|
+
|
|
3776
|
+
const metadata = {
|
|
3777
|
+
data: rows,
|
|
3778
|
+
total,
|
|
3779
|
+
limit,
|
|
3780
|
+
offset,
|
|
3781
|
+
hasMore,
|
|
3782
|
+
needsIncludes: !!include,
|
|
3783
|
+
includeSpec: include,
|
|
3784
|
+
status: 200
|
|
3785
|
+
};
|
|
3646
3786
|
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
log.debug(\`LIST \${ctx.table} include spec:\`, include);
|
|
3650
|
-
return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
|
|
3787
|
+
log.debug(\`LIST \${ctx.table} result: \${rows.length} rows, \${total} total, hasMore=\${hasMore}\`);
|
|
3788
|
+
return metadata;
|
|
3651
3789
|
} catch (e: any) {
|
|
3652
3790
|
log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
|
|
3653
3791
|
return {
|
|
@@ -3789,8 +3927,11 @@ describe('${Type} SDK Operations', () => {
|
|
|
3789
3927
|
});
|
|
3790
3928
|
|
|
3791
3929
|
it('should list ${tableName} relationships', async () => {
|
|
3792
|
-
const
|
|
3793
|
-
expect(
|
|
3930
|
+
const result = await sdk.${tableName}.list({ limit: 10 });
|
|
3931
|
+
expect(result).toBeDefined();
|
|
3932
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
3933
|
+
expect(typeof result.total).toBe('number');
|
|
3934
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
3794
3935
|
});
|
|
3795
3936
|
});
|
|
3796
3937
|
`;
|
|
@@ -4426,8 +4567,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
|
4426
4567
|
});
|
|
4427
4568
|
|
|
4428
4569
|
it('should list ${table.name}', async () => {
|
|
4429
|
-
const
|
|
4430
|
-
expect(
|
|
4570
|
+
const result = await sdk.${table.name}.list({ limit: 10 });
|
|
4571
|
+
expect(result).toBeDefined();
|
|
4572
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
4573
|
+
expect(typeof result.total).toBe('number');
|
|
4574
|
+
expect(typeof result.hasMore).toBe('boolean');
|
|
4431
4575
|
});
|
|
4432
4576
|
|
|
4433
4577
|
${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
|
|
@@ -4554,6 +4698,7 @@ async function generate(configPath) {
|
|
|
4554
4698
|
files.push({ path: join(serverDir, "include-spec.ts"), content: includeSpec });
|
|
4555
4699
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
4556
4700
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
4701
|
+
files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
4557
4702
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
4558
4703
|
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
4559
4704
|
files.push({
|