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/cli.js CHANGED
@@ -1170,6 +1170,187 @@ function generateUnifiedContractMarkdown(contract) {
1170
1170
  lines.push("");
1171
1171
  }
1172
1172
  }
1173
+ lines.push("## Filtering with WHERE Clauses");
1174
+ lines.push("");
1175
+ lines.push("The SDK provides type-safe WHERE clause filtering with support for various operators.");
1176
+ lines.push("");
1177
+ lines.push("### Basic Filtering");
1178
+ lines.push("");
1179
+ lines.push("**Direct equality:**");
1180
+ lines.push("");
1181
+ lines.push("```typescript");
1182
+ lines.push("// Find users with specific email");
1183
+ lines.push("const users = await sdk.users.list({");
1184
+ lines.push(" where: { email: 'user@example.com' }");
1185
+ lines.push("});");
1186
+ lines.push("");
1187
+ lines.push("// Multiple conditions (AND)");
1188
+ lines.push("const activeUsers = await sdk.users.list({");
1189
+ lines.push(" where: {");
1190
+ lines.push(" status: 'active',");
1191
+ lines.push(" role: 'admin'");
1192
+ lines.push(" }");
1193
+ lines.push("});");
1194
+ lines.push("```");
1195
+ lines.push("");
1196
+ lines.push("### Comparison Operators");
1197
+ lines.push("");
1198
+ lines.push("Use comparison operators for numeric, date, and other comparable fields:");
1199
+ lines.push("");
1200
+ lines.push("```typescript");
1201
+ lines.push("// Greater than / Less than");
1202
+ lines.push("const adults = await sdk.users.list({");
1203
+ lines.push(" where: { age: { $gt: 18 } }");
1204
+ lines.push("});");
1205
+ lines.push("");
1206
+ lines.push("// Range queries");
1207
+ lines.push("const workingAge = await sdk.users.list({");
1208
+ lines.push(" where: {");
1209
+ lines.push(" age: { $gte: 18, $lte: 65 }");
1210
+ lines.push(" }");
1211
+ lines.push("});");
1212
+ lines.push("");
1213
+ lines.push("// Not equal");
1214
+ lines.push("const notPending = await sdk.orders.list({");
1215
+ lines.push(" where: { status: { $ne: 'pending' } }");
1216
+ lines.push("});");
1217
+ lines.push("```");
1218
+ lines.push("");
1219
+ lines.push("### String Operators");
1220
+ lines.push("");
1221
+ lines.push("Pattern matching for string fields:");
1222
+ lines.push("");
1223
+ lines.push("```typescript");
1224
+ lines.push("// Case-sensitive LIKE");
1225
+ lines.push("const johnsmiths = await sdk.users.list({");
1226
+ lines.push(" where: { name: { $like: '%Smith%' } }");
1227
+ lines.push("});");
1228
+ lines.push("");
1229
+ lines.push("// Case-insensitive ILIKE");
1230
+ lines.push("const gmailUsers = await sdk.users.list({");
1231
+ lines.push(" where: { email: { $ilike: '%@gmail.com' } }");
1232
+ lines.push("});");
1233
+ lines.push("```");
1234
+ lines.push("");
1235
+ lines.push("### Array Operators");
1236
+ lines.push("");
1237
+ lines.push("Filter by multiple possible values:");
1238
+ lines.push("");
1239
+ lines.push("```typescript");
1240
+ lines.push("// IN - match any value in array");
1241
+ lines.push("const specificUsers = await sdk.users.list({");
1242
+ lines.push(" where: {");
1243
+ lines.push(" id: { $in: ['id1', 'id2', 'id3'] }");
1244
+ lines.push(" }");
1245
+ lines.push("});");
1246
+ lines.push("");
1247
+ lines.push("// NOT IN - exclude values");
1248
+ lines.push("const nonSystemUsers = await sdk.users.list({");
1249
+ lines.push(" where: {");
1250
+ lines.push(" role: { $nin: ['admin', 'system'] }");
1251
+ lines.push(" }");
1252
+ lines.push("});");
1253
+ lines.push("```");
1254
+ lines.push("");
1255
+ lines.push("### NULL Checks");
1256
+ lines.push("");
1257
+ lines.push("Check for null or non-null values:");
1258
+ lines.push("");
1259
+ lines.push("```typescript");
1260
+ lines.push("// IS NULL");
1261
+ lines.push("const activeRecords = await sdk.records.list({");
1262
+ lines.push(" where: { deleted_at: { $is: null } }");
1263
+ lines.push("});");
1264
+ lines.push("");
1265
+ lines.push("// IS NOT NULL");
1266
+ lines.push("const deletedRecords = await sdk.records.list({");
1267
+ lines.push(" where: { deleted_at: { $isNot: null } }");
1268
+ lines.push("});");
1269
+ lines.push("```");
1270
+ lines.push("");
1271
+ lines.push("### Combining Operators");
1272
+ lines.push("");
1273
+ lines.push("Mix multiple operators for complex queries:");
1274
+ lines.push("");
1275
+ lines.push("```typescript");
1276
+ lines.push("const filteredUsers = await sdk.users.list({");
1277
+ lines.push(" where: {");
1278
+ lines.push(" age: { $gte: 18, $lt: 65 },");
1279
+ lines.push(" email: { $ilike: '%@company.com' },");
1280
+ lines.push(" status: { $in: ['active', 'pending'] },");
1281
+ lines.push(" deleted_at: { $is: null }");
1282
+ lines.push(" },");
1283
+ lines.push(" limit: 50,");
1284
+ lines.push(" offset: 0");
1285
+ lines.push("});");
1286
+ lines.push("```");
1287
+ lines.push("");
1288
+ lines.push("### Available Operators");
1289
+ lines.push("");
1290
+ lines.push("| Operator | Description | Example | Types |");
1291
+ lines.push("|----------|-------------|---------|-------|");
1292
+ lines.push("| `$eq` | Equal to | `{ age: { $eq: 25 } }` | All |");
1293
+ lines.push("| `$ne` | Not equal to | `{ status: { $ne: 'inactive' } }` | All |");
1294
+ lines.push("| `$gt` | Greater than | `{ price: { $gt: 100 } }` | Number, Date |");
1295
+ lines.push("| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | Number, Date |");
1296
+ lines.push("| `$lt` | Less than | `{ quantity: { $lt: 10 } }` | Number, Date |");
1297
+ lines.push("| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | Number, Date |");
1298
+ lines.push("| `$in` | In array | `{ id: { $in: ['a', 'b'] } }` | All |");
1299
+ lines.push("| `$nin` | Not in array | `{ role: { $nin: ['admin'] } }` | All |");
1300
+ lines.push("| `$like` | Pattern match (case-sensitive) | `{ name: { $like: '%John%' } }` | String |");
1301
+ lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
1302
+ lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1303
+ lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
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("");
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
+ lines.push("");
1173
1354
  lines.push("## Resources");
1174
1355
  lines.push("");
1175
1356
  for (const resource of contract.resources) {
@@ -1826,115 +2007,131 @@ async function initCommand(args) {
1826
2007
  }
1827
2008
  var CONFIG_TEMPLATE = `/**
1828
2009
  * PostgreSDK Configuration
1829
- *
1830
- * This file configures how postgresdk generates your SDK.
2010
+ *
2011
+ * This file configures how postgresdk generates your type-safe API and SDK
2012
+ * from your PostgreSQL database schema.
2013
+ *
2014
+ * QUICK START:
2015
+ * 1. Update the connectionString below
2016
+ * 2. Run: postgresdk generate
2017
+ * 3. Start using your generated SDK!
2018
+ *
2019
+ * CLI COMMANDS:
2020
+ * postgresdk init Initialize this config file
2021
+ * postgresdk generate Generate API and SDK from your database
2022
+ * postgresdk pull Pull SDK from a remote API
2023
+ * postgresdk help Show help and examples
2024
+ *
1831
2025
  * Environment variables are automatically loaded from .env files.
1832
2026
  */
1833
2027
 
1834
2028
  export default {
1835
2029
  // ========== DATABASE CONNECTION (Required) ==========
1836
-
2030
+
1837
2031
  /**
1838
2032
  * PostgreSQL connection string
1839
2033
  * Format: postgres://user:password@host:port/database
2034
+ *
2035
+ * Tip: Use environment variables to keep credentials secure
1840
2036
  */
1841
2037
  connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
1842
-
2038
+
1843
2039
  // ========== BASIC OPTIONS ==========
1844
-
2040
+
1845
2041
  /**
1846
2042
  * Database schema to introspect
1847
- * @default "public"
2043
+ * Default: "public"
1848
2044
  */
1849
2045
  // schema: "public",
1850
-
2046
+
1851
2047
  /**
1852
2048
  * Output directory for server-side code (routes, validators, etc.)
1853
- * @default "./api/server"
2049
+ * Default: "./api/server"
1854
2050
  */
1855
2051
  // outServer: "./api/server",
1856
-
2052
+
1857
2053
  /**
1858
2054
  * Output directory for client SDK
1859
- * @default "./api/client"
2055
+ * Default: "./api/client"
1860
2056
  */
1861
2057
  // outClient: "./api/client",
1862
-
2058
+
1863
2059
  // ========== ADVANCED OPTIONS ==========
1864
-
2060
+
1865
2061
  /**
1866
2062
  * Column name for soft deletes. When set, DELETE operations will update
1867
2063
  * this column instead of removing rows.
1868
- * @default null (hard deletes)
1869
- * @example "deleted_at"
2064
+ *
2065
+ * Default: null (hard deletes)
2066
+ * Example: "deleted_at"
1870
2067
  */
1871
2068
  // softDeleteColumn: null,
1872
-
2069
+
1873
2070
  /**
1874
2071
  * Maximum depth for nested relationship includes to prevent infinite loops
1875
- * @default 2
2072
+ * Default: 2
1876
2073
  */
1877
2074
  // includeMethodsDepth: 2,
1878
-
1879
-
2075
+
2076
+
1880
2077
  /**
1881
2078
  * Server framework for generated API routes
1882
- * - "hono": Lightweight, edge-compatible web framework (default)
1883
- * - "express": Traditional Node.js framework (planned)
1884
- * - "fastify": High-performance Node.js framework (planned)
1885
- * @default "hono"
2079
+ * Options:
2080
+ * - "hono": Lightweight, edge-compatible web framework (default)
2081
+ * - "express": Traditional Node.js framework (planned)
2082
+ * - "fastify": High-performance Node.js framework (planned)
2083
+ *
2084
+ * Default: "hono"
1886
2085
  */
1887
2086
  // serverFramework: "hono",
1888
-
2087
+
1889
2088
  /**
1890
2089
  * Use .js extensions in server imports (for Vercel Edge, Deno, etc.)
1891
- * @default false
2090
+ * Default: false
1892
2091
  */
1893
2092
  // useJsExtensions: false,
1894
-
2093
+
1895
2094
  /**
1896
2095
  * Use .js extensions in client SDK imports (rarely needed)
1897
- * @default false
2096
+ * Default: false
1898
2097
  */
1899
2098
  // useJsExtensionsClient: false,
1900
-
2099
+
1901
2100
  // ========== TEST GENERATION ==========
1902
-
2101
+
1903
2102
  /**
1904
- * Generate basic SDK tests
1905
- * Uncomment to enable test generation with Docker setup
2103
+ * Generate basic SDK tests with Docker setup
2104
+ * Uncomment to enable test generation
1906
2105
  */
1907
2106
  // tests: {
1908
2107
  // generate: true,
1909
2108
  // output: "./api/tests",
1910
2109
  // framework: "vitest" // or "jest" or "bun"
1911
2110
  // },
1912
-
2111
+
1913
2112
  // ========== AUTHENTICATION ==========
1914
-
2113
+
1915
2114
  /**
1916
2115
  * Authentication configuration for your API
1917
- *
1918
- * Simple syntax examples:
2116
+ *
2117
+ * SIMPLE SYNTAX (recommended):
1919
2118
  * auth: { apiKey: process.env.API_KEY }
1920
2119
  * auth: { jwt: process.env.JWT_SECRET }
1921
- *
1922
- * Multiple API keys:
1923
2120
  * auth: { apiKeys: [process.env.KEY1, process.env.KEY2] }
1924
- *
1925
- * Full syntax for advanced options:
2121
+ *
2122
+ * FULL SYNTAX (advanced):
1926
2123
  */
1927
2124
  // auth: {
1928
2125
  // // Strategy: "none" | "api-key" | "jwt-hs256"
1929
2126
  // strategy: "none",
1930
- //
2127
+ //
1931
2128
  // // For API Key authentication
1932
2129
  // apiKeyHeader: "x-api-key", // Header name for API key
1933
2130
  // apiKeys: [ // List of valid API keys
1934
2131
  // process.env.API_KEY_1,
1935
2132
  // process.env.API_KEY_2,
1936
2133
  // ],
1937
- //
2134
+ //
1938
2135
  // // For JWT (HS256) authentication
1939
2136
  // jwt: {
1940
2137
  // sharedSecret: process.env.JWT_SECRET, // Secret for signing/verifying
@@ -1942,12 +2139,12 @@ export default {
1942
2139
  // audience: "my-users", // Optional: validate 'aud' claim
1943
2140
  // }
1944
2141
  // },
1945
-
2142
+
1946
2143
  // ========== SDK DISTRIBUTION (Pull Configuration) ==========
1947
-
2144
+
1948
2145
  /**
1949
2146
  * Configuration for pulling SDK from a remote API
1950
- * Used when running 'postgresdk pull' command
2147
+ * Used when running: postgresdk pull
1951
2148
  */
1952
2149
  // pull: {
1953
2150
  // from: "https://api.myapp.com", // API URL to pull SDK from
@@ -2237,7 +2434,14 @@ function buildGraph(model) {
2237
2434
 
2238
2435
  // src/emit-include-spec.ts
2239
2436
  function emitIncludeSpec(graph) {
2240
- 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
+ */
2241
2445
  `;
2242
2446
  const tables = Object.keys(graph);
2243
2447
  for (const table of tables) {
@@ -2266,7 +2470,14 @@ function toPascal(s) {
2266
2470
 
2267
2471
  // src/emit-include-builder.ts
2268
2472
  function emitIncludeBuilder(graph, maxDepth) {
2269
- 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
+ */
2270
2481
  export const RELATION_GRAPH = ${JSON.stringify(graph, null, 2)} as const;
2271
2482
  type TableName = keyof typeof RELATION_GRAPH;
2272
2483
 
@@ -2414,7 +2625,14 @@ function emitHonoRoutes(table, _graph, opts) {
2414
2625
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
2415
2626
  const ext = opts.useJsExtensions ? ".js" : "";
2416
2627
  const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
2417
- 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
+ */
2418
2636
  import { Hono } from "hono";
2419
2637
  import { z } from "zod";
2420
2638
  import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
@@ -2594,7 +2812,7 @@ function emitClient(table, graph, opts, model) {
2594
2812
  let includeMethodsCode = "";
2595
2813
  for (const method of includeMethods) {
2596
2814
  const isGetByPk = method.name.startsWith("getByPk");
2597
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: any; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2815
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2598
2816
  if (isGetByPk) {
2599
2817
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2600
2818
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -2616,8 +2834,16 @@ function emitClient(table, graph, opts, model) {
2616
2834
  `;
2617
2835
  }
2618
2836
  }
2619
- 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
+ */
2620
2845
  import { BaseClient } from "./base-client${ext}";
2846
+ import type { Where } from "./where-types${ext}";
2621
2847
  ${typeImports}
2622
2848
  ${otherTableImports.join(`
2623
2849
  `)}
@@ -2637,10 +2863,11 @@ export class ${Type}Client extends BaseClient {
2637
2863
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
2638
2864
  }
2639
2865
 
2640
- async list(params?: {
2641
- limit?: number;
2866
+ async list(params?: {
2867
+ include?: any;
2868
+ limit?: number;
2642
2869
  offset?: number;
2643
- where?: any;
2870
+ where?: Where<Select${Type}>;
2644
2871
  orderBy?: string;
2645
2872
  order?: "asc" | "desc";
2646
2873
  }): Promise<Select${Type}[]> {
@@ -2661,7 +2888,14 @@ ${includeMethodsCode}}
2661
2888
  }
2662
2889
  function emitClientIndex(tables, useJsExtensions) {
2663
2890
  const ext = useJsExtensions ? ".js" : "";
2664
- 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
+ */
2665
2899
  `;
2666
2900
  out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
2667
2901
  `;
@@ -2702,6 +2936,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
2702
2936
  out += `export { BaseClient } from "./base-client${ext}";
2703
2937
  `;
2704
2938
  out += `
2939
+ // Include specification types for custom queries
2940
+ `;
2941
+ out += `export type {
2942
+ `;
2943
+ for (const t of tables) {
2944
+ out += ` ${pascal(t.name)}IncludeSpec,
2945
+ `;
2946
+ }
2947
+ out += `} from "./include-spec${ext}";
2948
+ `;
2949
+ out += `
2705
2950
  // Zod schemas for form validation
2706
2951
  `;
2707
2952
  for (const t of tables) {
@@ -2723,7 +2968,14 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
2723
2968
 
2724
2969
  // src/emit-base-client.ts
2725
2970
  function emitBaseClient() {
2726
- 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
+ */
2727
2979
 
2728
2980
  export type HeaderMap = Record<string, string>;
2729
2981
  export type AuthHeadersProvider = () => Promise<HeaderMap> | HeaderMap;
@@ -2870,7 +3122,14 @@ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
2870
3122
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
2871
3123
  }
2872
3124
  const ext = useJsExtensions ? ".js" : "";
2873
- 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
+ */
2874
3133
  import { RELATION_GRAPH } from "./include-builder${ext}";
2875
3134
 
2876
3135
  // Minimal types to keep the file self-contained
@@ -3191,7 +3450,14 @@ function emitTypes(table, opts) {
3191
3450
  return ` ${col.name}: ${valueType};`;
3192
3451
  }).join(`
3193
3452
  `);
3194
- 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
+ */
3195
3461
  export type Insert${Type} = {
3196
3462
  ${insertFields}
3197
3463
  };
@@ -3206,7 +3472,14 @@ ${selectFields}
3206
3472
 
3207
3473
  // src/emit-logger.ts
3208
3474
  function emitLogger() {
3209
- 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
+ */
3210
3483
  const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
3211
3484
 
3212
3485
  export const logger = {
@@ -3239,6 +3512,76 @@ export function safe<T extends (c: any) => any>(handler: T) {
3239
3512
  `;
3240
3513
  }
3241
3514
 
3515
+ // src/emit-where-types.ts
3516
+ function emitWhereTypes() {
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
+ */
3525
+
3526
+ /**
3527
+ * WHERE clause operators for filtering
3528
+ */
3529
+ export type WhereOperator<T> = {
3530
+ /** Equal to */
3531
+ $eq?: T;
3532
+ /** Not equal to */
3533
+ $ne?: T;
3534
+ /** Greater than */
3535
+ $gt?: T;
3536
+ /** Greater than or equal to */
3537
+ $gte?: T;
3538
+ /** Less than */
3539
+ $lt?: T;
3540
+ /** Less than or equal to */
3541
+ $lte?: T;
3542
+ /** In array */
3543
+ $in?: T[];
3544
+ /** Not in array */
3545
+ $nin?: T[];
3546
+ /** LIKE pattern match (strings only) */
3547
+ $like?: T extends string ? string : never;
3548
+ /** Case-insensitive LIKE (strings only) */
3549
+ $ilike?: T extends string ? string : never;
3550
+ /** IS NULL */
3551
+ $is?: null;
3552
+ /** IS NOT NULL */
3553
+ $isNot?: null;
3554
+ };
3555
+
3556
+ /**
3557
+ * WHERE condition - can be a direct value or an operator object
3558
+ */
3559
+ export type WhereCondition<T> = T | WhereOperator<T>;
3560
+
3561
+ /**
3562
+ * Field-level WHERE conditions (without logical operators)
3563
+ */
3564
+ export type WhereFieldConditions<T> = {
3565
+ [K in keyof T]?: WhereCondition<T[K]>;
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
+ };
3582
+ `;
3583
+ }
3584
+
3242
3585
  // src/emit-auth.ts
3243
3586
  function emitAuth(cfgAuth) {
3244
3587
  const strategy = cfgAuth?.strategy ?? "none";
@@ -3253,7 +3596,14 @@ function emitAuth(cfgAuth) {
3253
3596
  const JWT_SHARED_SECRET = JSON.stringify(jwtShared);
3254
3597
  const JWT_ISSUER = jwtIssuer === undefined ? "undefined" : JSON.stringify(jwtIssuer);
3255
3598
  const JWT_AUDIENCE = jwtAudience === undefined ? "undefined" : JSON.stringify(jwtAudience);
3256
- 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
+ */
3257
3607
  import type { Context, Next } from "hono";
3258
3608
 
3259
3609
  // ---- Config inlined by generator ----
@@ -3410,7 +3760,14 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
3410
3760
  return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
3411
3761
  }).join(`
3412
3762
  `);
3413
- 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
+ */
3414
3771
  import { Hono } from "hono";
3415
3772
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
3416
3773
  import { getContract } from "./contract${ext}";
@@ -3544,7 +3901,14 @@ function emitSdkBundle(clientFiles, clientDir) {
3544
3901
  }
3545
3902
  const version = `1.0.0`;
3546
3903
  const generated = new Date().toISOString();
3547
- 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
+ */
3548
3912
 
3549
3913
  export const SDK_MANIFEST = {
3550
3914
  version: "${version}",
@@ -3648,6 +4012,155 @@ export async function getByPk(
3648
4012
  }
3649
4013
  }
3650
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
+
3651
4164
  /**
3652
4165
  * LIST operation - Get multiple records with optional filters
3653
4166
  */
@@ -3657,60 +4170,54 @@ export async function listRecords(
3657
4170
  ): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
3658
4171
  try {
3659
4172
  const { where: whereClause, limit = 50, offset = 0, include } = params;
3660
-
4173
+
3661
4174
  // Build WHERE clause
3662
- const whereParts: string[] = [];
3663
- const whereParams: any[] = [];
3664
4175
  let paramIndex = 1;
3665
-
4176
+ const whereParts: string[] = [];
4177
+ let whereParams: any[] = [];
4178
+
3666
4179
  // Add soft delete filter if applicable
3667
4180
  if (ctx.softDeleteColumn) {
3668
4181
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
3669
4182
  }
3670
-
4183
+
3671
4184
  // Add user-provided where conditions
3672
- if (whereClause && typeof whereClause === 'object') {
3673
- for (const [key, value] of Object.entries(whereClause)) {
3674
- if (value === null) {
3675
- whereParts.push(\`"\${key}" IS NULL\`);
3676
- } else if (value === undefined) {
3677
- // Skip undefined values
3678
- continue;
3679
- } else {
3680
- whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3681
- whereParams.push(value);
3682
- paramIndex++;
3683
- }
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;
3684
4191
  }
3685
4192
  }
3686
-
4193
+
3687
4194
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
3688
-
4195
+
3689
4196
  // Add limit and offset params
3690
4197
  const limitParam = \`$\${paramIndex}\`;
3691
4198
  const offsetParam = \`$\${paramIndex + 1}\`;
3692
4199
  const allParams = [...whereParams, limit, offset];
3693
-
4200
+
3694
4201
  const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
3695
4202
  log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
3696
-
4203
+
3697
4204
  const { rows } = await ctx.pg.query(text, allParams);
3698
-
4205
+
3699
4206
  if (!include) {
3700
4207
  log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
3701
4208
  return { data: rows, status: 200 };
3702
4209
  }
3703
-
4210
+
3704
4211
  // Include logic will be handled by the include-loader
3705
4212
  // For now, just return the rows with a note that includes need to be applied
3706
4213
  log.debug(\`LIST \${ctx.table} include spec:\`, include);
3707
4214
  return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
3708
4215
  } catch (e: any) {
3709
4216
  log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
3710
- return {
3711
- error: e?.message ?? "Internal error",
4217
+ return {
4218
+ error: e?.message ?? "Internal error",
3712
4219
  ...(DEBUG ? { stack: e?.stack } : {}),
3713
- status: 500
4220
+ status: 500
3714
4221
  };
3715
4222
  }
3716
4223
  }
@@ -4612,6 +5119,7 @@ async function generate(configPath) {
4612
5119
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
4613
5120
  files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
4614
5121
  files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
5122
+ files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
4615
5123
  files.push({
4616
5124
  path: join(serverDir, "include-builder.ts"),
4617
5125
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)