postgresdk 0.9.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -160,6 +160,7 @@ const authors = await sdk.authors.list({
160
160
  ### Filtering & Pagination
161
161
 
162
162
  ```typescript
163
+ // Simple equality filtering
163
164
  const users = await sdk.users.list({
164
165
  where: { status: "active" },
165
166
  orderBy: "created_at",
@@ -167,8 +168,20 @@ const users = await sdk.users.list({
167
168
  limit: 20,
168
169
  offset: 40
169
170
  });
171
+
172
+ // Advanced WHERE operators
173
+ const filtered = await sdk.users.list({
174
+ where: {
175
+ age: { $gte: 18, $lt: 65 }, // Range queries
176
+ email: { $ilike: '%@company.com' }, // Pattern matching
177
+ status: { $in: ['active', 'pending'] }, // Array matching
178
+ deleted_at: { $is: null } // NULL checks
179
+ }
180
+ });
170
181
  ```
171
182
 
183
+ See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`.
184
+
172
185
  ## Authentication
173
186
 
174
187
  postgresdk supports API key and JWT authentication:
package/dist/cli.js CHANGED
@@ -1170,6 +1170,140 @@ 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("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1306
+ lines.push("");
1173
1307
  lines.push("## Resources");
1174
1308
  lines.push("");
1175
1309
  for (const resource of contract.resources) {
@@ -1826,115 +1960,131 @@ async function initCommand(args) {
1826
1960
  }
1827
1961
  var CONFIG_TEMPLATE = `/**
1828
1962
  * PostgreSDK Configuration
1829
- *
1830
- * This file configures how postgresdk generates your SDK.
1963
+ *
1964
+ * This file configures how postgresdk generates your type-safe API and SDK
1965
+ * from your PostgreSQL database schema.
1966
+ *
1967
+ * QUICK START:
1968
+ * 1. Update the connectionString below
1969
+ * 2. Run: postgresdk generate
1970
+ * 3. Start using your generated SDK!
1971
+ *
1972
+ * CLI COMMANDS:
1973
+ * postgresdk init Initialize this config file
1974
+ * postgresdk generate Generate API and SDK from your database
1975
+ * postgresdk pull Pull SDK from a remote API
1976
+ * postgresdk help Show help and examples
1977
+ *
1831
1978
  * Environment variables are automatically loaded from .env files.
1832
1979
  */
1833
1980
 
1834
1981
  export default {
1835
1982
  // ========== DATABASE CONNECTION (Required) ==========
1836
-
1983
+
1837
1984
  /**
1838
1985
  * PostgreSQL connection string
1839
1986
  * Format: postgres://user:password@host:port/database
1987
+ *
1988
+ * Tip: Use environment variables to keep credentials secure
1840
1989
  */
1841
1990
  connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
1842
-
1991
+
1843
1992
  // ========== BASIC OPTIONS ==========
1844
-
1993
+
1845
1994
  /**
1846
1995
  * Database schema to introspect
1847
- * @default "public"
1996
+ * Default: "public"
1848
1997
  */
1849
1998
  // schema: "public",
1850
-
1999
+
1851
2000
  /**
1852
2001
  * Output directory for server-side code (routes, validators, etc.)
1853
- * @default "./api/server"
2002
+ * Default: "./api/server"
1854
2003
  */
1855
2004
  // outServer: "./api/server",
1856
-
2005
+
1857
2006
  /**
1858
2007
  * Output directory for client SDK
1859
- * @default "./api/client"
2008
+ * Default: "./api/client"
1860
2009
  */
1861
2010
  // outClient: "./api/client",
1862
-
2011
+
1863
2012
  // ========== ADVANCED OPTIONS ==========
1864
-
2013
+
1865
2014
  /**
1866
2015
  * Column name for soft deletes. When set, DELETE operations will update
1867
2016
  * this column instead of removing rows.
1868
- * @default null (hard deletes)
1869
- * @example "deleted_at"
2017
+ *
2018
+ * Default: null (hard deletes)
2019
+ * Example: "deleted_at"
1870
2020
  */
1871
2021
  // softDeleteColumn: null,
1872
-
2022
+
1873
2023
  /**
1874
2024
  * Maximum depth for nested relationship includes to prevent infinite loops
1875
- * @default 2
2025
+ * Default: 2
1876
2026
  */
1877
2027
  // includeMethodsDepth: 2,
1878
-
1879
-
2028
+
2029
+
1880
2030
  /**
1881
2031
  * 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"
2032
+ * Options:
2033
+ * - "hono": Lightweight, edge-compatible web framework (default)
2034
+ * - "express": Traditional Node.js framework (planned)
2035
+ * - "fastify": High-performance Node.js framework (planned)
2036
+ *
2037
+ * Default: "hono"
1886
2038
  */
1887
2039
  // serverFramework: "hono",
1888
-
2040
+
1889
2041
  /**
1890
2042
  * Use .js extensions in server imports (for Vercel Edge, Deno, etc.)
1891
- * @default false
2043
+ * Default: false
1892
2044
  */
1893
2045
  // useJsExtensions: false,
1894
-
2046
+
1895
2047
  /**
1896
2048
  * Use .js extensions in client SDK imports (rarely needed)
1897
- * @default false
2049
+ * Default: false
1898
2050
  */
1899
2051
  // useJsExtensionsClient: false,
1900
-
2052
+
1901
2053
  // ========== TEST GENERATION ==========
1902
-
2054
+
1903
2055
  /**
1904
- * Generate basic SDK tests
1905
- * Uncomment to enable test generation with Docker setup
2056
+ * Generate basic SDK tests with Docker setup
2057
+ * Uncomment to enable test generation
1906
2058
  */
1907
2059
  // tests: {
1908
2060
  // generate: true,
1909
2061
  // output: "./api/tests",
1910
2062
  // framework: "vitest" // or "jest" or "bun"
1911
2063
  // },
1912
-
2064
+
1913
2065
  // ========== AUTHENTICATION ==========
1914
-
2066
+
1915
2067
  /**
1916
2068
  * Authentication configuration for your API
1917
- *
1918
- * Simple syntax examples:
2069
+ *
2070
+ * SIMPLE SYNTAX (recommended):
1919
2071
  * auth: { apiKey: process.env.API_KEY }
1920
2072
  * auth: { jwt: process.env.JWT_SECRET }
1921
- *
1922
- * Multiple API keys:
1923
2073
  * auth: { apiKeys: [process.env.KEY1, process.env.KEY2] }
1924
- *
1925
- * Full syntax for advanced options:
2074
+ *
2075
+ * FULL SYNTAX (advanced):
1926
2076
  */
1927
2077
  // auth: {
1928
2078
  // // Strategy: "none" | "api-key" | "jwt-hs256"
1929
2079
  // strategy: "none",
1930
- //
2080
+ //
1931
2081
  // // For API Key authentication
1932
2082
  // apiKeyHeader: "x-api-key", // Header name for API key
1933
2083
  // apiKeys: [ // List of valid API keys
1934
2084
  // process.env.API_KEY_1,
1935
2085
  // process.env.API_KEY_2,
1936
2086
  // ],
1937
- //
2087
+ //
1938
2088
  // // For JWT (HS256) authentication
1939
2089
  // jwt: {
1940
2090
  // sharedSecret: process.env.JWT_SECRET, // Secret for signing/verifying
@@ -1942,12 +2092,12 @@ export default {
1942
2092
  // audience: "my-users", // Optional: validate 'aud' claim
1943
2093
  // }
1944
2094
  // },
1945
-
2095
+
1946
2096
  // ========== SDK DISTRIBUTION (Pull Configuration) ==========
1947
-
2097
+
1948
2098
  /**
1949
2099
  * Configuration for pulling SDK from a remote API
1950
- * Used when running 'postgresdk pull' command
2100
+ * Used when running: postgresdk pull
1951
2101
  */
1952
2102
  // pull: {
1953
2103
  // from: "https://api.myapp.com", // API URL to pull SDK from
@@ -2594,7 +2744,7 @@ function emitClient(table, graph, opts, model) {
2594
2744
  let includeMethodsCode = "";
2595
2745
  for (const method of includeMethods) {
2596
2746
  const isGetByPk = method.name.startsWith("getByPk");
2597
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: any; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2747
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
2598
2748
  if (isGetByPk) {
2599
2749
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
2600
2750
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -2618,6 +2768,7 @@ function emitClient(table, graph, opts, model) {
2618
2768
  }
2619
2769
  return `/* Generated. Do not edit. */
2620
2770
  import { BaseClient } from "./base-client${ext}";
2771
+ import type { Where } from "./where-types${ext}";
2621
2772
  ${typeImports}
2622
2773
  ${otherTableImports.join(`
2623
2774
  `)}
@@ -2637,10 +2788,11 @@ export class ${Type}Client extends BaseClient {
2637
2788
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
2638
2789
  }
2639
2790
 
2640
- async list(params?: {
2641
- limit?: number;
2791
+ async list(params?: {
2792
+ include?: any;
2793
+ limit?: number;
2642
2794
  offset?: number;
2643
- where?: any;
2795
+ where?: Where<Select${Type}>;
2644
2796
  orderBy?: string;
2645
2797
  order?: "asc" | "desc";
2646
2798
  }): Promise<Select${Type}[]> {
@@ -2702,6 +2854,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
2702
2854
  out += `export { BaseClient } from "./base-client${ext}";
2703
2855
  `;
2704
2856
  out += `
2857
+ // Include specification types for custom queries
2858
+ `;
2859
+ out += `export type {
2860
+ `;
2861
+ for (const t of tables) {
2862
+ out += ` ${pascal(t.name)}IncludeSpec,
2863
+ `;
2864
+ }
2865
+ out += `} from "./include-spec${ext}";
2866
+ `;
2867
+ out += `
2705
2868
  // Zod schemas for form validation
2706
2869
  `;
2707
2870
  for (const t of tables) {
@@ -3239,6 +3402,54 @@ export function safe<T extends (c: any) => any>(handler: T) {
3239
3402
  `;
3240
3403
  }
3241
3404
 
3405
+ // src/emit-where-types.ts
3406
+ function emitWhereTypes() {
3407
+ return `/* Generated. Do not edit. */
3408
+
3409
+ /**
3410
+ * WHERE clause operators for filtering
3411
+ */
3412
+ export type WhereOperator<T> = {
3413
+ /** Equal to */
3414
+ $eq?: T;
3415
+ /** Not equal to */
3416
+ $ne?: T;
3417
+ /** Greater than */
3418
+ $gt?: T;
3419
+ /** Greater than or equal to */
3420
+ $gte?: T;
3421
+ /** Less than */
3422
+ $lt?: T;
3423
+ /** Less than or equal to */
3424
+ $lte?: T;
3425
+ /** In array */
3426
+ $in?: T[];
3427
+ /** Not in array */
3428
+ $nin?: T[];
3429
+ /** LIKE pattern match (strings only) */
3430
+ $like?: T extends string ? string : never;
3431
+ /** Case-insensitive LIKE (strings only) */
3432
+ $ilike?: T extends string ? string : never;
3433
+ /** IS NULL */
3434
+ $is?: null;
3435
+ /** IS NOT NULL */
3436
+ $isNot?: null;
3437
+ };
3438
+
3439
+ /**
3440
+ * WHERE condition - can be a direct value or an operator object
3441
+ */
3442
+ export type WhereCondition<T> = T | WhereOperator<T>;
3443
+
3444
+ /**
3445
+ * WHERE clause type for a given table type
3446
+ */
3447
+ export type Where<T> = {
3448
+ [K in keyof T]?: WhereCondition<T[K]>;
3449
+ };
3450
+ `;
3451
+ }
3452
+
3242
3453
  // src/emit-auth.ts
3243
3454
  function emitAuth(cfgAuth) {
3244
3455
  const strategy = cfgAuth?.strategy ?? "none";
@@ -3671,12 +3882,88 @@ export async function listRecords(
3671
3882
  // Add user-provided where conditions
3672
3883
  if (whereClause && typeof whereClause === 'object') {
3673
3884
  for (const [key, value] of Object.entries(whereClause)) {
3674
- if (value === null) {
3675
- whereParts.push(\`"\${key}" IS NULL\`);
3676
- } else if (value === undefined) {
3885
+ if (value === undefined) {
3677
3886
  // Skip undefined values
3678
3887
  continue;
3888
+ }
3889
+
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\`);
3679
3965
  } else {
3966
+ // Direct value (simple equality)
3680
3967
  whereParts.push(\`"\${key}" = $\${paramIndex}\`);
3681
3968
  whereParams.push(value);
3682
3969
  paramIndex++;
@@ -4612,6 +4899,7 @@ async function generate(configPath) {
4612
4899
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
4613
4900
  files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
4614
4901
  files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
4902
+ files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
4615
4903
  files.push({
4616
4904
  path: join(serverDir, "include-builder.ts"),
4617
4905
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Emits WHERE clause type utilities for type-safe filtering
3
+ */
4
+ export declare function emitWhereTypes(): string;
package/dist/index.js CHANGED
@@ -1169,6 +1169,140 @@ 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("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1305
+ lines.push("");
1172
1306
  lines.push("## Resources");
1173
1307
  lines.push("");
1174
1308
  for (const resource of contract.resources) {
@@ -1850,7 +1984,7 @@ function emitClient(table, graph, opts, model) {
1850
1984
  let includeMethodsCode = "";
1851
1985
  for (const method of includeMethods) {
1852
1986
  const isGetByPk = method.name.startsWith("getByPk");
1853
- const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: any; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
1987
+ const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
1854
1988
  if (isGetByPk) {
1855
1989
  const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
1856
1990
  const baseReturnType = method.returnType.replace(" | null", "");
@@ -1874,6 +2008,7 @@ function emitClient(table, graph, opts, model) {
1874
2008
  }
1875
2009
  return `/* Generated. Do not edit. */
1876
2010
  import { BaseClient } from "./base-client${ext}";
2011
+ import type { Where } from "./where-types${ext}";
1877
2012
  ${typeImports}
1878
2013
  ${otherTableImports.join(`
1879
2014
  `)}
@@ -1893,10 +2028,11 @@ export class ${Type}Client extends BaseClient {
1893
2028
  return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
1894
2029
  }
1895
2030
 
1896
- async list(params?: {
1897
- limit?: number;
2031
+ async list(params?: {
2032
+ include?: any;
2033
+ limit?: number;
1898
2034
  offset?: number;
1899
- where?: any;
2035
+ where?: Where<Select${Type}>;
1900
2036
  orderBy?: string;
1901
2037
  order?: "asc" | "desc";
1902
2038
  }): Promise<Select${Type}[]> {
@@ -1958,6 +2094,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
1958
2094
  out += `export { BaseClient } from "./base-client${ext}";
1959
2095
  `;
1960
2096
  out += `
2097
+ // Include specification types for custom queries
2098
+ `;
2099
+ out += `export type {
2100
+ `;
2101
+ for (const t of tables) {
2102
+ out += ` ${pascal(t.name)}IncludeSpec,
2103
+ `;
2104
+ }
2105
+ out += `} from "./include-spec${ext}";
2106
+ `;
2107
+ out += `
1961
2108
  // Zod schemas for form validation
1962
2109
  `;
1963
2110
  for (const t of tables) {
@@ -2495,6 +2642,54 @@ export function safe<T extends (c: any) => any>(handler: T) {
2495
2642
  `;
2496
2643
  }
2497
2644
 
2645
+ // src/emit-where-types.ts
2646
+ function emitWhereTypes() {
2647
+ return `/* Generated. Do not edit. */
2648
+
2649
+ /**
2650
+ * WHERE clause operators for filtering
2651
+ */
2652
+ export type WhereOperator<T> = {
2653
+ /** Equal to */
2654
+ $eq?: T;
2655
+ /** Not equal to */
2656
+ $ne?: T;
2657
+ /** Greater than */
2658
+ $gt?: T;
2659
+ /** Greater than or equal to */
2660
+ $gte?: T;
2661
+ /** Less than */
2662
+ $lt?: T;
2663
+ /** Less than or equal to */
2664
+ $lte?: T;
2665
+ /** In array */
2666
+ $in?: T[];
2667
+ /** Not in array */
2668
+ $nin?: T[];
2669
+ /** LIKE pattern match (strings only) */
2670
+ $like?: T extends string ? string : never;
2671
+ /** Case-insensitive LIKE (strings only) */
2672
+ $ilike?: T extends string ? string : never;
2673
+ /** IS NULL */
2674
+ $is?: null;
2675
+ /** IS NOT NULL */
2676
+ $isNot?: null;
2677
+ };
2678
+
2679
+ /**
2680
+ * WHERE condition - can be a direct value or an operator object
2681
+ */
2682
+ export type WhereCondition<T> = T | WhereOperator<T>;
2683
+
2684
+ /**
2685
+ * WHERE clause type for a given table type
2686
+ */
2687
+ export type Where<T> = {
2688
+ [K in keyof T]?: WhereCondition<T[K]>;
2689
+ };
2690
+ `;
2691
+ }
2692
+
2498
2693
  // src/emit-auth.ts
2499
2694
  function emitAuth(cfgAuth) {
2500
2695
  const strategy = cfgAuth?.strategy ?? "none";
@@ -2927,12 +3122,88 @@ export async function listRecords(
2927
3122
  // Add user-provided where conditions
2928
3123
  if (whereClause && typeof whereClause === 'object') {
2929
3124
  for (const [key, value] of Object.entries(whereClause)) {
2930
- if (value === null) {
2931
- whereParts.push(\`"\${key}" IS NULL\`);
2932
- } else if (value === undefined) {
3125
+ if (value === undefined) {
2933
3126
  // Skip undefined values
2934
3127
  continue;
3128
+ }
3129
+
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\`);
2935
3205
  } else {
3206
+ // Direct value (simple equality)
2936
3207
  whereParts.push(\`"\${key}" = $\${paramIndex}\`);
2937
3208
  whereParams.push(value);
2938
3209
  paramIndex++;
@@ -3868,6 +4139,7 @@ async function generate(configPath) {
3868
4139
  files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
3869
4140
  files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
3870
4141
  files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
4142
+ files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
3871
4143
  files.push({
3872
4144
  path: join(serverDir, "include-builder.ts"),
3873
4145
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.9.9",
3
+ "version": "0.10.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {