postgresdk 0.10.4 → 0.12.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
@@ -160,7 +160,7 @@ const authors = await sdk.authors.list({
160
160
  ### Filtering & Pagination
161
161
 
162
162
  ```typescript
163
- // Simple equality filtering
163
+ // Simple equality filtering with single-column sorting
164
164
  const users = await sdk.users.list({
165
165
  where: { status: "active" },
166
166
  orderBy: "created_at",
@@ -169,6 +169,12 @@ const users = await sdk.users.list({
169
169
  offset: 40
170
170
  });
171
171
 
172
+ // Multi-column sorting
173
+ const sorted = await sdk.users.list({
174
+ orderBy: ["status", "created_at"],
175
+ order: ["asc", "desc"] // or use single direction: order: "asc"
176
+ });
177
+
172
178
  // Advanced WHERE operators
173
179
  const filtered = await sdk.users.list({
174
180
  where: {
@@ -285,6 +291,41 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
285
291
  const apiRouter = createRouter({ pg: pool });
286
292
  ```
287
293
 
294
+ ### Request-Level Middleware (onRequest Hook)
295
+
296
+ The `onRequest` hook executes before every endpoint operation, enabling:
297
+ - Setting PostgreSQL session variables for audit logging
298
+ - Configuring Row-Level Security (RLS) based on authenticated user
299
+ - Request-level logging or monitoring
300
+
301
+ ```typescript
302
+ import { createRouter } from "./api/server/router";
303
+
304
+ const apiRouter = createRouter({
305
+ pg,
306
+ onRequest: async (c, pg) => {
307
+ // Access Hono context - fully type-safe
308
+ const auth = c.get('auth');
309
+
310
+ // Set PostgreSQL session variable for audit triggers
311
+ if (auth?.kind === 'jwt' && auth.claims?.sub) {
312
+ await pg.query(`SET LOCAL app.user_id = '${auth.claims.sub}'`);
313
+ }
314
+
315
+ // Or configure RLS policies
316
+ if (auth?.tenant_id) {
317
+ await pg.query(`SET LOCAL app.tenant_id = '${auth.tenant_id}'`);
318
+ }
319
+ }
320
+ });
321
+ ```
322
+
323
+ The hook receives:
324
+ - `c` - Hono Context object with full type safety and IDE autocomplete
325
+ - `pg` - PostgreSQL client for setting session variables
326
+
327
+ **Note:** The router works with or without the `onRequest` hook - fully backward compatible.
328
+
288
329
  ## Server Integration
289
330
 
290
331
  postgresdk generates Hono-compatible routes:
package/dist/cli.js CHANGED
@@ -793,9 +793,9 @@ const items = await sdk.${tableName}.list();
793
793
  const filtered = await sdk.${tableName}.list({
794
794
  limit: 20,
795
795
  offset: 0,
796
- ${table.columns[0]?.name || "field"}_like: 'search',
797
- order_by: '${table.columns[0]?.name || "created_at"}',
798
- order_dir: 'desc'
796
+ where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
797
+ orderBy: '${table.columns[0]?.name || "created_at"}',
798
+ order: 'desc'
799
799
  });`,
800
800
  correspondsTo: `GET ${basePath}`
801
801
  });
@@ -1058,8 +1058,8 @@ function generateQueryParams(table) {
1058
1058
  const params = {
1059
1059
  limit: "number - Max records to return (default: 50)",
1060
1060
  offset: "number - Records to skip",
1061
- order_by: "string - Field to sort by",
1062
- order_dir: "'asc' | 'desc' - Sort direction"
1061
+ orderBy: "string | string[] - Field(s) to sort by",
1062
+ order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
1063
1063
  };
1064
1064
  let filterCount = 0;
1065
1065
  for (const col of table.columns) {
@@ -1351,6 +1351,64 @@ function generateUnifiedContractMarkdown(contract) {
1351
1351
  lines.push("");
1352
1352
  lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1353
1353
  lines.push("");
1354
+ lines.push("## Sorting");
1355
+ lines.push("");
1356
+ lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
1357
+ lines.push("");
1358
+ lines.push("### Single Column Sorting");
1359
+ lines.push("");
1360
+ lines.push("```typescript");
1361
+ lines.push("// Sort by one column ascending");
1362
+ lines.push("const users = await sdk.users.list({");
1363
+ lines.push(" orderBy: 'created_at',");
1364
+ lines.push(" order: 'asc'");
1365
+ lines.push("});");
1366
+ lines.push("");
1367
+ lines.push("// Sort descending");
1368
+ lines.push("const latest = await sdk.users.list({");
1369
+ lines.push(" orderBy: 'created_at',");
1370
+ lines.push(" order: 'desc'");
1371
+ lines.push("});");
1372
+ lines.push("");
1373
+ lines.push("// Order defaults to 'asc' if not specified");
1374
+ lines.push("const sorted = await sdk.users.list({");
1375
+ lines.push(" orderBy: 'name'");
1376
+ lines.push("});");
1377
+ lines.push("```");
1378
+ lines.push("");
1379
+ lines.push("### Multi-Column Sorting");
1380
+ lines.push("");
1381
+ lines.push("```typescript");
1382
+ lines.push("// Sort by multiple columns (all same direction)");
1383
+ lines.push("const users = await sdk.users.list({");
1384
+ lines.push(" orderBy: ['status', 'created_at'],");
1385
+ lines.push(" order: 'desc'");
1386
+ lines.push("});");
1387
+ lines.push("");
1388
+ lines.push("// Different direction per column");
1389
+ lines.push("const sorted = await sdk.users.list({");
1390
+ lines.push(" orderBy: ['status', 'created_at'],");
1391
+ lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
1392
+ lines.push("});");
1393
+ lines.push("```");
1394
+ lines.push("");
1395
+ lines.push("### Combining Sorting with Filters");
1396
+ lines.push("");
1397
+ lines.push("```typescript");
1398
+ lines.push("const results = await sdk.users.list({");
1399
+ lines.push(" where: {");
1400
+ lines.push(" status: 'active',");
1401
+ lines.push(" age: { $gte: 18 }");
1402
+ lines.push(" },");
1403
+ lines.push(" orderBy: 'created_at',");
1404
+ lines.push(" order: 'desc',");
1405
+ lines.push(" limit: 50,");
1406
+ lines.push(" offset: 0");
1407
+ lines.push("});");
1408
+ lines.push("```");
1409
+ lines.push("");
1410
+ lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
1411
+ lines.push("");
1354
1412
  lines.push("## Resources");
1355
1413
  lines.push("");
1356
1414
  for (const resource of contract.resources) {
@@ -2625,6 +2683,7 @@ function emitHonoRoutes(table, _graph, opts) {
2625
2683
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
2626
2684
  const ext = opts.useJsExtensions ? ".js" : "";
2627
2685
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
2686
+ const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
2628
2687
  return `/**
2629
2688
  * AUTO-GENERATED FILE - DO NOT EDIT
2630
2689
  *
@@ -2634,21 +2693,25 @@ function emitHonoRoutes(table, _graph, opts) {
2634
2693
  * To make changes, modify your schema or configuration and regenerate.
2635
2694
  */
2636
2695
  import { Hono } from "hono";
2696
+ import type { Context } from "hono";
2637
2697
  import { z } from "zod";
2638
2698
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
2639
2699
  import { loadIncludes } from "../include-loader${ext}";
2640
2700
  import * as coreOps from "../core/operations${ext}";
2641
2701
  ${authImport}
2642
2702
 
2703
+ const columnEnum = z.enum([${columnNames}]);
2704
+
2643
2705
  const listSchema = z.object({
2644
2706
  where: z.any().optional(),
2645
2707
  include: z.any().optional(),
2646
2708
  limit: z.number().int().positive().max(100).optional(),
2647
2709
  offset: z.number().int().min(0).optional(),
2648
- orderBy: z.any().optional()
2710
+ orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2711
+ order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
2649
2712
  });
2650
2713
 
2651
- export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }) {
2714
+ 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> }) {
2652
2715
  const base = "/v1/${fileTableName}";
2653
2716
 
2654
2717
  // Create operation context
@@ -2668,12 +2731,16 @@ ${hasAuth ? `
2668
2731
  app.post(base, async (c) => {
2669
2732
  const body = await c.req.json().catch(() => ({}));
2670
2733
  const parsed = Insert${Type}Schema.safeParse(body);
2671
-
2734
+
2672
2735
  if (!parsed.success) {
2673
2736
  const issues = parsed.error.flatten();
2674
2737
  return c.json({ error: "Invalid body", issues }, 400);
2675
2738
  }
2676
-
2739
+
2740
+ if (deps.onRequest) {
2741
+ await deps.onRequest(c, deps.pg);
2742
+ }
2743
+
2677
2744
  const result = await coreOps.createRecord(ctx, parsed.data);
2678
2745
 
2679
2746
  if (result.error) {
@@ -2686,6 +2753,11 @@ ${hasAuth ? `
2686
2753
  // GET BY PK
2687
2754
  app.get(\`\${base}/${pkPath}\`, async (c) => {
2688
2755
  ${getPkParams}
2756
+
2757
+ if (deps.onRequest) {
2758
+ await deps.onRequest(c, deps.pg);
2759
+ }
2760
+
2689
2761
  const result = await coreOps.getByPk(ctx, pkValues);
2690
2762
 
2691
2763
  if (result.error) {
@@ -2698,12 +2770,16 @@ ${hasAuth ? `
2698
2770
  // LIST
2699
2771
  app.post(\`\${base}/list\`, async (c) => {
2700
2772
  const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
2701
-
2773
+
2702
2774
  if (!body.success) {
2703
2775
  const issues = body.error.flatten();
2704
2776
  return c.json({ error: "Invalid body", issues }, 400);
2705
2777
  }
2706
-
2778
+
2779
+ if (deps.onRequest) {
2780
+ await deps.onRequest(c, deps.pg);
2781
+ }
2782
+
2707
2783
  const result = await coreOps.listRecords(ctx, body.data);
2708
2784
 
2709
2785
  if (result.error) {
@@ -2749,12 +2825,16 @@ ${hasAuth ? `
2749
2825
  ${getPkParams}
2750
2826
  const body = await c.req.json().catch(() => ({}));
2751
2827
  const parsed = Update${Type}Schema.safeParse(body);
2752
-
2828
+
2753
2829
  if (!parsed.success) {
2754
2830
  const issues = parsed.error.flatten();
2755
2831
  return c.json({ error: "Invalid body", issues }, 400);
2756
2832
  }
2757
-
2833
+
2834
+ if (deps.onRequest) {
2835
+ await deps.onRequest(c, deps.pg);
2836
+ }
2837
+
2758
2838
  const result = await coreOps.updateRecord(ctx, pkValues, parsed.data);
2759
2839
 
2760
2840
  if (result.error) {
@@ -2767,6 +2847,11 @@ ${hasAuth ? `
2767
2847
  // DELETE
2768
2848
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
2769
2849
  ${getPkParams}
2850
+
2851
+ if (deps.onRequest) {
2852
+ await deps.onRequest(c, deps.pg);
2853
+ }
2854
+
2770
2855
  const result = await coreOps.deleteRecord(ctx, pkValues);
2771
2856
 
2772
2857
  if (result.error) {
@@ -2812,7 +2897,7 @@ function emitClient(table, graph, opts, model) {
2812
2897
  let includeMethodsCode = "";
2813
2898
  for (const method of includeMethods) {
2814
2899
  const isGetByPk = method.name.startsWith("getByPk");
2815
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2900
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
2816
2901
  if (isGetByPk) {
2817
2902
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2818
2903
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -2868,8 +2953,8 @@ export class ${Type}Client extends BaseClient {
2868
2953
  limit?: number;
2869
2954
  offset?: number;
2870
2955
  where?: Where<Select${Type}>;
2871
- orderBy?: string;
2872
- order?: "asc" | "desc";
2956
+ orderBy?: string | string[];
2957
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2873
2958
  }): Promise<Select${Type}[]> {
2874
2959
  return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
2875
2960
  }
@@ -3777,6 +3862,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
3777
3862
  * To make changes, modify your schema or configuration and regenerate.
3778
3863
  */
3779
3864
  import { Hono } from "hono";
3865
+ import type { Context } from "hono";
3780
3866
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
3781
3867
  import { getContract } from "./contract${ext}";
3782
3868
  ${imports}
@@ -3784,32 +3870,46 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
3784
3870
 
3785
3871
  /**
3786
3872
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
3787
- *
3873
+ *
3788
3874
  * @example
3789
3875
  * import { Hono } from "hono";
3790
3876
  * import { createRouter } from "./generated/server/router";
3791
- *
3877
+ *
3792
3878
  * // Using pg driver (Node.js)
3793
3879
  * import { Client } from "pg";
3794
3880
  * const pg = new Client({ connectionString: process.env.DATABASE_URL });
3795
3881
  * await pg.connect();
3796
- *
3882
+ *
3797
3883
  * // OR using Neon driver (Edge-compatible)
3798
3884
  * import { Pool } from "@neondatabase/serverless";
3799
3885
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
3800
3886
  * const pg = pool; // Pool already has the compatible query method
3801
- *
3887
+ *
3802
3888
  * // Mount all generated routes
3803
3889
  * const app = new Hono();
3804
3890
  * const apiRouter = createRouter({ pg });
3805
3891
  * app.route("/api", apiRouter);
3806
- *
3892
+ *
3807
3893
  * // Or mount directly at root
3808
3894
  * const router = createRouter({ pg });
3809
3895
  * app.route("/", router);
3896
+ *
3897
+ * // With onRequest hook for audit logging or session variables
3898
+ * const router = createRouter({
3899
+ * pg,
3900
+ * onRequest: async (c, pg) => {
3901
+ * const auth = c.get('auth'); // Type-safe! IDE autocomplete works
3902
+ * if (auth?.kind === 'jwt' && auth.claims?.sub) {
3903
+ * await pg.query(\`SET LOCAL app.user_id = '\${auth.claims.sub}'\`);
3904
+ * }
3905
+ * }
3906
+ * });
3810
3907
  */
3811
3908
  export function createRouter(
3812
- deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
3909
+ deps: {
3910
+ pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> },
3911
+ onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void>
3912
+ }
3813
3913
  ): Hono {
3814
3914
  const router = new Hono();
3815
3915
 
@@ -3868,22 +3968,36 @@ ${registrations}
3868
3968
 
3869
3969
  /**
3870
3970
  * Register all generated routes directly on an existing Hono app.
3871
- *
3971
+ *
3872
3972
  * @example
3873
3973
  * import { Hono } from "hono";
3874
3974
  * import { registerAllRoutes } from "./generated/server/router";
3875
- *
3975
+ *
3876
3976
  * const app = new Hono();
3877
- *
3977
+ *
3878
3978
  * // Setup database connection (see createRouter example for both pg and Neon options)
3879
3979
  * const pg = yourDatabaseClient;
3880
- *
3980
+ *
3881
3981
  * // Register all routes at once
3882
3982
  * registerAllRoutes(app, { pg });
3983
+ *
3984
+ * // With onRequest hook
3985
+ * registerAllRoutes(app, {
3986
+ * pg,
3987
+ * onRequest: async (c, pg) => {
3988
+ * const auth = c.get('auth'); // Type-safe!
3989
+ * if (auth?.kind === 'jwt' && auth.claims?.sub) {
3990
+ * await pg.query(\`SET LOCAL app.user_id = '\${auth.claims.sub}'\`);
3991
+ * }
3992
+ * }
3993
+ * });
3883
3994
  */
3884
3995
  export function registerAllRoutes(
3885
3996
  app: Hono,
3886
- deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
3997
+ deps: {
3998
+ pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> },
3999
+ onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void>
4000
+ }
3887
4001
  ) {
3888
4002
  ${registrations.replace(/router/g, "app")}
3889
4003
  }
@@ -4174,10 +4288,10 @@ function buildWhereClause(
4174
4288
  */
4175
4289
  export async function listRecords(
4176
4290
  ctx: OperationContext,
4177
- params: { where?: any; limit?: number; offset?: number; include?: any }
4291
+ params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
4178
4292
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
4179
4293
  try {
4180
- const { where: whereClause, limit = 50, offset = 0, include } = params;
4294
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
4181
4295
 
4182
4296
  // Build WHERE clause
4183
4297
  let paramIndex = 1;
@@ -4201,12 +4315,26 @@ export async function listRecords(
4201
4315
 
4202
4316
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
4203
4317
 
4318
+ // Build ORDER BY clause
4319
+ let orderBySQL = "";
4320
+ if (orderBy) {
4321
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
4322
+ const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
4323
+
4324
+ const orderParts = columns.map((col, i) => {
4325
+ const dir = (directions[i] || "asc").toUpperCase();
4326
+ return \`"\${col}" \${dir}\`;
4327
+ });
4328
+
4329
+ orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
4330
+ }
4331
+
4204
4332
  // Add limit and offset params
4205
4333
  const limitParam = \`$\${paramIndex}\`;
4206
4334
  const offsetParam = \`$\${paramIndex + 1}\`;
4207
4335
  const allParams = [...whereParams, limit, offset];
4208
4336
 
4209
- const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4337
+ const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
4210
4338
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
4211
4339
 
4212
4340
  const { rows } = await ctx.pg.query(text, allParams);
@@ -39,6 +39,8 @@ export declare function listRecords(ctx: OperationContext, params: {
39
39
  limit?: number;
40
40
  offset?: number;
41
41
  include?: any;
42
+ orderBy?: string | string[];
43
+ order?: "asc" | "desc" | ("asc" | "desc")[];
42
44
  }): Promise<{
43
45
  data?: any;
44
46
  error?: string;
package/dist/index.js CHANGED
@@ -792,9 +792,9 @@ const items = await sdk.${tableName}.list();
792
792
  const filtered = await sdk.${tableName}.list({
793
793
  limit: 20,
794
794
  offset: 0,
795
- ${table.columns[0]?.name || "field"}_like: 'search',
796
- order_by: '${table.columns[0]?.name || "created_at"}',
797
- order_dir: 'desc'
795
+ where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
796
+ orderBy: '${table.columns[0]?.name || "created_at"}',
797
+ order: 'desc'
798
798
  });`,
799
799
  correspondsTo: `GET ${basePath}`
800
800
  });
@@ -1057,8 +1057,8 @@ function generateQueryParams(table) {
1057
1057
  const params = {
1058
1058
  limit: "number - Max records to return (default: 50)",
1059
1059
  offset: "number - Records to skip",
1060
- order_by: "string - Field to sort by",
1061
- order_dir: "'asc' | 'desc' - Sort direction"
1060
+ orderBy: "string | string[] - Field(s) to sort by",
1061
+ order: "'asc' | 'desc' | ('asc' | 'desc')[] - Sort direction(s)"
1062
1062
  };
1063
1063
  let filterCount = 0;
1064
1064
  for (const col of table.columns) {
@@ -1350,6 +1350,64 @@ function generateUnifiedContractMarkdown(contract) {
1350
1350
  lines.push("");
1351
1351
  lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1352
1352
  lines.push("");
1353
+ lines.push("## Sorting");
1354
+ lines.push("");
1355
+ lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
1356
+ lines.push("");
1357
+ lines.push("### Single Column Sorting");
1358
+ lines.push("");
1359
+ lines.push("```typescript");
1360
+ lines.push("// Sort by one column ascending");
1361
+ lines.push("const users = await sdk.users.list({");
1362
+ lines.push(" orderBy: 'created_at',");
1363
+ lines.push(" order: 'asc'");
1364
+ lines.push("});");
1365
+ lines.push("");
1366
+ lines.push("// Sort descending");
1367
+ lines.push("const latest = await sdk.users.list({");
1368
+ lines.push(" orderBy: 'created_at',");
1369
+ lines.push(" order: 'desc'");
1370
+ lines.push("});");
1371
+ lines.push("");
1372
+ lines.push("// Order defaults to 'asc' if not specified");
1373
+ lines.push("const sorted = await sdk.users.list({");
1374
+ lines.push(" orderBy: 'name'");
1375
+ lines.push("});");
1376
+ lines.push("```");
1377
+ lines.push("");
1378
+ lines.push("### Multi-Column Sorting");
1379
+ lines.push("");
1380
+ lines.push("```typescript");
1381
+ lines.push("// Sort by multiple columns (all same direction)");
1382
+ lines.push("const users = await sdk.users.list({");
1383
+ lines.push(" orderBy: ['status', 'created_at'],");
1384
+ lines.push(" order: 'desc'");
1385
+ lines.push("});");
1386
+ lines.push("");
1387
+ lines.push("// Different direction per column");
1388
+ lines.push("const sorted = await sdk.users.list({");
1389
+ lines.push(" orderBy: ['status', 'created_at'],");
1390
+ lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
1391
+ lines.push("});");
1392
+ lines.push("```");
1393
+ lines.push("");
1394
+ lines.push("### Combining Sorting with Filters");
1395
+ lines.push("");
1396
+ lines.push("```typescript");
1397
+ lines.push("const results = await sdk.users.list({");
1398
+ lines.push(" where: {");
1399
+ lines.push(" status: 'active',");
1400
+ lines.push(" age: { $gte: 18 }");
1401
+ lines.push(" },");
1402
+ lines.push(" orderBy: 'created_at',");
1403
+ lines.push(" order: 'desc',");
1404
+ lines.push(" limit: 50,");
1405
+ lines.push(" offset: 0");
1406
+ lines.push("});");
1407
+ lines.push("```");
1408
+ lines.push("");
1409
+ lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
1410
+ lines.push("");
1353
1411
  lines.push("## Resources");
1354
1412
  lines.push("");
1355
1413
  for (const resource of contract.resources) {
@@ -1865,6 +1923,7 @@ function emitHonoRoutes(table, _graph, opts) {
1865
1923
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
1866
1924
  const ext = opts.useJsExtensions ? ".js" : "";
1867
1925
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
1926
+ const columnNames = table.columns.map((c) => `"${c.name}"`).join(", ");
1868
1927
  return `/**
1869
1928
  * AUTO-GENERATED FILE - DO NOT EDIT
1870
1929
  *
@@ -1874,21 +1933,25 @@ function emitHonoRoutes(table, _graph, opts) {
1874
1933
  * To make changes, modify your schema or configuration and regenerate.
1875
1934
  */
1876
1935
  import { Hono } from "hono";
1936
+ import type { Context } from "hono";
1877
1937
  import { z } from "zod";
1878
1938
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
1879
1939
  import { loadIncludes } from "../include-loader${ext}";
1880
1940
  import * as coreOps from "../core/operations${ext}";
1881
1941
  ${authImport}
1882
1942
 
1943
+ const columnEnum = z.enum([${columnNames}]);
1944
+
1883
1945
  const listSchema = z.object({
1884
1946
  where: z.any().optional(),
1885
1947
  include: z.any().optional(),
1886
1948
  limit: z.number().int().positive().max(100).optional(),
1887
1949
  offset: z.number().int().min(0).optional(),
1888
- orderBy: z.any().optional()
1950
+ orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
1951
+ order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional()
1889
1952
  });
1890
1953
 
1891
- export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }) {
1954
+ 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> }) {
1892
1955
  const base = "/v1/${fileTableName}";
1893
1956
 
1894
1957
  // Create operation context
@@ -1908,12 +1971,16 @@ ${hasAuth ? `
1908
1971
  app.post(base, async (c) => {
1909
1972
  const body = await c.req.json().catch(() => ({}));
1910
1973
  const parsed = Insert${Type}Schema.safeParse(body);
1911
-
1974
+
1912
1975
  if (!parsed.success) {
1913
1976
  const issues = parsed.error.flatten();
1914
1977
  return c.json({ error: "Invalid body", issues }, 400);
1915
1978
  }
1916
-
1979
+
1980
+ if (deps.onRequest) {
1981
+ await deps.onRequest(c, deps.pg);
1982
+ }
1983
+
1917
1984
  const result = await coreOps.createRecord(ctx, parsed.data);
1918
1985
 
1919
1986
  if (result.error) {
@@ -1926,6 +1993,11 @@ ${hasAuth ? `
1926
1993
  // GET BY PK
1927
1994
  app.get(\`\${base}/${pkPath}\`, async (c) => {
1928
1995
  ${getPkParams}
1996
+
1997
+ if (deps.onRequest) {
1998
+ await deps.onRequest(c, deps.pg);
1999
+ }
2000
+
1929
2001
  const result = await coreOps.getByPk(ctx, pkValues);
1930
2002
 
1931
2003
  if (result.error) {
@@ -1938,12 +2010,16 @@ ${hasAuth ? `
1938
2010
  // LIST
1939
2011
  app.post(\`\${base}/list\`, async (c) => {
1940
2012
  const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
1941
-
2013
+
1942
2014
  if (!body.success) {
1943
2015
  const issues = body.error.flatten();
1944
2016
  return c.json({ error: "Invalid body", issues }, 400);
1945
2017
  }
1946
-
2018
+
2019
+ if (deps.onRequest) {
2020
+ await deps.onRequest(c, deps.pg);
2021
+ }
2022
+
1947
2023
  const result = await coreOps.listRecords(ctx, body.data);
1948
2024
 
1949
2025
  if (result.error) {
@@ -1989,12 +2065,16 @@ ${hasAuth ? `
1989
2065
  ${getPkParams}
1990
2066
  const body = await c.req.json().catch(() => ({}));
1991
2067
  const parsed = Update${Type}Schema.safeParse(body);
1992
-
2068
+
1993
2069
  if (!parsed.success) {
1994
2070
  const issues = parsed.error.flatten();
1995
2071
  return c.json({ error: "Invalid body", issues }, 400);
1996
2072
  }
1997
-
2073
+
2074
+ if (deps.onRequest) {
2075
+ await deps.onRequest(c, deps.pg);
2076
+ }
2077
+
1998
2078
  const result = await coreOps.updateRecord(ctx, pkValues, parsed.data);
1999
2079
 
2000
2080
  if (result.error) {
@@ -2007,6 +2087,11 @@ ${hasAuth ? `
2007
2087
  // DELETE
2008
2088
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
2009
2089
  ${getPkParams}
2090
+
2091
+ if (deps.onRequest) {
2092
+ await deps.onRequest(c, deps.pg);
2093
+ }
2094
+
2010
2095
  const result = await coreOps.deleteRecord(ctx, pkValues);
2011
2096
 
2012
2097
  if (result.error) {
@@ -2052,7 +2137,7 @@ function emitClient(table, graph, opts, model) {
2052
2137
  let includeMethodsCode = "";
2053
2138
  for (const method of includeMethods) {
2054
2139
  const isGetByPk = method.name.startsWith("getByPk");
2055
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2140
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[]; }, "include">`;
2056
2141
  if (isGetByPk) {
2057
2142
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2058
2143
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -2108,8 +2193,8 @@ export class ${Type}Client extends BaseClient {
2108
2193
  limit?: number;
2109
2194
  offset?: number;
2110
2195
  where?: Where<Select${Type}>;
2111
- orderBy?: string;
2112
- order?: "asc" | "desc";
2196
+ orderBy?: string | string[];
2197
+ order?: "asc" | "desc" | ("asc" | "desc")[];
2113
2198
  }): Promise<Select${Type}[]> {
2114
2199
  return this.post<Select${Type}[]>(\`\${this.resource}/list\`, params ?? {});
2115
2200
  }
@@ -3017,6 +3102,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
3017
3102
  * To make changes, modify your schema or configuration and regenerate.
3018
3103
  */
3019
3104
  import { Hono } from "hono";
3105
+ import type { Context } from "hono";
3020
3106
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
3021
3107
  import { getContract } from "./contract${ext}";
3022
3108
  ${imports}
@@ -3024,32 +3110,46 @@ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
3024
3110
 
3025
3111
  /**
3026
3112
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
3027
- *
3113
+ *
3028
3114
  * @example
3029
3115
  * import { Hono } from "hono";
3030
3116
  * import { createRouter } from "./generated/server/router";
3031
- *
3117
+ *
3032
3118
  * // Using pg driver (Node.js)
3033
3119
  * import { Client } from "pg";
3034
3120
  * const pg = new Client({ connectionString: process.env.DATABASE_URL });
3035
3121
  * await pg.connect();
3036
- *
3122
+ *
3037
3123
  * // OR using Neon driver (Edge-compatible)
3038
3124
  * import { Pool } from "@neondatabase/serverless";
3039
3125
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
3040
3126
  * const pg = pool; // Pool already has the compatible query method
3041
- *
3127
+ *
3042
3128
  * // Mount all generated routes
3043
3129
  * const app = new Hono();
3044
3130
  * const apiRouter = createRouter({ pg });
3045
3131
  * app.route("/api", apiRouter);
3046
- *
3132
+ *
3047
3133
  * // Or mount directly at root
3048
3134
  * const router = createRouter({ pg });
3049
3135
  * app.route("/", router);
3136
+ *
3137
+ * // With onRequest hook for audit logging or session variables
3138
+ * const router = createRouter({
3139
+ * pg,
3140
+ * onRequest: async (c, pg) => {
3141
+ * const auth = c.get('auth'); // Type-safe! IDE autocomplete works
3142
+ * if (auth?.kind === 'jwt' && auth.claims?.sub) {
3143
+ * await pg.query(\`SET LOCAL app.user_id = '\${auth.claims.sub}'\`);
3144
+ * }
3145
+ * }
3146
+ * });
3050
3147
  */
3051
3148
  export function createRouter(
3052
- deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
3149
+ deps: {
3150
+ pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> },
3151
+ onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void>
3152
+ }
3053
3153
  ): Hono {
3054
3154
  const router = new Hono();
3055
3155
 
@@ -3108,22 +3208,36 @@ ${registrations}
3108
3208
 
3109
3209
  /**
3110
3210
  * Register all generated routes directly on an existing Hono app.
3111
- *
3211
+ *
3112
3212
  * @example
3113
3213
  * import { Hono } from "hono";
3114
3214
  * import { registerAllRoutes } from "./generated/server/router";
3115
- *
3215
+ *
3116
3216
  * const app = new Hono();
3117
- *
3217
+ *
3118
3218
  * // Setup database connection (see createRouter example for both pg and Neon options)
3119
3219
  * const pg = yourDatabaseClient;
3120
- *
3220
+ *
3121
3221
  * // Register all routes at once
3122
3222
  * registerAllRoutes(app, { pg });
3223
+ *
3224
+ * // With onRequest hook
3225
+ * registerAllRoutes(app, {
3226
+ * pg,
3227
+ * onRequest: async (c, pg) => {
3228
+ * const auth = c.get('auth'); // Type-safe!
3229
+ * if (auth?.kind === 'jwt' && auth.claims?.sub) {
3230
+ * await pg.query(\`SET LOCAL app.user_id = '\${auth.claims.sub}'\`);
3231
+ * }
3232
+ * }
3233
+ * });
3123
3234
  */
3124
3235
  export function registerAllRoutes(
3125
3236
  app: Hono,
3126
- deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }
3237
+ deps: {
3238
+ pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> },
3239
+ onRequest?: (c: Context, pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> }) => Promise<void>
3240
+ }
3127
3241
  ) {
3128
3242
  ${registrations.replace(/router/g, "app")}
3129
3243
  }
@@ -3414,10 +3528,10 @@ function buildWhereClause(
3414
3528
  */
3415
3529
  export async function listRecords(
3416
3530
  ctx: OperationContext,
3417
- params: { where?: any; limit?: number; offset?: number; include?: any }
3531
+ params: { where?: any; limit?: number; offset?: number; include?: any; orderBy?: string | string[]; order?: "asc" | "desc" | ("asc" | "desc")[] }
3418
3532
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3419
3533
  try {
3420
- const { where: whereClause, limit = 50, offset = 0, include } = params;
3534
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order } = params;
3421
3535
 
3422
3536
  // Build WHERE clause
3423
3537
  let paramIndex = 1;
@@ -3441,12 +3555,26 @@ export async function listRecords(
3441
3555
 
3442
3556
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
3443
3557
 
3558
+ // Build ORDER BY clause
3559
+ let orderBySQL = "";
3560
+ if (orderBy) {
3561
+ const columns = Array.isArray(orderBy) ? orderBy : [orderBy];
3562
+ const directions = Array.isArray(order) ? order : (order ? Array(columns.length).fill(order) : Array(columns.length).fill("asc"));
3563
+
3564
+ const orderParts = columns.map((col, i) => {
3565
+ const dir = (directions[i] || "asc").toUpperCase();
3566
+ return \`"\${col}" \${dir}\`;
3567
+ });
3568
+
3569
+ orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
3570
+ }
3571
+
3444
3572
  // Add limit and offset params
3445
3573
  const limitParam = \`$\${paramIndex}\`;
3446
3574
  const offsetParam = \`$\${paramIndex + 1}\`;
3447
3575
  const allParams = [...whereParams, limit, offset];
3448
3576
 
3449
- const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3577
+ const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3450
3578
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3451
3579
 
3452
3580
  const { rows } = await ctx.pg.query(text, allParams);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.10.4",
3
+ "version": "0.12.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {