postgresdk 0.12.1 → 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 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 users = await sdk.users.list();
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 authors = await sdk.authors.list({
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 books = await sdk.books.list({
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 authors = await sdk.authors.list({
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
- // Simple equality filtering with single-column sorting
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({
@@ -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<{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }>`,
789
+ description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
790
790
  example: `// Get all ${tableName}
791
- const items = await sdk.${tableName}.list();
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: `${Type}[]`
815
+ responseBody: `{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }`
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 results = await sdk.${tableName}.${method.name}();
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}({
@@ -2768,6 +2778,13 @@ const listSchema = z.object({
2768
2778
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
2769
2779
  });
2770
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
+ */
2771
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> }) {
2772
2789
  const base = "/v1/${fileTableName}";
2773
2790
 
@@ -2847,34 +2864,50 @@ ${hasAuth ? `
2847
2864
  if (result.needsIncludes && result.includeSpec) {
2848
2865
  try {
2849
2866
  const stitched = await loadIncludes(
2850
- "${fileTableName}",
2851
- result.data,
2852
- result.includeSpec,
2853
- deps.pg,
2867
+ "${fileTableName}",
2868
+ result.data,
2869
+ result.includeSpec,
2870
+ deps.pg,
2854
2871
  ${opts.includeMethodsDepth}
2855
2872
  );
2856
- return c.json(stitched);
2873
+ return c.json({
2874
+ data: stitched,
2875
+ total: result.total,
2876
+ limit: result.limit,
2877
+ offset: result.offset,
2878
+ hasMore: result.hasMore
2879
+ });
2857
2880
  } catch (e: any) {
2858
2881
  const strict = process.env.SDK_STRICT_INCLUDE === "1";
2859
2882
  if (strict) {
2860
- return c.json({
2861
- error: "include-stitch-failed",
2883
+ return c.json({
2884
+ error: "include-stitch-failed",
2862
2885
  message: e?.message,
2863
2886
  ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
2864
2887
  }, 500);
2865
2888
  }
2866
2889
  // Non-strict: return base rows with error metadata
2867
- return c.json({
2868
- data: result.data,
2869
- includeError: {
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: {
2870
2897
  message: e?.message,
2871
2898
  ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
2872
2899
  }
2873
2900
  }, 200);
2874
2901
  }
2875
2902
  }
2876
-
2877
- return c.json(result.data, result.status as any);
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);
2878
2911
  });
2879
2912
 
2880
2913
  // UPDATE
@@ -2955,21 +2988,36 @@ function emitClient(table, graph, opts, model) {
2955
2988
  for (const method of includeMethods) {
2956
2989
  const isGetByPk = method.name.startsWith("getByPk");
2957
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("");
2958
2996
  if (isGetByPk) {
2959
2997
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2960
2998
  const baseReturnType = method.returnType.replace(" | null", "");
2961
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
+ */
2962
3005
  async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
2963
- const results = await this.post<${baseReturnType}[]>(\`\${this.resource}/list\`, {
3006
+ const results = await this.post<{ data: ${baseReturnType}[]; total: number; limit: number; offset: number; hasMore: boolean; }>(\`\${this.resource}/list\`, {
2964
3007
  where: ${pkWhere},
2965
3008
  include: ${JSON.stringify(method.includeSpec)},
2966
- limit: 1
3009
+ limit: 1
2967
3010
  });
2968
- return (results[0] as ${baseReturnType}) ?? null;
3011
+ return (results.data[0] as ${baseReturnType}) ?? null;
2969
3012
  }
2970
3013
  `;
2971
3014
  } else {
2972
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
+ */
2973
3021
  async ${method.name}(${baseParams}): Promise<${method.returnType}> {
2974
3022
  return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
2975
3023
  }
@@ -2996,15 +3044,36 @@ ${otherTableImports.join(`
2996
3044
  export class ${Type}Client extends BaseClient {
2997
3045
  private readonly resource = "/v1/${table.name}";
2998
3046
 
3047
+ /**
3048
+ * Create a new ${table.name} record
3049
+ * @param data - The data to insert
3050
+ * @returns The created record
3051
+ */
2999
3052
  async create(data: Insert${Type}): Promise<Select${Type}> {
3000
3053
  return this.post<Select${Type}>(this.resource, data);
3001
3054
  }
3002
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
+ */
3003
3061
  async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
3004
3062
  const path = ${pkPathExpr};
3005
3063
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
3006
3064
  }
3007
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
+ */
3008
3077
  async list(params?: {
3009
3078
  include?: any;
3010
3079
  limit?: number;
@@ -3012,15 +3081,38 @@ export class ${Type}Client extends BaseClient {
3012
3081
  where?: Where<Select${Type}>;
3013
3082
  orderBy?: string | string[];
3014
3083
  order?: "asc" | "desc" | ("asc" | "desc")[];
3015
- }): Promise<Select${Type}[]> {
3016
- return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
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 ?? {});
3017
3098
  }
3018
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
+ */
3019
3106
  async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
3020
3107
  const path = ${pkPathExpr};
3021
3108
  return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
3022
3109
  }
3023
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
+ */
3024
3116
  async delete(pk: ${pkType}): Promise<Select${Type} | null> {
3025
3117
  const path = ${pkPathExpr};
3026
3118
  return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
@@ -3611,12 +3703,25 @@ function emitTypes(table, opts, enums) {
3611
3703
  *
3612
3704
  * To make changes, modify your schema or configuration and regenerate.
3613
3705
  */
3706
+
3707
+ /**
3708
+ * Type for inserting a new ${table.name} record.
3709
+ * Fields with defaults or nullable columns are optional.
3710
+ */
3614
3711
  export type Insert${Type} = {
3615
3712
  ${insertFields}
3616
3713
  };
3617
3714
 
3715
+ /**
3716
+ * Type for updating an existing ${table.name} record.
3717
+ * All fields are optional, allowing partial updates.
3718
+ */
3618
3719
  export type Update${Type} = Partial<Insert${Type}>;
3619
3720
 
3721
+ /**
3722
+ * Type representing a ${table.name} record from the database.
3723
+ * All fields are included as returned by SELECT queries.
3724
+ */
3620
3725
  export type Select${Type} = {
3621
3726
  ${selectFields}
3622
3727
  };
@@ -4349,7 +4454,7 @@ function buildWhereClause(
4349
4454
  export async function listRecords(
4350
4455
  ctx: OperationContext,
4351
4456
  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 }> {
4457
+ ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
4353
4458
  try {
4354
4459
  const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
4355
4460
 
@@ -4394,20 +4499,34 @@ export async function listRecords(
4394
4499
  const offsetParam = \`$\${paramIndex + 1}\`;
4395
4500
  const allParams = [...whereParams, limit, offset];
4396
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
4397
4509
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4398
4510
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
4399
4511
 
4400
4512
  const { rows } = await ctx.pg.query(text, allParams);
4401
4513
 
4402
- if (!include) {
4403
- log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
4404
- return { data: rows, status: 200 };
4405
- }
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
+ };
4406
4527
 
4407
- // Include logic will be handled by the include-loader
4408
- // For now, just return the rows with a note that includes need to be applied
4409
- log.debug(\`LIST \${ctx.table} include spec:\`, include);
4410
- 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;
4411
4530
  } catch (e: any) {
4412
4531
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
4413
4532
  return {
@@ -4549,8 +4668,11 @@ describe('${Type} SDK Operations', () => {
4549
4668
  });
4550
4669
 
4551
4670
  it('should list ${tableName} relationships', async () => {
4552
- const list = await sdk.${tableName}.list({ limit: 10 });
4553
- expect(Array.isArray(list)).toBe(true);
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');
4554
4676
  });
4555
4677
  });
4556
4678
  `;
@@ -5186,8 +5308,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
5186
5308
  });
5187
5309
 
5188
5310
  it('should list ${table.name}', async () => {
5189
- const list = await sdk.${table.name}.list({ limit: 10 });
5190
- expect(Array.isArray(list)).toBe(true);
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');
5191
5316
  });
5192
5317
 
5193
5318
  ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
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({
@@ -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<{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }>`,
788
+ description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
789
789
  example: `// Get all ${tableName}
790
- const items = await sdk.${tableName}.list();
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: `${Type}[]`
814
+ responseBody: `{ data: ${Type}[]; total: number; limit: number; offset: number; hasMore: boolean; }`
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 results = await sdk.${tableName}.${method.name}();
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}({
@@ -2008,6 +2018,13 @@ const listSchema = z.object({
2008
2018
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
2009
2019
  });
2010
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
+ */
2011
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> }) {
2012
2029
  const base = "/v1/${fileTableName}";
2013
2030
 
@@ -2087,34 +2104,50 @@ ${hasAuth ? `
2087
2104
  if (result.needsIncludes && result.includeSpec) {
2088
2105
  try {
2089
2106
  const stitched = await loadIncludes(
2090
- "${fileTableName}",
2091
- result.data,
2092
- result.includeSpec,
2093
- deps.pg,
2107
+ "${fileTableName}",
2108
+ result.data,
2109
+ result.includeSpec,
2110
+ deps.pg,
2094
2111
  ${opts.includeMethodsDepth}
2095
2112
  );
2096
- return c.json(stitched);
2113
+ return c.json({
2114
+ data: stitched,
2115
+ total: result.total,
2116
+ limit: result.limit,
2117
+ offset: result.offset,
2118
+ hasMore: result.hasMore
2119
+ });
2097
2120
  } catch (e: any) {
2098
2121
  const strict = process.env.SDK_STRICT_INCLUDE === "1";
2099
2122
  if (strict) {
2100
- return c.json({
2101
- error: "include-stitch-failed",
2123
+ return c.json({
2124
+ error: "include-stitch-failed",
2102
2125
  message: e?.message,
2103
2126
  ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
2104
2127
  }, 500);
2105
2128
  }
2106
2129
  // Non-strict: return base rows with error metadata
2107
- return c.json({
2108
- data: result.data,
2109
- includeError: {
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: {
2110
2137
  message: e?.message,
2111
2138
  ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
2112
2139
  }
2113
2140
  }, 200);
2114
2141
  }
2115
2142
  }
2116
-
2117
- return c.json(result.data, result.status as any);
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);
2118
2151
  });
2119
2152
 
2120
2153
  // UPDATE
@@ -2195,21 +2228,36 @@ function emitClient(table, graph, opts, model) {
2195
2228
  for (const method of includeMethods) {
2196
2229
  const isGetByPk = method.name.startsWith("getByPk");
2197
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("");
2198
2236
  if (isGetByPk) {
2199
2237
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2200
2238
  const baseReturnType = method.returnType.replace(" | null", "");
2201
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
+ */
2202
2245
  async ${method.name}(pk: ${pkType}): Promise<${method.returnType}> {
2203
- const results = await this.post<${baseReturnType}[]>(\`\${this.resource}/list\`, {
2246
+ const results = await this.post<{ data: ${baseReturnType}[]; total: number; limit: number; offset: number; hasMore: boolean; }>(\`\${this.resource}/list\`, {
2204
2247
  where: ${pkWhere},
2205
2248
  include: ${JSON.stringify(method.includeSpec)},
2206
- limit: 1
2249
+ limit: 1
2207
2250
  });
2208
- return (results[0] as ${baseReturnType}) ?? null;
2251
+ return (results.data[0] as ${baseReturnType}) ?? null;
2209
2252
  }
2210
2253
  `;
2211
2254
  } else {
2212
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
+ */
2213
2261
  async ${method.name}(${baseParams}): Promise<${method.returnType}> {
2214
2262
  return this.post<${method.returnType}>(\`\${this.resource}/list\`, { ...params, include: ${JSON.stringify(method.includeSpec)} });
2215
2263
  }
@@ -2236,15 +2284,36 @@ ${otherTableImports.join(`
2236
2284
  export class ${Type}Client extends BaseClient {
2237
2285
  private readonly resource = "/v1/${table.name}";
2238
2286
 
2287
+ /**
2288
+ * Create a new ${table.name} record
2289
+ * @param data - The data to insert
2290
+ * @returns The created record
2291
+ */
2239
2292
  async create(data: Insert${Type}): Promise<Select${Type}> {
2240
2293
  return this.post<Select${Type}>(this.resource, data);
2241
2294
  }
2242
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
+ */
2243
2301
  async getByPk(pk: ${pkType}): Promise<Select${Type} | null> {
2244
2302
  const path = ${pkPathExpr};
2245
2303
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
2246
2304
  }
2247
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
+ */
2248
2317
  async list(params?: {
2249
2318
  include?: any;
2250
2319
  limit?: number;
@@ -2252,15 +2321,38 @@ export class ${Type}Client extends BaseClient {
2252
2321
  where?: Where<Select${Type}>;
2253
2322
  orderBy?: string | string[];
2254
2323
  order?: "asc" | "desc" | ("asc" | "desc")[];
2255
- }): Promise<Select${Type}[]> {
2256
- return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
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 ?? {});
2257
2338
  }
2258
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
+ */
2259
2346
  async update(pk: ${pkType}, patch: Update${Type}): Promise<Select${Type} | null> {
2260
2347
  const path = ${pkPathExpr};
2261
2348
  return this.patch<Select${Type} | null>(\`\${this.resource}/\${path}\`, patch);
2262
2349
  }
2263
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
+ */
2264
2356
  async delete(pk: ${pkType}): Promise<Select${Type} | null> {
2265
2357
  const path = ${pkPathExpr};
2266
2358
  return this.del<Select${Type} | null>(\`\${this.resource}/\${path}\`);
@@ -2851,12 +2943,25 @@ function emitTypes(table, opts, enums) {
2851
2943
  *
2852
2944
  * To make changes, modify your schema or configuration and regenerate.
2853
2945
  */
2946
+
2947
+ /**
2948
+ * Type for inserting a new ${table.name} record.
2949
+ * Fields with defaults or nullable columns are optional.
2950
+ */
2854
2951
  export type Insert${Type} = {
2855
2952
  ${insertFields}
2856
2953
  };
2857
2954
 
2955
+ /**
2956
+ * Type for updating an existing ${table.name} record.
2957
+ * All fields are optional, allowing partial updates.
2958
+ */
2858
2959
  export type Update${Type} = Partial<Insert${Type}>;
2859
2960
 
2961
+ /**
2962
+ * Type representing a ${table.name} record from the database.
2963
+ * All fields are included as returned by SELECT queries.
2964
+ */
2860
2965
  export type Select${Type} = {
2861
2966
  ${selectFields}
2862
2967
  };
@@ -3589,7 +3694,7 @@ function buildWhereClause(
3589
3694
  export async function listRecords(
3590
3695
  ctx: OperationContext,
3591
3696
  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 }> {
3697
+ ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3593
3698
  try {
3594
3699
  const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
3595
3700
 
@@ -3634,20 +3739,34 @@ export async function listRecords(
3634
3739
  const offsetParam = \`$\${paramIndex + 1}\`;
3635
3740
  const allParams = [...whereParams, limit, offset];
3636
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
3637
3749
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3638
3750
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3639
3751
 
3640
3752
  const { rows } = await ctx.pg.query(text, allParams);
3641
3753
 
3642
- if (!include) {
3643
- log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
3644
- return { data: rows, status: 200 };
3645
- }
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
+ };
3646
3767
 
3647
- // Include logic will be handled by the include-loader
3648
- // For now, just return the rows with a note that includes need to be applied
3649
- log.debug(\`LIST \${ctx.table} include spec:\`, include);
3650
- 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;
3651
3770
  } catch (e: any) {
3652
3771
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
3653
3772
  return {
@@ -3789,8 +3908,11 @@ describe('${Type} SDK Operations', () => {
3789
3908
  });
3790
3909
 
3791
3910
  it('should list ${tableName} relationships', async () => {
3792
- const list = await sdk.${tableName}.list({ limit: 10 });
3793
- expect(Array.isArray(list)).toBe(true);
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');
3794
3916
  });
3795
3917
  });
3796
3918
  `;
@@ -4426,8 +4548,11 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
4426
4548
  });
4427
4549
 
4428
4550
  it('should list ${table.name}', async () => {
4429
- const list = await sdk.${table.name}.list({ limit: 10 });
4430
- expect(Array.isArray(list)).toBe(true);
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');
4431
4556
  });
4432
4557
 
4433
4558
  ${hasData && hasSinglePK ? `it('should get ${table.name} by id', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.12.1",
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": {