postgresdk 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -1
  3. package/dist/cli.js +337 -117
  4. package/dist/index.js +337 -117
  5. package/package.json +11 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 postgresdk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -178,9 +178,46 @@ const filtered = await sdk.users.list({
178
178
  deleted_at: { $is: null } // NULL checks
179
179
  }
180
180
  });
181
+
182
+ // OR logic - match any condition
183
+ const results = await sdk.users.list({
184
+ where: {
185
+ $or: [
186
+ { email: { $ilike: '%@gmail.com' } },
187
+ { email: { $ilike: '%@yahoo.com' } },
188
+ { status: 'premium' }
189
+ ]
190
+ }
191
+ });
192
+
193
+ // Complex queries with AND/OR
194
+ const complex = await sdk.users.list({
195
+ where: {
196
+ status: 'active', // Implicit AND at root level
197
+ $or: [
198
+ { age: { $lt: 18 } },
199
+ { age: { $gt: 65 } }
200
+ ]
201
+ }
202
+ });
203
+
204
+ // Nested logic (2 levels)
205
+ const nested = await sdk.users.list({
206
+ where: {
207
+ $and: [
208
+ {
209
+ $or: [
210
+ { firstName: { $ilike: '%john%' } },
211
+ { lastName: { $ilike: '%john%' } }
212
+ ]
213
+ },
214
+ { status: 'active' }
215
+ ]
216
+ }
217
+ });
181
218
  ```
182
219
 
183
- See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`.
220
+ See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`.
184
221
 
185
222
  ## Authentication
186
223
 
package/dist/cli.js CHANGED
@@ -1302,6 +1302,53 @@ function generateUnifiedContractMarkdown(contract) {
1302
1302
  lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1303
1303
  lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
1304
1304
  lines.push("");
1305
+ lines.push("### Logical Operators");
1306
+ lines.push("");
1307
+ lines.push("Combine conditions using `$or` and `$and` (supports 2 levels of nesting):");
1308
+ lines.push("");
1309
+ lines.push("| Operator | Description | Example |");
1310
+ lines.push("|----------|-------------|---------|");
1311
+ lines.push("| `$or` | Match any condition | `{ $or: [{ status: 'active' }, { role: 'admin' }] }` |");
1312
+ lines.push("| `$and` | Match all conditions (explicit) | `{ $and: [{ age: { $gte: 18 } }, { status: 'verified' }] }` |");
1313
+ lines.push("");
1314
+ lines.push("```typescript");
1315
+ lines.push("// OR - match any condition");
1316
+ lines.push("const results = await sdk.users.list({");
1317
+ lines.push(" where: {");
1318
+ lines.push(" $or: [");
1319
+ lines.push(" { email: { $ilike: '%@gmail.com' } },");
1320
+ lines.push(" { status: 'premium' }");
1321
+ lines.push(" ]");
1322
+ lines.push(" }");
1323
+ lines.push("});");
1324
+ lines.push("");
1325
+ lines.push("// Mixed AND + OR (implicit AND at root level)");
1326
+ lines.push("const complex = await sdk.users.list({");
1327
+ lines.push(" where: {");
1328
+ lines.push(" status: 'active', // AND");
1329
+ lines.push(" $or: [");
1330
+ lines.push(" { age: { $lt: 18 } },");
1331
+ lines.push(" { age: { $gt: 65 } }");
1332
+ lines.push(" ]");
1333
+ lines.push(" }");
1334
+ lines.push("});");
1335
+ lines.push("");
1336
+ lines.push("// Nested (2 levels max)");
1337
+ lines.push("const nested = await sdk.users.list({");
1338
+ lines.push(" where: {");
1339
+ lines.push(" $and: [");
1340
+ lines.push(" {");
1341
+ lines.push(" $or: [");
1342
+ lines.push(" { firstName: { $ilike: '%john%' } },");
1343
+ lines.push(" { lastName: { $ilike: '%john%' } }");
1344
+ lines.push(" ]");
1345
+ lines.push(" },");
1346
+ lines.push(" { status: 'active' }");
1347
+ lines.push(" ]");
1348
+ lines.push(" }");
1349
+ lines.push("});");
1350
+ lines.push("```");
1351
+ lines.push("");
1305
1352
  lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1306
1353
  lines.push("");
1307
1354
  lines.push("## Resources");
@@ -2387,7 +2434,14 @@ function buildGraph(model) {
2387
2434
 
2388
2435
  // src/emit-include-spec.ts
2389
2436
  function emitIncludeSpec(graph) {
2390
- let out = `/* Generated. Do not edit. */
2437
+ let out = `/**
2438
+ * AUTO-GENERATED FILE - DO NOT EDIT
2439
+ *
2440
+ * This file was automatically generated by PostgreSDK.
2441
+ * Any manual changes will be overwritten on the next generation.
2442
+ *
2443
+ * To make changes, modify your schema or configuration and regenerate.
2444
+ */
2391
2445
  `;
2392
2446
  const tables = Object.keys(graph);
2393
2447
  for (const table of tables) {
@@ -2416,7 +2470,14 @@ function toPascal(s) {
2416
2470
 
2417
2471
  // src/emit-include-builder.ts
2418
2472
  function emitIncludeBuilder(graph, maxDepth) {
2419
- return `// Generated. Do not edit.
2473
+ return `/**
2474
+ * AUTO-GENERATED FILE - DO NOT EDIT
2475
+ *
2476
+ * This file was automatically generated by PostgreSDK.
2477
+ * Any manual changes will be overwritten on the next generation.
2478
+ *
2479
+ * To make changes, modify your schema or configuration and regenerate.
2480
+ */
2420
2481
  export const RELATION_GRAPH = ${JSON.stringify(graph, null, 2)} as const;
2421
2482
  type TableName = keyof typeof RELATION_GRAPH;
2422
2483
 
@@ -2564,7 +2625,14 @@ function emitHonoRoutes(table, _graph, opts) {
2564
2625
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
2565
2626
  const ext = opts.useJsExtensions ? ".js" : "";
2566
2627
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
2567
- return `/* Generated. Do not edit. */
2628
+ return `/**
2629
+ * AUTO-GENERATED FILE - DO NOT EDIT
2630
+ *
2631
+ * This file was automatically generated by PostgreSDK.
2632
+ * Any manual changes will be overwritten on the next generation.
2633
+ *
2634
+ * To make changes, modify your schema or configuration and regenerate.
2635
+ */
2568
2636
  import { Hono } from "hono";
2569
2637
  import { z } from "zod";
2570
2638
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
@@ -2766,7 +2834,14 @@ function emitClient(table, graph, opts, model) {
2766
2834
  `;
2767
2835
  }
2768
2836
  }
2769
- return `/* Generated. Do not edit. */
2837
+ return `/**
2838
+ * AUTO-GENERATED FILE - DO NOT EDIT
2839
+ *
2840
+ * This file was automatically generated by PostgreSDK.
2841
+ * Any manual changes will be overwritten on the next generation.
2842
+ *
2843
+ * To make changes, modify your schema or configuration and regenerate.
2844
+ */
2770
2845
  import { BaseClient } from "./base-client${ext}";
2771
2846
  import type { Where } from "./where-types${ext}";
2772
2847
  ${typeImports}
@@ -2813,7 +2888,14 @@ ${includeMethodsCode}}
2813
2888
  }
2814
2889
  function emitClientIndex(tables, useJsExtensions) {
2815
2890
  const ext = useJsExtensions ? ".js" : "";
2816
- let out = `/* Generated. Do not edit. */
2891
+ let out = `/**
2892
+ * AUTO-GENERATED FILE - DO NOT EDIT
2893
+ *
2894
+ * This file was automatically generated by PostgreSDK.
2895
+ * Any manual changes will be overwritten on the next generation.
2896
+ *
2897
+ * To make changes, modify your schema or configuration and regenerate.
2898
+ */
2817
2899
  `;
2818
2900
  out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
2819
2901
  `;
@@ -2886,7 +2968,14 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
2886
2968
 
2887
2969
  // src/emit-base-client.ts
2888
2970
  function emitBaseClient() {
2889
- return `/* Generated. Do not edit. */
2971
+ return `/**
2972
+ * AUTO-GENERATED FILE - DO NOT EDIT
2973
+ *
2974
+ * This file was automatically generated by PostgreSDK.
2975
+ * Any manual changes will be overwritten on the next generation.
2976
+ *
2977
+ * To make changes, modify your schema or configuration and regenerate.
2978
+ */
2890
2979
 
2891
2980
  export type HeaderMap = Record<string, string>;
2892
2981
  export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
@@ -3033,7 +3122,14 @@ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
3033
3122
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
3034
3123
  }
3035
3124
  const ext = useJsExtensions ? ".js" : "";
3036
- return `/* Generated. Do not edit. */
3125
+ return `/**
3126
+ * AUTO-GENERATED FILE - DO NOT EDIT
3127
+ *
3128
+ * This file was automatically generated by PostgreSDK.
3129
+ * Any manual changes will be overwritten on the next generation.
3130
+ *
3131
+ * To make changes, modify your schema or configuration and regenerate.
3132
+ */
3037
3133
  import { RELATION_GRAPH } from "./include-builder${ext}";
3038
3134
 
3039
3135
  // Minimal types to keep the file self-contained
@@ -3354,7 +3450,14 @@ function emitTypes(table, opts) {
3354
3450
  return ` ${col.name}: ${valueType};`;
3355
3451
  }).join(`
3356
3452
  `);
3357
- return `/* Generated. Do not edit. */
3453
+ return `/**
3454
+ * AUTO-GENERATED FILE - DO NOT EDIT
3455
+ *
3456
+ * This file was automatically generated by PostgreSDK.
3457
+ * Any manual changes will be overwritten on the next generation.
3458
+ *
3459
+ * To make changes, modify your schema or configuration and regenerate.
3460
+ */
3358
3461
  export type Insert${Type} = {
3359
3462
  ${insertFields}
3360
3463
  };
@@ -3369,7 +3472,14 @@ ${selectFields}
3369
3472
 
3370
3473
  // src/emit-logger.ts
3371
3474
  function emitLogger() {
3372
- return `/* Generated. Do not edit. */
3475
+ return `/**
3476
+ * AUTO-GENERATED FILE - DO NOT EDIT
3477
+ *
3478
+ * This file was automatically generated by PostgreSDK.
3479
+ * Any manual changes will be overwritten on the next generation.
3480
+ *
3481
+ * To make changes, modify your schema or configuration and regenerate.
3482
+ */
3373
3483
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
3374
3484
 
3375
3485
  export const logger = {
@@ -3404,7 +3514,14 @@ export function safe<T extends (c: any) => any>(handler: T) {
3404
3514
 
3405
3515
  // src/emit-where-types.ts
3406
3516
  function emitWhereTypes() {
3407
- return `/* Generated. Do not edit. */
3517
+ return `/**
3518
+ * AUTO-GENERATED FILE - DO NOT EDIT
3519
+ *
3520
+ * This file was automatically generated by PostgreSDK.
3521
+ * Any manual changes will be overwritten on the next generation.
3522
+ *
3523
+ * To make changes, modify your schema or configuration and regenerate.
3524
+ */
3408
3525
 
3409
3526
  /**
3410
3527
  * WHERE clause operators for filtering
@@ -3442,11 +3559,26 @@ export type WhereOperator<T> = {
3442
3559
  export type WhereCondition<T> = T | WhereOperator<T>;
3443
3560
 
3444
3561
  /**
3445
- * WHERE clause type for a given table type
3562
+ * Field-level WHERE conditions (without logical operators)
3446
3563
  */
3447
- export type Where<T> = {
3564
+ export type WhereFieldConditions<T> = {
3448
3565
  [K in keyof T]?: WhereCondition<T[K]>;
3449
3566
  };
3567
+
3568
+ /**
3569
+ * WHERE clause type with support for $or/$and logical operators (2 levels max)
3570
+ *
3571
+ * Examples:
3572
+ * - Basic OR: { $or: [{ name: 'Alice' }, { name: 'Bob' }] }
3573
+ * - Mixed AND + OR: { status: 'active', $or: [{ age: { $gt: 65 } }, { age: { $lt: 18 } }] }
3574
+ * - Nested (2 levels): { $and: [{ $or: [{ name: 'Alice' }, { name: 'Bob' }] }, { status: 'active' }] }
3575
+ */
3576
+ export type Where<T> = WhereFieldConditions<T> & {
3577
+ /** OR - at least one condition must be true */
3578
+ $or?: (WhereFieldConditions<T>)[];
3579
+ /** AND - all conditions must be true (alternative to implicit root-level AND) */
3580
+ $and?: (WhereFieldConditions<T> | { $or?: WhereFieldConditions<T>[] })[];
3581
+ };
3450
3582
  `;
3451
3583
  }
3452
3584
 
@@ -3464,7 +3596,14 @@ function emitAuth(cfgAuth) {
3464
3596
  const JWT_SHARED_SECRET = JSON.stringify(jwtShared);
3465
3597
  const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
3466
3598
  const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
3467
- return `/* Generated. Do not edit. */
3599
+ return `/**
3600
+ * AUTO-GENERATED FILE - DO NOT EDIT
3601
+ *
3602
+ * This file was automatically generated by PostgreSDK.
3603
+ * Any manual changes will be overwritten on the next generation.
3604
+ *
3605
+ * To make changes, modify your schema or configuration and regenerate.
3606
+ */
3468
3607
  import type { Context, Next } from "hono";
3469
3608
 
3470
3609
  // ---- Config inlined by generator ----
@@ -3621,7 +3760,14 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
3621
3760
  return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
3622
3761
  }).join(`
3623
3762
  `);
3624
- return `/* Generated. Do not edit. */
3763
+ return `/**
3764
+ * AUTO-GENERATED FILE - DO NOT EDIT
3765
+ *
3766
+ * This file was automatically generated by PostgreSDK.
3767
+ * Any manual changes will be overwritten on the next generation.
3768
+ *
3769
+ * To make changes, modify your schema or configuration and regenerate.
3770
+ */
3625
3771
  import { Hono } from "hono";
3626
3772
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
3627
3773
  import { getContract } from "./contract${ext}";
@@ -3755,7 +3901,14 @@ function emitSdkBundle(clientFiles, clientDir) {
3755
3901
  }
3756
3902
  const version = `1.0.0`;
3757
3903
  const generated = new Date().toISOString();
3758
- return `/* Generated. Do not edit. */
3904
+ return `/**
3905
+ * AUTO-GENERATED FILE - DO NOT EDIT
3906
+ *
3907
+ * This file was automatically generated by PostgreSDK.
3908
+ * Any manual changes will be overwritten on the next generation.
3909
+ *
3910
+ * To make changes, modify your schema or configuration and regenerate.
3911
+ */
3759
3912
 
3760
3913
  export const SDK_MANIFEST = {
3761
3914
  version: "${version}",
@@ -3859,6 +4012,155 @@ export async function getByPk(
3859
4012
  }
3860
4013
  }
3861
4014
 
4015
+ /**
4016
+ * Build WHERE clause recursively, supporting $or/$and operators
4017
+ * Returns { sql: string, params: any[], nextParamIndex: number }
4018
+ */
4019
+ function buildWhereClause(
4020
+ whereClause: any,
4021
+ startParamIndex: number
4022
+ ): { sql: string; params: any[]; nextParamIndex: number } {
4023
+ const whereParts: string[] = [];
4024
+ const whereParams: any[] = [];
4025
+ let paramIndex = startParamIndex;
4026
+
4027
+ if (!whereClause || typeof whereClause !== 'object') {
4028
+ return { sql: '', params: [], nextParamIndex: paramIndex };
4029
+ }
4030
+
4031
+ // Separate logical operators from field conditions
4032
+ const { $or, $and, ...fieldConditions } = whereClause;
4033
+
4034
+ // Process field-level conditions
4035
+ for (const [key, value] of Object.entries(fieldConditions)) {
4036
+ if (value === undefined) {
4037
+ continue;
4038
+ }
4039
+
4040
+ // Handle operator objects like { $gt: 5, $like: "%test%" }
4041
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
4042
+ for (const [op, opValue] of Object.entries(value)) {
4043
+ if (opValue === undefined) continue;
4044
+
4045
+ switch (op) {
4046
+ case '$eq':
4047
+ whereParts.push(\`"\${key}" = $\${paramIndex}\`);
4048
+ whereParams.push(opValue);
4049
+ paramIndex++;
4050
+ break;
4051
+ case '$ne':
4052
+ whereParts.push(\`"\${key}" != $\${paramIndex}\`);
4053
+ whereParams.push(opValue);
4054
+ paramIndex++;
4055
+ break;
4056
+ case '$gt':
4057
+ whereParts.push(\`"\${key}" > $\${paramIndex}\`);
4058
+ whereParams.push(opValue);
4059
+ paramIndex++;
4060
+ break;
4061
+ case '$gte':
4062
+ whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
4063
+ whereParams.push(opValue);
4064
+ paramIndex++;
4065
+ break;
4066
+ case '$lt':
4067
+ whereParts.push(\`"\${key}" < $\${paramIndex}\`);
4068
+ whereParams.push(opValue);
4069
+ paramIndex++;
4070
+ break;
4071
+ case '$lte':
4072
+ whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
4073
+ whereParams.push(opValue);
4074
+ paramIndex++;
4075
+ break;
4076
+ case '$in':
4077
+ if (Array.isArray(opValue) && opValue.length > 0) {
4078
+ whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
4079
+ whereParams.push(opValue);
4080
+ paramIndex++;
4081
+ }
4082
+ break;
4083
+ case '$nin':
4084
+ if (Array.isArray(opValue) && opValue.length > 0) {
4085
+ whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
4086
+ whereParams.push(opValue);
4087
+ paramIndex++;
4088
+ }
4089
+ break;
4090
+ case '$like':
4091
+ whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
4092
+ whereParams.push(opValue);
4093
+ paramIndex++;
4094
+ break;
4095
+ case '$ilike':
4096
+ whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
4097
+ whereParams.push(opValue);
4098
+ paramIndex++;
4099
+ break;
4100
+ case '$is':
4101
+ if (opValue === null) {
4102
+ whereParts.push(\`"\${key}" IS NULL\`);
4103
+ }
4104
+ break;
4105
+ case '$isNot':
4106
+ if (opValue === null) {
4107
+ whereParts.push(\`"\${key}" IS NOT NULL\`);
4108
+ }
4109
+ break;
4110
+ }
4111
+ }
4112
+ } else if (value === null) {
4113
+ // Direct null value
4114
+ whereParts.push(\`"\${key}" IS NULL\`);
4115
+ } else {
4116
+ // Direct value (simple equality)
4117
+ whereParts.push(\`"\${key}" = $\${paramIndex}\`);
4118
+ whereParams.push(value);
4119
+ paramIndex++;
4120
+ }
4121
+ }
4122
+
4123
+ // Handle $or operator
4124
+ if ($or && Array.isArray($or)) {
4125
+ if ($or.length === 0) {
4126
+ // Empty OR is logically FALSE - matches nothing
4127
+ whereParts.push('FALSE');
4128
+ } else {
4129
+ const orParts: string[] = [];
4130
+ for (const orCondition of $or) {
4131
+ const result = buildWhereClause(orCondition, paramIndex);
4132
+ if (result.sql) {
4133
+ orParts.push(result.sql);
4134
+ whereParams.push(...result.params);
4135
+ paramIndex = result.nextParamIndex;
4136
+ }
4137
+ }
4138
+ if (orParts.length > 0) {
4139
+ whereParts.push(\`(\${orParts.join(' OR ')})\`);
4140
+ }
4141
+ }
4142
+ }
4143
+
4144
+ // Handle $and operator
4145
+ if ($and && Array.isArray($and) && $and.length > 0) {
4146
+ const andParts: string[] = [];
4147
+ for (const andCondition of $and) {
4148
+ const result = buildWhereClause(andCondition, paramIndex);
4149
+ if (result.sql) {
4150
+ andParts.push(result.sql);
4151
+ whereParams.push(...result.params);
4152
+ paramIndex = result.nextParamIndex;
4153
+ }
4154
+ }
4155
+ if (andParts.length > 0) {
4156
+ whereParts.push(\`(\${andParts.join(' AND ')})\`);
4157
+ }
4158
+ }
4159
+
4160
+ const sql = whereParts.join(' AND ');
4161
+ return { sql, params: whereParams, nextParamIndex: paramIndex };
4162
+ }
4163
+
3862
4164
  /**
3863
4165
  * LIST operation - Get multiple records with optional filters
3864
4166
  */
@@ -3868,136 +4170,54 @@ export async function listRecords(
3868
4170
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3869
4171
  try {
3870
4172
  const { where: whereClause, limit = 50, offset = 0, include } = params;
3871
-
4173
+
3872
4174
  // Build WHERE clause
3873
- const whereParts: string[] = [];
3874
- const whereParams: any[] = [];
3875
4175
  let paramIndex = 1;
3876
-
4176
+ const whereParts: string[] = [];
4177
+ let whereParams: any[] = [];
4178
+
3877
4179
  // Add soft delete filter if applicable
3878
4180
  if (ctx.softDeleteColumn) {
3879
4181
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
3880
4182
  }
3881
-
3882
- // Add user-provided where conditions
3883
- if (whereClause && typeof whereClause === 'object') {
3884
- for (const [key, value] of Object.entries(whereClause)) {
3885
- if (value === undefined) {
3886
- // Skip undefined values
3887
- continue;
3888
- }
3889
4183
 
3890
- // Handle operator objects like { $gt: 5, $like: "%test%" }
3891
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3892
- for (const [op, opValue] of Object.entries(value)) {
3893
- if (opValue === undefined) continue;
3894
-
3895
- switch (op) {
3896
- case '$eq':
3897
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3898
- whereParams.push(opValue);
3899
- paramIndex++;
3900
- break;
3901
- case '$ne':
3902
- whereParts.push(\`"\${key}" != $\${paramIndex}\`);
3903
- whereParams.push(opValue);
3904
- paramIndex++;
3905
- break;
3906
- case '$gt':
3907
- whereParts.push(\`"\${key}" > $\${paramIndex}\`);
3908
- whereParams.push(opValue);
3909
- paramIndex++;
3910
- break;
3911
- case '$gte':
3912
- whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
3913
- whereParams.push(opValue);
3914
- paramIndex++;
3915
- break;
3916
- case '$lt':
3917
- whereParts.push(\`"\${key}" < $\${paramIndex}\`);
3918
- whereParams.push(opValue);
3919
- paramIndex++;
3920
- break;
3921
- case '$lte':
3922
- whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
3923
- whereParams.push(opValue);
3924
- paramIndex++;
3925
- break;
3926
- case '$in':
3927
- if (Array.isArray(opValue) && opValue.length > 0) {
3928
- whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
3929
- whereParams.push(opValue);
3930
- paramIndex++;
3931
- }
3932
- break;
3933
- case '$nin':
3934
- if (Array.isArray(opValue) && opValue.length > 0) {
3935
- whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
3936
- whereParams.push(opValue);
3937
- paramIndex++;
3938
- }
3939
- break;
3940
- case '$like':
3941
- whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
3942
- whereParams.push(opValue);
3943
- paramIndex++;
3944
- break;
3945
- case '$ilike':
3946
- whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
3947
- whereParams.push(opValue);
3948
- paramIndex++;
3949
- break;
3950
- case '$is':
3951
- if (opValue === null) {
3952
- whereParts.push(\`"\${key}" IS NULL\`);
3953
- }
3954
- break;
3955
- case '$isNot':
3956
- if (opValue === null) {
3957
- whereParts.push(\`"\${key}" IS NOT NULL\`);
3958
- }
3959
- break;
3960
- }
3961
- }
3962
- } else if (value === null) {
3963
- // Direct null value
3964
- whereParts.push(\`"\${key}" IS NULL\`);
3965
- } else {
3966
- // Direct value (simple equality)
3967
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3968
- whereParams.push(value);
3969
- paramIndex++;
3970
- }
4184
+ // Add user-provided where conditions
4185
+ if (whereClause) {
4186
+ const result = buildWhereClause(whereClause, paramIndex);
4187
+ if (result.sql) {
4188
+ whereParts.push(result.sql);
4189
+ whereParams = result.params;
4190
+ paramIndex = result.nextParamIndex;
3971
4191
  }
3972
4192
  }
3973
-
4193
+
3974
4194
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
3975
-
4195
+
3976
4196
  // Add limit and offset params
3977
4197
  const limitParam = \`$\${paramIndex}\`;
3978
4198
  const offsetParam = \`$\${paramIndex + 1}\`;
3979
4199
  const allParams = [...whereParams, limit, offset];
3980
-
4200
+
3981
4201
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3982
4202
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3983
-
4203
+
3984
4204
  const { rows } = await ctx.pg.query(text, allParams);
3985
-
4205
+
3986
4206
  if (!include) {
3987
4207
  log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
3988
4208
  return { data: rows, status: 200 };
3989
4209
  }
3990
-
4210
+
3991
4211
  // Include logic will be handled by the include-loader
3992
4212
  // For now, just return the rows with a note that includes need to be applied
3993
4213
  log.debug(\`LIST \${ctx.table} include spec:\`, include);
3994
4214
  return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
3995
4215
  } catch (e: any) {
3996
4216
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
3997
- return {
3998
- error: e?.message ?? "Internal error",
4217
+ return {
4218
+ error: e?.message ?? "Internal error",
3999
4219
  ...(DEBUG ? { stack: e?.stack } : {}),
4000
- status: 500
4220
+ status: 500
4001
4221
  };
4002
4222
  }
4003
4223
  }
package/dist/index.js CHANGED
@@ -1301,6 +1301,53 @@ function generateUnifiedContractMarkdown(contract) {
1301
1301
  lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1302
1302
  lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
1303
1303
  lines.push("");
1304
+ lines.push("### Logical Operators");
1305
+ lines.push("");
1306
+ lines.push("Combine conditions using `$or` and `$and` (supports 2 levels of nesting):");
1307
+ lines.push("");
1308
+ lines.push("| Operator | Description | Example |");
1309
+ lines.push("|----------|-------------|---------|");
1310
+ lines.push("| `$or` | Match any condition | `{ $or: [{ status: 'active' }, { role: 'admin' }] }` |");
1311
+ lines.push("| `$and` | Match all conditions (explicit) | `{ $and: [{ age: { $gte: 18 } }, { status: 'verified' }] }` |");
1312
+ lines.push("");
1313
+ lines.push("```typescript");
1314
+ lines.push("// OR - match any condition");
1315
+ lines.push("const results = await sdk.users.list({");
1316
+ lines.push(" where: {");
1317
+ lines.push(" $or: [");
1318
+ lines.push(" { email: { $ilike: '%@gmail.com' } },");
1319
+ lines.push(" { status: 'premium' }");
1320
+ lines.push(" ]");
1321
+ lines.push(" }");
1322
+ lines.push("});");
1323
+ lines.push("");
1324
+ lines.push("// Mixed AND + OR (implicit AND at root level)");
1325
+ lines.push("const complex = await sdk.users.list({");
1326
+ lines.push(" where: {");
1327
+ lines.push(" status: 'active', // AND");
1328
+ lines.push(" $or: [");
1329
+ lines.push(" { age: { $lt: 18 } },");
1330
+ lines.push(" { age: { $gt: 65 } }");
1331
+ lines.push(" ]");
1332
+ lines.push(" }");
1333
+ lines.push("});");
1334
+ lines.push("");
1335
+ lines.push("// Nested (2 levels max)");
1336
+ lines.push("const nested = await sdk.users.list({");
1337
+ lines.push(" where: {");
1338
+ lines.push(" $and: [");
1339
+ lines.push(" {");
1340
+ lines.push(" $or: [");
1341
+ lines.push(" { firstName: { $ilike: '%john%' } },");
1342
+ lines.push(" { lastName: { $ilike: '%john%' } }");
1343
+ lines.push(" ]");
1344
+ lines.push(" },");
1345
+ lines.push(" { status: 'active' }");
1346
+ lines.push(" ]");
1347
+ lines.push(" }");
1348
+ lines.push("});");
1349
+ lines.push("```");
1350
+ lines.push("");
1304
1351
  lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1305
1352
  lines.push("");
1306
1353
  lines.push("## Resources");
@@ -1627,7 +1674,14 @@ function buildGraph(model) {
1627
1674
 
1628
1675
  // src/emit-include-spec.ts
1629
1676
  function emitIncludeSpec(graph) {
1630
- let out = `/* Generated. Do not edit. */
1677
+ let out = `/**
1678
+ * AUTO-GENERATED FILE - DO NOT EDIT
1679
+ *
1680
+ * This file was automatically generated by PostgreSDK.
1681
+ * Any manual changes will be overwritten on the next generation.
1682
+ *
1683
+ * To make changes, modify your schema or configuration and regenerate.
1684
+ */
1631
1685
  `;
1632
1686
  const tables = Object.keys(graph);
1633
1687
  for (const table of tables) {
@@ -1656,7 +1710,14 @@ function toPascal(s) {
1656
1710
 
1657
1711
  // src/emit-include-builder.ts
1658
1712
  function emitIncludeBuilder(graph, maxDepth) {
1659
- return `// Generated. Do not edit.
1713
+ return `/**
1714
+ * AUTO-GENERATED FILE - DO NOT EDIT
1715
+ *
1716
+ * This file was automatically generated by PostgreSDK.
1717
+ * Any manual changes will be overwritten on the next generation.
1718
+ *
1719
+ * To make changes, modify your schema or configuration and regenerate.
1720
+ */
1660
1721
  export const RELATION_GRAPH = ${JSON.stringify(graph, null, 2)} as const;
1661
1722
  type TableName = keyof typeof RELATION_GRAPH;
1662
1723
 
@@ -1804,7 +1865,14 @@ function emitHonoRoutes(table, _graph, opts) {
1804
1865
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
1805
1866
  const ext = opts.useJsExtensions ? ".js" : "";
1806
1867
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
1807
- return `/* Generated. Do not edit. */
1868
+ return `/**
1869
+ * AUTO-GENERATED FILE - DO NOT EDIT
1870
+ *
1871
+ * This file was automatically generated by PostgreSDK.
1872
+ * Any manual changes will be overwritten on the next generation.
1873
+ *
1874
+ * To make changes, modify your schema or configuration and regenerate.
1875
+ */
1808
1876
  import { Hono } from "hono";
1809
1877
  import { z } from "zod";
1810
1878
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
@@ -2006,7 +2074,14 @@ function emitClient(table, graph, opts, model) {
2006
2074
  `;
2007
2075
  }
2008
2076
  }
2009
- return `/* Generated. Do not edit. */
2077
+ return `/**
2078
+ * AUTO-GENERATED FILE - DO NOT EDIT
2079
+ *
2080
+ * This file was automatically generated by PostgreSDK.
2081
+ * Any manual changes will be overwritten on the next generation.
2082
+ *
2083
+ * To make changes, modify your schema or configuration and regenerate.
2084
+ */
2010
2085
  import { BaseClient } from "./base-client${ext}";
2011
2086
  import type { Where } from "./where-types${ext}";
2012
2087
  ${typeImports}
@@ -2053,7 +2128,14 @@ ${includeMethodsCode}}
2053
2128
  }
2054
2129
  function emitClientIndex(tables, useJsExtensions) {
2055
2130
  const ext = useJsExtensions ? ".js" : "";
2056
- let out = `/* Generated. Do not edit. */
2131
+ let out = `/**
2132
+ * AUTO-GENERATED FILE - DO NOT EDIT
2133
+ *
2134
+ * This file was automatically generated by PostgreSDK.
2135
+ * Any manual changes will be overwritten on the next generation.
2136
+ *
2137
+ * To make changes, modify your schema or configuration and regenerate.
2138
+ */
2057
2139
  `;
2058
2140
  out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
2059
2141
  `;
@@ -2126,7 +2208,14 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
2126
2208
 
2127
2209
  // src/emit-base-client.ts
2128
2210
  function emitBaseClient() {
2129
- return `/* Generated. Do not edit. */
2211
+ return `/**
2212
+ * AUTO-GENERATED FILE - DO NOT EDIT
2213
+ *
2214
+ * This file was automatically generated by PostgreSDK.
2215
+ * Any manual changes will be overwritten on the next generation.
2216
+ *
2217
+ * To make changes, modify your schema or configuration and regenerate.
2218
+ */
2130
2219
 
2131
2220
  export type HeaderMap = Record<string, string>;
2132
2221
  export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
@@ -2273,7 +2362,14 @@ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
2273
2362
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
2274
2363
  }
2275
2364
  const ext = useJsExtensions ? ".js" : "";
2276
- return `/* Generated. Do not edit. */
2365
+ return `/**
2366
+ * AUTO-GENERATED FILE - DO NOT EDIT
2367
+ *
2368
+ * This file was automatically generated by PostgreSDK.
2369
+ * Any manual changes will be overwritten on the next generation.
2370
+ *
2371
+ * To make changes, modify your schema or configuration and regenerate.
2372
+ */
2277
2373
  import { RELATION_GRAPH } from "./include-builder${ext}";
2278
2374
 
2279
2375
  // Minimal types to keep the file self-contained
@@ -2594,7 +2690,14 @@ function emitTypes(table, opts) {
2594
2690
  return ` ${col.name}: ${valueType};`;
2595
2691
  }).join(`
2596
2692
  `);
2597
- return `/* Generated. Do not edit. */
2693
+ return `/**
2694
+ * AUTO-GENERATED FILE - DO NOT EDIT
2695
+ *
2696
+ * This file was automatically generated by PostgreSDK.
2697
+ * Any manual changes will be overwritten on the next generation.
2698
+ *
2699
+ * To make changes, modify your schema or configuration and regenerate.
2700
+ */
2598
2701
  export type Insert${Type} = {
2599
2702
  ${insertFields}
2600
2703
  };
@@ -2609,7 +2712,14 @@ ${selectFields}
2609
2712
 
2610
2713
  // src/emit-logger.ts
2611
2714
  function emitLogger() {
2612
- return `/* Generated. Do not edit. */
2715
+ return `/**
2716
+ * AUTO-GENERATED FILE - DO NOT EDIT
2717
+ *
2718
+ * This file was automatically generated by PostgreSDK.
2719
+ * Any manual changes will be overwritten on the next generation.
2720
+ *
2721
+ * To make changes, modify your schema or configuration and regenerate.
2722
+ */
2613
2723
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
2614
2724
 
2615
2725
  export const logger = {
@@ -2644,7 +2754,14 @@ export function safe<T extends (c: any) => any>(handler: T) {
2644
2754
 
2645
2755
  // src/emit-where-types.ts
2646
2756
  function emitWhereTypes() {
2647
- return `/* Generated. Do not edit. */
2757
+ return `/**
2758
+ * AUTO-GENERATED FILE - DO NOT EDIT
2759
+ *
2760
+ * This file was automatically generated by PostgreSDK.
2761
+ * Any manual changes will be overwritten on the next generation.
2762
+ *
2763
+ * To make changes, modify your schema or configuration and regenerate.
2764
+ */
2648
2765
 
2649
2766
  /**
2650
2767
  * WHERE clause operators for filtering
@@ -2682,11 +2799,26 @@ export type WhereOperator<T> = {
2682
2799
  export type WhereCondition<T> = T | WhereOperator<T>;
2683
2800
 
2684
2801
  /**
2685
- * WHERE clause type for a given table type
2802
+ * Field-level WHERE conditions (without logical operators)
2686
2803
  */
2687
- export type Where<T> = {
2804
+ export type WhereFieldConditions<T> = {
2688
2805
  [K in keyof T]?: WhereCondition<T[K]>;
2689
2806
  };
2807
+
2808
+ /**
2809
+ * WHERE clause type with support for $or/$and logical operators (2 levels max)
2810
+ *
2811
+ * Examples:
2812
+ * - Basic OR: { $or: [{ name: 'Alice' }, { name: 'Bob' }] }
2813
+ * - Mixed AND + OR: { status: 'active', $or: [{ age: { $gt: 65 } }, { age: { $lt: 18 } }] }
2814
+ * - Nested (2 levels): { $and: [{ $or: [{ name: 'Alice' }, { name: 'Bob' }] }, { status: 'active' }] }
2815
+ */
2816
+ export type Where<T> = WhereFieldConditions<T> & {
2817
+ /** OR - at least one condition must be true */
2818
+ $or?: (WhereFieldConditions<T>)[];
2819
+ /** AND - all conditions must be true (alternative to implicit root-level AND) */
2820
+ $and?: (WhereFieldConditions<T> | { $or?: WhereFieldConditions<T>[] })[];
2821
+ };
2690
2822
  `;
2691
2823
  }
2692
2824
 
@@ -2704,7 +2836,14 @@ function emitAuth(cfgAuth) {
2704
2836
  const JWT_SHARED_SECRET = JSON.stringify(jwtShared);
2705
2837
  const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
2706
2838
  const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
2707
- return `/* Generated. Do not edit. */
2839
+ return `/**
2840
+ * AUTO-GENERATED FILE - DO NOT EDIT
2841
+ *
2842
+ * This file was automatically generated by PostgreSDK.
2843
+ * Any manual changes will be overwritten on the next generation.
2844
+ *
2845
+ * To make changes, modify your schema or configuration and regenerate.
2846
+ */
2708
2847
  import type { Context, Next } from "hono";
2709
2848
 
2710
2849
  // ---- Config inlined by generator ----
@@ -2861,7 +3000,14 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
2861
3000
  return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
2862
3001
  }).join(`
2863
3002
  `);
2864
- return `/* Generated. Do not edit. */
3003
+ return `/**
3004
+ * AUTO-GENERATED FILE - DO NOT EDIT
3005
+ *
3006
+ * This file was automatically generated by PostgreSDK.
3007
+ * Any manual changes will be overwritten on the next generation.
3008
+ *
3009
+ * To make changes, modify your schema or configuration and regenerate.
3010
+ */
2865
3011
  import { Hono } from "hono";
2866
3012
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
2867
3013
  import { getContract } from "./contract${ext}";
@@ -2995,7 +3141,14 @@ function emitSdkBundle(clientFiles, clientDir) {
2995
3141
  }
2996
3142
  const version = `1.0.0`;
2997
3143
  const generated = new Date().toISOString();
2998
- return `/* Generated. Do not edit. */
3144
+ return `/**
3145
+ * AUTO-GENERATED FILE - DO NOT EDIT
3146
+ *
3147
+ * This file was automatically generated by PostgreSDK.
3148
+ * Any manual changes will be overwritten on the next generation.
3149
+ *
3150
+ * To make changes, modify your schema or configuration and regenerate.
3151
+ */
2999
3152
 
3000
3153
  export const SDK_MANIFEST = {
3001
3154
  version: "${version}",
@@ -3099,6 +3252,155 @@ export async function getByPk(
3099
3252
  }
3100
3253
  }
3101
3254
 
3255
+ /**
3256
+ * Build WHERE clause recursively, supporting $or/$and operators
3257
+ * Returns { sql: string, params: any[], nextParamIndex: number }
3258
+ */
3259
+ function buildWhereClause(
3260
+ whereClause: any,
3261
+ startParamIndex: number
3262
+ ): { sql: string; params: any[]; nextParamIndex: number } {
3263
+ const whereParts: string[] = [];
3264
+ const whereParams: any[] = [];
3265
+ let paramIndex = startParamIndex;
3266
+
3267
+ if (!whereClause || typeof whereClause !== 'object') {
3268
+ return { sql: '', params: [], nextParamIndex: paramIndex };
3269
+ }
3270
+
3271
+ // Separate logical operators from field conditions
3272
+ const { $or, $and, ...fieldConditions } = whereClause;
3273
+
3274
+ // Process field-level conditions
3275
+ for (const [key, value] of Object.entries(fieldConditions)) {
3276
+ if (value === undefined) {
3277
+ continue;
3278
+ }
3279
+
3280
+ // Handle operator objects like { $gt: 5, $like: "%test%" }
3281
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3282
+ for (const [op, opValue] of Object.entries(value)) {
3283
+ if (opValue === undefined) continue;
3284
+
3285
+ switch (op) {
3286
+ case '$eq':
3287
+ whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3288
+ whereParams.push(opValue);
3289
+ paramIndex++;
3290
+ break;
3291
+ case '$ne':
3292
+ whereParts.push(\`"\${key}" != $\${paramIndex}\`);
3293
+ whereParams.push(opValue);
3294
+ paramIndex++;
3295
+ break;
3296
+ case '$gt':
3297
+ whereParts.push(\`"\${key}" > $\${paramIndex}\`);
3298
+ whereParams.push(opValue);
3299
+ paramIndex++;
3300
+ break;
3301
+ case '$gte':
3302
+ whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
3303
+ whereParams.push(opValue);
3304
+ paramIndex++;
3305
+ break;
3306
+ case '$lt':
3307
+ whereParts.push(\`"\${key}" < $\${paramIndex}\`);
3308
+ whereParams.push(opValue);
3309
+ paramIndex++;
3310
+ break;
3311
+ case '$lte':
3312
+ whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
3313
+ whereParams.push(opValue);
3314
+ paramIndex++;
3315
+ break;
3316
+ case '$in':
3317
+ if (Array.isArray(opValue) && opValue.length > 0) {
3318
+ whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
3319
+ whereParams.push(opValue);
3320
+ paramIndex++;
3321
+ }
3322
+ break;
3323
+ case '$nin':
3324
+ if (Array.isArray(opValue) && opValue.length > 0) {
3325
+ whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
3326
+ whereParams.push(opValue);
3327
+ paramIndex++;
3328
+ }
3329
+ break;
3330
+ case '$like':
3331
+ whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
3332
+ whereParams.push(opValue);
3333
+ paramIndex++;
3334
+ break;
3335
+ case '$ilike':
3336
+ whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
3337
+ whereParams.push(opValue);
3338
+ paramIndex++;
3339
+ break;
3340
+ case '$is':
3341
+ if (opValue === null) {
3342
+ whereParts.push(\`"\${key}" IS NULL\`);
3343
+ }
3344
+ break;
3345
+ case '$isNot':
3346
+ if (opValue === null) {
3347
+ whereParts.push(\`"\${key}" IS NOT NULL\`);
3348
+ }
3349
+ break;
3350
+ }
3351
+ }
3352
+ } else if (value === null) {
3353
+ // Direct null value
3354
+ whereParts.push(\`"\${key}" IS NULL\`);
3355
+ } else {
3356
+ // Direct value (simple equality)
3357
+ whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3358
+ whereParams.push(value);
3359
+ paramIndex++;
3360
+ }
3361
+ }
3362
+
3363
+ // Handle $or operator
3364
+ if ($or && Array.isArray($or)) {
3365
+ if ($or.length === 0) {
3366
+ // Empty OR is logically FALSE - matches nothing
3367
+ whereParts.push('FALSE');
3368
+ } else {
3369
+ const orParts: string[] = [];
3370
+ for (const orCondition of $or) {
3371
+ const result = buildWhereClause(orCondition, paramIndex);
3372
+ if (result.sql) {
3373
+ orParts.push(result.sql);
3374
+ whereParams.push(...result.params);
3375
+ paramIndex = result.nextParamIndex;
3376
+ }
3377
+ }
3378
+ if (orParts.length > 0) {
3379
+ whereParts.push(\`(\${orParts.join(' OR ')})\`);
3380
+ }
3381
+ }
3382
+ }
3383
+
3384
+ // Handle $and operator
3385
+ if ($and && Array.isArray($and) && $and.length > 0) {
3386
+ const andParts: string[] = [];
3387
+ for (const andCondition of $and) {
3388
+ const result = buildWhereClause(andCondition, paramIndex);
3389
+ if (result.sql) {
3390
+ andParts.push(result.sql);
3391
+ whereParams.push(...result.params);
3392
+ paramIndex = result.nextParamIndex;
3393
+ }
3394
+ }
3395
+ if (andParts.length > 0) {
3396
+ whereParts.push(\`(\${andParts.join(' AND ')})\`);
3397
+ }
3398
+ }
3399
+
3400
+ const sql = whereParts.join(' AND ');
3401
+ return { sql, params: whereParams, nextParamIndex: paramIndex };
3402
+ }
3403
+
3102
3404
  /**
3103
3405
  * LIST operation - Get multiple records with optional filters
3104
3406
  */
@@ -3108,136 +3410,54 @@ export async function listRecords(
3108
3410
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3109
3411
  try {
3110
3412
  const { where: whereClause, limit = 50, offset = 0, include } = params;
3111
-
3413
+
3112
3414
  // Build WHERE clause
3113
- const whereParts: string[] = [];
3114
- const whereParams: any[] = [];
3115
3415
  let paramIndex = 1;
3116
-
3416
+ const whereParts: string[] = [];
3417
+ let whereParams: any[] = [];
3418
+
3117
3419
  // Add soft delete filter if applicable
3118
3420
  if (ctx.softDeleteColumn) {
3119
3421
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
3120
3422
  }
3121
-
3122
- // Add user-provided where conditions
3123
- if (whereClause && typeof whereClause === 'object') {
3124
- for (const [key, value] of Object.entries(whereClause)) {
3125
- if (value === undefined) {
3126
- // Skip undefined values
3127
- continue;
3128
- }
3129
3423
 
3130
- // Handle operator objects like { $gt: 5, $like: "%test%" }
3131
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3132
- for (const [op, opValue] of Object.entries(value)) {
3133
- if (opValue === undefined) continue;
3134
-
3135
- switch (op) {
3136
- case '$eq':
3137
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3138
- whereParams.push(opValue);
3139
- paramIndex++;
3140
- break;
3141
- case '$ne':
3142
- whereParts.push(\`"\${key}" != $\${paramIndex}\`);
3143
- whereParams.push(opValue);
3144
- paramIndex++;
3145
- break;
3146
- case '$gt':
3147
- whereParts.push(\`"\${key}" > $\${paramIndex}\`);
3148
- whereParams.push(opValue);
3149
- paramIndex++;
3150
- break;
3151
- case '$gte':
3152
- whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
3153
- whereParams.push(opValue);
3154
- paramIndex++;
3155
- break;
3156
- case '$lt':
3157
- whereParts.push(\`"\${key}" < $\${paramIndex}\`);
3158
- whereParams.push(opValue);
3159
- paramIndex++;
3160
- break;
3161
- case '$lte':
3162
- whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
3163
- whereParams.push(opValue);
3164
- paramIndex++;
3165
- break;
3166
- case '$in':
3167
- if (Array.isArray(opValue) && opValue.length > 0) {
3168
- whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
3169
- whereParams.push(opValue);
3170
- paramIndex++;
3171
- }
3172
- break;
3173
- case '$nin':
3174
- if (Array.isArray(opValue) && opValue.length > 0) {
3175
- whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
3176
- whereParams.push(opValue);
3177
- paramIndex++;
3178
- }
3179
- break;
3180
- case '$like':
3181
- whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
3182
- whereParams.push(opValue);
3183
- paramIndex++;
3184
- break;
3185
- case '$ilike':
3186
- whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
3187
- whereParams.push(opValue);
3188
- paramIndex++;
3189
- break;
3190
- case '$is':
3191
- if (opValue === null) {
3192
- whereParts.push(\`"\${key}" IS NULL\`);
3193
- }
3194
- break;
3195
- case '$isNot':
3196
- if (opValue === null) {
3197
- whereParts.push(\`"\${key}" IS NOT NULL\`);
3198
- }
3199
- break;
3200
- }
3201
- }
3202
- } else if (value === null) {
3203
- // Direct null value
3204
- whereParts.push(\`"\${key}" IS NULL\`);
3205
- } else {
3206
- // Direct value (simple equality)
3207
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3208
- whereParams.push(value);
3209
- paramIndex++;
3210
- }
3424
+ // Add user-provided where conditions
3425
+ if (whereClause) {
3426
+ const result = buildWhereClause(whereClause, paramIndex);
3427
+ if (result.sql) {
3428
+ whereParts.push(result.sql);
3429
+ whereParams = result.params;
3430
+ paramIndex = result.nextParamIndex;
3211
3431
  }
3212
3432
  }
3213
-
3433
+
3214
3434
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
3215
-
3435
+
3216
3436
  // Add limit and offset params
3217
3437
  const limitParam = \`$\${paramIndex}\`;
3218
3438
  const offsetParam = \`$\${paramIndex + 1}\`;
3219
3439
  const allParams = [...whereParams, limit, offset];
3220
-
3440
+
3221
3441
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3222
3442
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3223
-
3443
+
3224
3444
  const { rows } = await ctx.pg.query(text, allParams);
3225
-
3445
+
3226
3446
  if (!include) {
3227
3447
  log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
3228
3448
  return { data: rows, status: 200 };
3229
3449
  }
3230
-
3450
+
3231
3451
  // Include logic will be handled by the include-loader
3232
3452
  // For now, just return the rows with a note that includes need to be applied
3233
3453
  log.debug(\`LIST \${ctx.table} include spec:\`, include);
3234
3454
  return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
3235
3455
  } catch (e: any) {
3236
3456
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
3237
- return {
3238
- error: e?.message ?? "Internal error",
3457
+ return {
3458
+ error: e?.message ?? "Internal error",
3239
3459
  ...(DEBUG ? { stack: e?.stack } : {}),
3240
- status: 500
3460
+ status: 500
3241
3461
  };
3242
3462
  }
3243
3463
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "scripts": {
24
24
  "build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
25
- "test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
25
+ "test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
26
26
  "test:init": "bun test/test-init.ts",
27
27
  "test:gen": "bun test/test-gen.ts",
28
28
  "test:gen-with-tests": "bun test/test-gen-with-tests.ts",
@@ -53,7 +53,16 @@
53
53
  "typescript": "^5.5.0",
54
54
  "vitest": "^3.2.4"
55
55
  },
56
+ "author": "Ben Honda <ben@theadpharm.com>",
56
57
  "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/adpharm/postgresdk.git"
61
+ },
62
+ "homepage": "https://github.com/adpharm/postgresdk#readme",
63
+ "bugs": {
64
+ "url": "https://github.com/adpharm/postgresdk/issues"
65
+ },
57
66
  "keywords": [
58
67
  "postgres",
59
68
  "sdk",