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 +42 -1
- package/dist/cli.js +158 -30
- package/dist/core/operations.d.ts +2 -0
- package/dist/index.js +158 -30
- package/package.json +1 -1
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"}
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
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.
|
|
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: {
|
|
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: {
|
|
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);
|
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"}
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
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.
|
|
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: {
|
|
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: {
|
|
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);
|