postgresdk 0.9.9 → 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.
package/dist/index.js CHANGED
@@ -1169,6 +1169,187 @@ function generateUnifiedContractMarkdown(contract) {
1169
1169
  lines.push("");
1170
1170
  }
1171
1171
  }
1172
+ lines.push("## Filtering with WHERE Clauses");
1173
+ lines.push("");
1174
+ lines.push("The SDK provides type-safe WHERE clause filtering with support for various operators.");
1175
+ lines.push("");
1176
+ lines.push("### Basic Filtering");
1177
+ lines.push("");
1178
+ lines.push("**Direct equality:**");
1179
+ lines.push("");
1180
+ lines.push("```typescript");
1181
+ lines.push("// Find users with specific email");
1182
+ lines.push("const users = await sdk.users.list({");
1183
+ lines.push(" where: { email: 'user@example.com' }");
1184
+ lines.push("});");
1185
+ lines.push("");
1186
+ lines.push("// Multiple conditions (AND)");
1187
+ lines.push("const activeUsers = await sdk.users.list({");
1188
+ lines.push(" where: {");
1189
+ lines.push(" status: 'active',");
1190
+ lines.push(" role: 'admin'");
1191
+ lines.push(" }");
1192
+ lines.push("});");
1193
+ lines.push("```");
1194
+ lines.push("");
1195
+ lines.push("### Comparison Operators");
1196
+ lines.push("");
1197
+ lines.push("Use comparison operators for numeric, date, and other comparable fields:");
1198
+ lines.push("");
1199
+ lines.push("```typescript");
1200
+ lines.push("// Greater than / Less than");
1201
+ lines.push("const adults = await sdk.users.list({");
1202
+ lines.push(" where: { age: { $gt: 18 } }");
1203
+ lines.push("});");
1204
+ lines.push("");
1205
+ lines.push("// Range queries");
1206
+ lines.push("const workingAge = await sdk.users.list({");
1207
+ lines.push(" where: {");
1208
+ lines.push(" age: { $gte: 18, $lte: 65 }");
1209
+ lines.push(" }");
1210
+ lines.push("});");
1211
+ lines.push("");
1212
+ lines.push("// Not equal");
1213
+ lines.push("const notPending = await sdk.orders.list({");
1214
+ lines.push(" where: { status: { $ne: 'pending' } }");
1215
+ lines.push("});");
1216
+ lines.push("```");
1217
+ lines.push("");
1218
+ lines.push("### String Operators");
1219
+ lines.push("");
1220
+ lines.push("Pattern matching for string fields:");
1221
+ lines.push("");
1222
+ lines.push("```typescript");
1223
+ lines.push("// Case-sensitive LIKE");
1224
+ lines.push("const johnsmiths = await sdk.users.list({");
1225
+ lines.push(" where: { name: { $like: '%Smith%' } }");
1226
+ lines.push("});");
1227
+ lines.push("");
1228
+ lines.push("// Case-insensitive ILIKE");
1229
+ lines.push("const gmailUsers = await sdk.users.list({");
1230
+ lines.push(" where: { email: { $ilike: '%@gmail.com' } }");
1231
+ lines.push("});");
1232
+ lines.push("```");
1233
+ lines.push("");
1234
+ lines.push("### Array Operators");
1235
+ lines.push("");
1236
+ lines.push("Filter by multiple possible values:");
1237
+ lines.push("");
1238
+ lines.push("```typescript");
1239
+ lines.push("// IN - match any value in array");
1240
+ lines.push("const specificUsers = await sdk.users.list({");
1241
+ lines.push(" where: {");
1242
+ lines.push(" id: { $in: ['id1', 'id2', 'id3'] }");
1243
+ lines.push(" }");
1244
+ lines.push("});");
1245
+ lines.push("");
1246
+ lines.push("// NOT IN - exclude values");
1247
+ lines.push("const nonSystemUsers = await sdk.users.list({");
1248
+ lines.push(" where: {");
1249
+ lines.push(" role: { $nin: ['admin', 'system'] }");
1250
+ lines.push(" }");
1251
+ lines.push("});");
1252
+ lines.push("```");
1253
+ lines.push("");
1254
+ lines.push("### NULL Checks");
1255
+ lines.push("");
1256
+ lines.push("Check for null or non-null values:");
1257
+ lines.push("");
1258
+ lines.push("```typescript");
1259
+ lines.push("// IS NULL");
1260
+ lines.push("const activeRecords = await sdk.records.list({");
1261
+ lines.push(" where: { deleted_at: { $is: null } }");
1262
+ lines.push("});");
1263
+ lines.push("");
1264
+ lines.push("// IS NOT NULL");
1265
+ lines.push("const deletedRecords = await sdk.records.list({");
1266
+ lines.push(" where: { deleted_at: { $isNot: null } }");
1267
+ lines.push("});");
1268
+ lines.push("```");
1269
+ lines.push("");
1270
+ lines.push("### Combining Operators");
1271
+ lines.push("");
1272
+ lines.push("Mix multiple operators for complex queries:");
1273
+ lines.push("");
1274
+ lines.push("```typescript");
1275
+ lines.push("const filteredUsers = await sdk.users.list({");
1276
+ lines.push(" where: {");
1277
+ lines.push(" age: { $gte: 18, $lt: 65 },");
1278
+ lines.push(" email: { $ilike: '%@company.com' },");
1279
+ lines.push(" status: { $in: ['active', 'pending'] },");
1280
+ lines.push(" deleted_at: { $is: null }");
1281
+ lines.push(" },");
1282
+ lines.push(" limit: 50,");
1283
+ lines.push(" offset: 0");
1284
+ lines.push("});");
1285
+ lines.push("```");
1286
+ lines.push("");
1287
+ lines.push("### Available Operators");
1288
+ lines.push("");
1289
+ lines.push("| Operator | Description | Example | Types |");
1290
+ lines.push("|----------|-------------|---------|-------|");
1291
+ lines.push("| `$eq` | Equal to | `{ age: { $eq: 25 } }` | All |");
1292
+ lines.push("| `$ne` | Not equal to | `{ status: { $ne: 'inactive' } }` | All |");
1293
+ lines.push("| `$gt` | Greater than | `{ price: { $gt: 100 } }` | Number, Date |");
1294
+ lines.push("| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | Number, Date |");
1295
+ lines.push("| `$lt` | Less than | `{ quantity: { $lt: 10 } }` | Number, Date |");
1296
+ lines.push("| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | Number, Date |");
1297
+ lines.push("| `$in` | In array | `{ id: { $in: ['a', 'b'] } }` | All |");
1298
+ lines.push("| `$nin` | Not in array | `{ role: { $nin: ['admin'] } }` | All |");
1299
+ lines.push("| `$like` | Pattern match (case-sensitive) | `{ name: { $like: '%John%' } }` | String |");
1300
+ lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
1301
+ lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1302
+ lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
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("");
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
+ lines.push("");
1172
1353
  lines.push("## Resources");
1173
1354
  lines.push("");
1174
1355
  for (const resource of contract.resources) {
@@ -1493,7 +1674,14 @@ function buildGraph(model) {
1493
1674
 
1494
1675
  // src/emit-include-spec.ts
1495
1676
  function emitIncludeSpec(graph) {
1496
- 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
+ */
1497
1685
  `;
1498
1686
  const tables = Object.keys(graph);
1499
1687
  for (const table of tables) {
@@ -1522,7 +1710,14 @@ function toPascal(s) {
1522
1710
 
1523
1711
  // src/emit-include-builder.ts
1524
1712
  function emitIncludeBuilder(graph, maxDepth) {
1525
- 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
+ */
1526
1721
  export const RELATION_GRAPH = ${JSON.stringify(graph, null, 2)} as const;
1527
1722
  type TableName = keyof typeof RELATION_GRAPH;
1528
1723
 
@@ -1670,7 +1865,14 @@ function emitHonoRoutes(table, _graph, opts) {
1670
1865
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
1671
1866
  const ext = opts.useJsExtensions ? ".js" : "";
1672
1867
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
1673
- 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
+ */
1674
1876
  import { Hono } from "hono";
1675
1877
  import { z } from "zod";
1676
1878
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
@@ -1850,7 +2052,7 @@ function emitClient(table, graph, opts, model) {
1850
2052
  let includeMethodsCode = "";
1851
2053
  for (const method of includeMethods) {
1852
2054
  const isGetByPk = method.name.startsWith("getByPk");
1853
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: any; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2055
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
1854
2056
  if (isGetByPk) {
1855
2057
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
1856
2058
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -1872,8 +2074,16 @@ function emitClient(table, graph, opts, model) {
1872
2074
  `;
1873
2075
  }
1874
2076
  }
1875
- 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
+ */
1876
2085
  import { BaseClient } from "./base-client${ext}";
2086
+ import type { Where } from "./where-types${ext}";
1877
2087
  ${typeImports}
1878
2088
  ${otherTableImports.join(`
1879
2089
  `)}
@@ -1893,10 +2103,11 @@ export class ${Type}Client extends BaseClient {
1893
2103
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
1894
2104
  }
1895
2105
 
1896
- async list(params?: {
1897
- limit?: number;
2106
+ async list(params?: {
2107
+ include?: any;
2108
+ limit?: number;
1898
2109
  offset?: number;
1899
- where?: any;
2110
+ where?: Where<Select${Type}>;
1900
2111
  orderBy?: string;
1901
2112
  order?: "asc" | "desc";
1902
2113
  }): Promise<Select${Type}[]> {
@@ -1917,7 +2128,14 @@ ${includeMethodsCode}}
1917
2128
  }
1918
2129
  function emitClientIndex(tables, useJsExtensions) {
1919
2130
  const ext = useJsExtensions ? ".js" : "";
1920
- 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
+ */
1921
2139
  `;
1922
2140
  out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
1923
2141
  `;
@@ -1958,6 +2176,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
1958
2176
  out += `export { BaseClient } from "./base-client${ext}";
1959
2177
  `;
1960
2178
  out += `
2179
+ // Include specification types for custom queries
2180
+ `;
2181
+ out += `export type {
2182
+ `;
2183
+ for (const t of tables) {
2184
+ out += ` ${pascal(t.name)}IncludeSpec,
2185
+ `;
2186
+ }
2187
+ out += `} from "./include-spec${ext}";
2188
+ `;
2189
+ out += `
1961
2190
  // Zod schemas for form validation
1962
2191
  `;
1963
2192
  for (const t of tables) {
@@ -1979,7 +2208,14 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
1979
2208
 
1980
2209
  // src/emit-base-client.ts
1981
2210
  function emitBaseClient() {
1982
- 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
+ */
1983
2219
 
1984
2220
  export type HeaderMap = Record<string, string>;
1985
2221
  export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
@@ -2126,7 +2362,14 @@ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
2126
2362
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
2127
2363
  }
2128
2364
  const ext = useJsExtensions ? ".js" : "";
2129
- 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
+ */
2130
2373
  import { RELATION_GRAPH } from "./include-builder${ext}";
2131
2374
 
2132
2375
  // Minimal types to keep the file self-contained
@@ -2447,7 +2690,14 @@ function emitTypes(table, opts) {
2447
2690
  return ` ${col.name}: ${valueType};`;
2448
2691
  }).join(`
2449
2692
  `);
2450
- 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
+ */
2451
2701
  export type Insert${Type} = {
2452
2702
  ${insertFields}
2453
2703
  };
@@ -2462,7 +2712,14 @@ ${selectFields}
2462
2712
 
2463
2713
  // src/emit-logger.ts
2464
2714
  function emitLogger() {
2465
- 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
+ */
2466
2723
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
2467
2724
 
2468
2725
  export const logger = {
@@ -2495,6 +2752,76 @@ export function safe<T extends (c: any) => any>(handler: T) {
2495
2752
  `;
2496
2753
  }
2497
2754
 
2755
+ // src/emit-where-types.ts
2756
+ function emitWhereTypes() {
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
+ */
2765
+
2766
+ /**
2767
+ * WHERE clause operators for filtering
2768
+ */
2769
+ export type WhereOperator<T> = {
2770
+ /** Equal to */
2771
+ $eq?: T;
2772
+ /** Not equal to */
2773
+ $ne?: T;
2774
+ /** Greater than */
2775
+ $gt?: T;
2776
+ /** Greater than or equal to */
2777
+ $gte?: T;
2778
+ /** Less than */
2779
+ $lt?: T;
2780
+ /** Less than or equal to */
2781
+ $lte?: T;
2782
+ /** In array */
2783
+ $in?: T[];
2784
+ /** Not in array */
2785
+ $nin?: T[];
2786
+ /** LIKE pattern match (strings only) */
2787
+ $like?: T extends string ? string : never;
2788
+ /** Case-insensitive LIKE (strings only) */
2789
+ $ilike?: T extends string ? string : never;
2790
+ /** IS NULL */
2791
+ $is?: null;
2792
+ /** IS NOT NULL */
2793
+ $isNot?: null;
2794
+ };
2795
+
2796
+ /**
2797
+ * WHERE condition - can be a direct value or an operator object
2798
+ */
2799
+ export type WhereCondition<T> = T | WhereOperator<T>;
2800
+
2801
+ /**
2802
+ * Field-level WHERE conditions (without logical operators)
2803
+ */
2804
+ export type WhereFieldConditions<T> = {
2805
+ [K in keyof T]?: WhereCondition<T[K]>;
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
+ };
2822
+ `;
2823
+ }
2824
+
2498
2825
  // src/emit-auth.ts
2499
2826
  function emitAuth(cfgAuth) {
2500
2827
  const strategy = cfgAuth?.strategy ?? "none";
@@ -2509,7 +2836,14 @@ function emitAuth(cfgAuth) {
2509
2836
  const JWT_SHARED_SECRET = JSON.stringify(jwtShared);
2510
2837
  const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
2511
2838
  const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
2512
- 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
+ */
2513
2847
  import type { Context, Next } from "hono";
2514
2848
 
2515
2849
  // ---- Config inlined by generator ----
@@ -2666,7 +3000,14 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
2666
3000
  return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
2667
3001
  }).join(`
2668
3002
  `);
2669
- 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
+ */
2670
3011
  import { Hono } from "hono";
2671
3012
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
2672
3013
  import { getContract } from "./contract${ext}";
@@ -2800,7 +3141,14 @@ function emitSdkBundle(clientFiles, clientDir) {
2800
3141
  }
2801
3142
  const version = `1.0.0`;
2802
3143
  const generated = new Date().toISOString();
2803
- 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
+ */
2804
3152
 
2805
3153
  export const SDK_MANIFEST = {
2806
3154
  version: "${version}",
@@ -2904,6 +3252,155 @@ export async function getByPk(
2904
3252
  }
2905
3253
  }
2906
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
+
2907
3404
  /**
2908
3405
  * LIST operation - Get multiple records with optional filters
2909
3406
  */
@@ -2913,60 +3410,54 @@ export async function listRecords(
2913
3410
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
2914
3411
  try {
2915
3412
  const { where: whereClause, limit = 50, offset = 0, include } = params;
2916
-
3413
+
2917
3414
  // Build WHERE clause
2918
- const whereParts: string[] = [];
2919
- const whereParams: any[] = [];
2920
3415
  let paramIndex = 1;
2921
-
3416
+ const whereParts: string[] = [];
3417
+ let whereParams: any[] = [];
3418
+
2922
3419
  // Add soft delete filter if applicable
2923
3420
  if (ctx.softDeleteColumn) {
2924
3421
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
2925
3422
  }
2926
-
3423
+
2927
3424
  // Add user-provided where conditions
2928
- if (whereClause && typeof whereClause === 'object') {
2929
- for (const [key, value] of Object.entries(whereClause)) {
2930
- if (value === null) {
2931
- whereParts.push(\`"\${key}" IS NULL\`);
2932
- } else if (value === undefined) {
2933
- // Skip undefined values
2934
- continue;
2935
- } else {
2936
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
2937
- whereParams.push(value);
2938
- paramIndex++;
2939
- }
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;
2940
3431
  }
2941
3432
  }
2942
-
3433
+
2943
3434
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
2944
-
3435
+
2945
3436
  // Add limit and offset params
2946
3437
  const limitParam = \`$\${paramIndex}\`;
2947
3438
  const offsetParam = \`$\${paramIndex + 1}\`;
2948
3439
  const allParams = [...whereParams, limit, offset];
2949
-
3440
+
2950
3441
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
2951
3442
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
2952
-
3443
+
2953
3444
  const { rows } = await ctx.pg.query(text, allParams);
2954
-
3445
+
2955
3446
  if (!include) {
2956
3447
  log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
2957
3448
  return { data: rows, status: 200 };
2958
3449
  }
2959
-
3450
+
2960
3451
  // Include logic will be handled by the include-loader
2961
3452
  // For now, just return the rows with a note that includes need to be applied
2962
3453
  log.debug(\`LIST \${ctx.table} include spec:\`, include);
2963
3454
  return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
2964
3455
  } catch (e: any) {
2965
3456
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
2966
- return {
2967
- error: e?.message ?? "Internal error",
3457
+ return {
3458
+ error: e?.message ?? "Internal error",
2968
3459
  ...(DEBUG ? { stack: e?.stack } : {}),
2969
- status: 500
3460
+ status: 500
2970
3461
  };
2971
3462
  }
2972
3463
  }
@@ -3868,6 +4359,7 @@ async function generate(configPath) {
3868
4359
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
3869
4360
  files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
3870
4361
  files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
4362
+ files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
3871
4363
  files.push({
3872
4364
  path: join(serverDir, "include-builder.ts"),
3873
4365
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)