postgresdk 0.9.8 → 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 +13 -0
- package/dist/cli.js +374 -56
- package/dist/core/operations.d.ts +1 -0
- package/dist/emit-where-types.d.ts +4 -0
- package/dist/index.js +315 -13
- package/package.json +2 -2
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
|
-
*
|
|
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
|
-
*
|
|
2002
|
+
* Default: "./api/server"
|
|
1854
2003
|
*/
|
|
1855
2004
|
// outServer: "./api/server",
|
|
1856
|
-
|
|
2005
|
+
|
|
1857
2006
|
/**
|
|
1858
2007
|
* Output directory for client SDK
|
|
1859
|
-
*
|
|
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
|
-
*
|
|
1869
|
-
*
|
|
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
|
-
*
|
|
2025
|
+
* Default: 2
|
|
1876
2026
|
*/
|
|
1877
2027
|
// includeMethodsDepth: 2,
|
|
1878
|
-
|
|
1879
|
-
|
|
2028
|
+
|
|
2029
|
+
|
|
1880
2030
|
/**
|
|
1881
2031
|
* Server framework for generated API routes
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
2100
|
+
* Used when running: postgresdk pull
|
|
1951
2101
|
*/
|
|
1952
2102
|
// pull: {
|
|
1953
2103
|
// from: "https://api.myapp.com", // API URL to pull SDK from
|
|
@@ -2423,6 +2573,7 @@ import * as coreOps from "../core/operations${ext}";
|
|
|
2423
2573
|
${authImport}
|
|
2424
2574
|
|
|
2425
2575
|
const listSchema = z.object({
|
|
2576
|
+
where: z.any().optional(),
|
|
2426
2577
|
include: z.any().optional(),
|
|
2427
2578
|
limit: z.number().int().positive().max(100).optional(),
|
|
2428
2579
|
offset: z.number().int().min(0).optional(),
|
|
@@ -2593,7 +2744,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2593
2744
|
let includeMethodsCode = "";
|
|
2594
2745
|
for (const method of includeMethods) {
|
|
2595
2746
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
2596
|
-
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?:
|
|
2747
|
+
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
|
|
2597
2748
|
if (isGetByPk) {
|
|
2598
2749
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
2599
2750
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
@@ -2617,6 +2768,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2617
2768
|
}
|
|
2618
2769
|
return `/* Generated. Do not edit. */
|
|
2619
2770
|
import { BaseClient } from "./base-client${ext}";
|
|
2771
|
+
import type { Where } from "./where-types${ext}";
|
|
2620
2772
|
${typeImports}
|
|
2621
2773
|
${otherTableImports.join(`
|
|
2622
2774
|
`)}
|
|
@@ -2636,10 +2788,11 @@ export class ${Type}Client extends BaseClient {
|
|
|
2636
2788
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
2637
2789
|
}
|
|
2638
2790
|
|
|
2639
|
-
async list(params?: {
|
|
2640
|
-
|
|
2791
|
+
async list(params?: {
|
|
2792
|
+
include?: any;
|
|
2793
|
+
limit?: number;
|
|
2641
2794
|
offset?: number;
|
|
2642
|
-
where?:
|
|
2795
|
+
where?: Where<Select${Type}>;
|
|
2643
2796
|
orderBy?: string;
|
|
2644
2797
|
order?: "asc" | "desc";
|
|
2645
2798
|
}): Promise<Select${Type}[]> {
|
|
@@ -2701,6 +2854,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
2701
2854
|
out += `export { BaseClient } from "./base-client${ext}";
|
|
2702
2855
|
`;
|
|
2703
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 += `
|
|
2704
2868
|
// Zod schemas for form validation
|
|
2705
2869
|
`;
|
|
2706
2870
|
for (const t of tables) {
|
|
@@ -3238,6 +3402,54 @@ export function safe<T extends (c: any) => any>(handler: T) {
|
|
|
3238
3402
|
`;
|
|
3239
3403
|
}
|
|
3240
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
|
+
|
|
3241
3453
|
// src/emit-auth.ts
|
|
3242
3454
|
function emitAuth(cfgAuth) {
|
|
3243
3455
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
@@ -3652,19 +3864,124 @@ export async function getByPk(
|
|
|
3652
3864
|
*/
|
|
3653
3865
|
export async function listRecords(
|
|
3654
3866
|
ctx: OperationContext,
|
|
3655
|
-
params: { limit?: number; offset?: number; include?: any }
|
|
3656
|
-
): Promise<{ data?: any; error?: string; issues?: any;
|
|
3867
|
+
params: { where?: any; limit?: number; offset?: number; include?: any }
|
|
3868
|
+
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
3657
3869
|
try {
|
|
3658
|
-
const { limit = 50, offset = 0, include } = params;
|
|
3870
|
+
const { where: whereClause, limit = 50, offset = 0, include } = params;
|
|
3871
|
+
|
|
3872
|
+
// Build WHERE clause
|
|
3873
|
+
const whereParts: string[] = [];
|
|
3874
|
+
const whereParams: any[] = [];
|
|
3875
|
+
let paramIndex = 1;
|
|
3876
|
+
|
|
3877
|
+
// Add soft delete filter if applicable
|
|
3878
|
+
if (ctx.softDeleteColumn) {
|
|
3879
|
+
whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// Add user-provided where conditions
|
|
3883
|
+
if (whereClause && typeof whereClause === 'object') {
|
|
3884
|
+
for (const [key, value] of Object.entries(whereClause)) {
|
|
3885
|
+
if (value === undefined) {
|
|
3886
|
+
// Skip undefined values
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
// Handle operator objects like { $gt: 5, $like: "%test%" }
|
|
3891
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
3892
|
+
for (const [op, opValue] of Object.entries(value)) {
|
|
3893
|
+
if (opValue === undefined) continue;
|
|
3894
|
+
|
|
3895
|
+
switch (op) {
|
|
3896
|
+
case '$eq':
|
|
3897
|
+
whereParts.push(\`"\${key}" = $\${paramIndex}\`);
|
|
3898
|
+
whereParams.push(opValue);
|
|
3899
|
+
paramIndex++;
|
|
3900
|
+
break;
|
|
3901
|
+
case '$ne':
|
|
3902
|
+
whereParts.push(\`"\${key}" != $\${paramIndex}\`);
|
|
3903
|
+
whereParams.push(opValue);
|
|
3904
|
+
paramIndex++;
|
|
3905
|
+
break;
|
|
3906
|
+
case '$gt':
|
|
3907
|
+
whereParts.push(\`"\${key}" > $\${paramIndex}\`);
|
|
3908
|
+
whereParams.push(opValue);
|
|
3909
|
+
paramIndex++;
|
|
3910
|
+
break;
|
|
3911
|
+
case '$gte':
|
|
3912
|
+
whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
|
|
3913
|
+
whereParams.push(opValue);
|
|
3914
|
+
paramIndex++;
|
|
3915
|
+
break;
|
|
3916
|
+
case '$lt':
|
|
3917
|
+
whereParts.push(\`"\${key}" < $\${paramIndex}\`);
|
|
3918
|
+
whereParams.push(opValue);
|
|
3919
|
+
paramIndex++;
|
|
3920
|
+
break;
|
|
3921
|
+
case '$lte':
|
|
3922
|
+
whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
|
|
3923
|
+
whereParams.push(opValue);
|
|
3924
|
+
paramIndex++;
|
|
3925
|
+
break;
|
|
3926
|
+
case '$in':
|
|
3927
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
3928
|
+
whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
|
|
3929
|
+
whereParams.push(opValue);
|
|
3930
|
+
paramIndex++;
|
|
3931
|
+
}
|
|
3932
|
+
break;
|
|
3933
|
+
case '$nin':
|
|
3934
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
3935
|
+
whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
|
|
3936
|
+
whereParams.push(opValue);
|
|
3937
|
+
paramIndex++;
|
|
3938
|
+
}
|
|
3939
|
+
break;
|
|
3940
|
+
case '$like':
|
|
3941
|
+
whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
|
|
3942
|
+
whereParams.push(opValue);
|
|
3943
|
+
paramIndex++;
|
|
3944
|
+
break;
|
|
3945
|
+
case '$ilike':
|
|
3946
|
+
whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
|
|
3947
|
+
whereParams.push(opValue);
|
|
3948
|
+
paramIndex++;
|
|
3949
|
+
break;
|
|
3950
|
+
case '$is':
|
|
3951
|
+
if (opValue === null) {
|
|
3952
|
+
whereParts.push(\`"\${key}" IS NULL\`);
|
|
3953
|
+
}
|
|
3954
|
+
break;
|
|
3955
|
+
case '$isNot':
|
|
3956
|
+
if (opValue === null) {
|
|
3957
|
+
whereParts.push(\`"\${key}" IS NOT NULL\`);
|
|
3958
|
+
}
|
|
3959
|
+
break;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
} else if (value === null) {
|
|
3963
|
+
// Direct null value
|
|
3964
|
+
whereParts.push(\`"\${key}" IS NULL\`);
|
|
3965
|
+
} else {
|
|
3966
|
+
// Direct value (simple equality)
|
|
3967
|
+
whereParts.push(\`"\${key}" = $\${paramIndex}\`);
|
|
3968
|
+
whereParams.push(value);
|
|
3969
|
+
paramIndex++;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
3659
3975
|
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3976
|
+
// Add limit and offset params
|
|
3977
|
+
const limitParam = \`$\${paramIndex}\`;
|
|
3978
|
+
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
3979
|
+
const allParams = [...whereParams, limit, offset];
|
|
3663
3980
|
|
|
3664
|
-
const text = \`SELECT * FROM "\${ctx.table}" \${
|
|
3665
|
-
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:",
|
|
3981
|
+
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3982
|
+
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
3666
3983
|
|
|
3667
|
-
const { rows } = await ctx.pg.query(text,
|
|
3984
|
+
const { rows } = await ctx.pg.query(text, allParams);
|
|
3668
3985
|
|
|
3669
3986
|
if (!include) {
|
|
3670
3987
|
log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
|
|
@@ -4582,6 +4899,7 @@ async function generate(configPath) {
|
|
|
4582
4899
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
4583
4900
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
4584
4901
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
4902
|
+
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
4585
4903
|
files.push({
|
|
4586
4904
|
path: join(serverDir, "include-builder.ts"),
|
|
4587
4905
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
@@ -35,6 +35,7 @@ export declare function getByPk(ctx: OperationContext, pkValues: any[]): Promise
|
|
|
35
35
|
* LIST operation - Get multiple records with optional filters
|
|
36
36
|
*/
|
|
37
37
|
export declare function listRecords(ctx: OperationContext, params: {
|
|
38
|
+
where?: any;
|
|
38
39
|
limit?: number;
|
|
39
40
|
offset?: number;
|
|
40
41
|
include?: any;
|
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) {
|
|
@@ -1679,6 +1813,7 @@ import * as coreOps from "../core/operations${ext}";
|
|
|
1679
1813
|
${authImport}
|
|
1680
1814
|
|
|
1681
1815
|
const listSchema = z.object({
|
|
1816
|
+
where: z.any().optional(),
|
|
1682
1817
|
include: z.any().optional(),
|
|
1683
1818
|
limit: z.number().int().positive().max(100).optional(),
|
|
1684
1819
|
offset: z.number().int().min(0).optional(),
|
|
@@ -1849,7 +1984,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
1849
1984
|
let includeMethodsCode = "";
|
|
1850
1985
|
for (const method of includeMethods) {
|
|
1851
1986
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
1852
|
-
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?:
|
|
1987
|
+
const baseParams = isGetByPk ? "" : `params?: Omit<{ limit?: number; offset?: number; where?: Where<Select${Type}>; orderBy?: string; order?: "asc" | "desc"; }, "include">`;
|
|
1853
1988
|
if (isGetByPk) {
|
|
1854
1989
|
const pkWhere = hasCompositePk ? `{ ${safePk.map((col) => `${col}: pk.${col}`).join(", ")} }` : `{ ${safePk[0] || "id"}: pk }`;
|
|
1855
1990
|
const baseReturnType = method.returnType.replace(" | null", "");
|
|
@@ -1873,6 +2008,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
1873
2008
|
}
|
|
1874
2009
|
return `/* Generated. Do not edit. */
|
|
1875
2010
|
import { BaseClient } from "./base-client${ext}";
|
|
2011
|
+
import type { Where } from "./where-types${ext}";
|
|
1876
2012
|
${typeImports}
|
|
1877
2013
|
${otherTableImports.join(`
|
|
1878
2014
|
`)}
|
|
@@ -1892,10 +2028,11 @@ export class ${Type}Client extends BaseClient {
|
|
|
1892
2028
|
return this.get<Select${Type} | null>(\`\${this.resource}/\${path}\`);
|
|
1893
2029
|
}
|
|
1894
2030
|
|
|
1895
|
-
async list(params?: {
|
|
1896
|
-
|
|
2031
|
+
async list(params?: {
|
|
2032
|
+
include?: any;
|
|
2033
|
+
limit?: number;
|
|
1897
2034
|
offset?: number;
|
|
1898
|
-
where?:
|
|
2035
|
+
where?: Where<Select${Type}>;
|
|
1899
2036
|
orderBy?: string;
|
|
1900
2037
|
order?: "asc" | "desc";
|
|
1901
2038
|
}): Promise<Select${Type}[]> {
|
|
@@ -1957,6 +2094,17 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
1957
2094
|
out += `export { BaseClient } from "./base-client${ext}";
|
|
1958
2095
|
`;
|
|
1959
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 += `
|
|
1960
2108
|
// Zod schemas for form validation
|
|
1961
2109
|
`;
|
|
1962
2110
|
for (const t of tables) {
|
|
@@ -2494,6 +2642,54 @@ export function safe<T extends (c: any) => any>(handler: T) {
|
|
|
2494
2642
|
`;
|
|
2495
2643
|
}
|
|
2496
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
|
+
|
|
2497
2693
|
// src/emit-auth.ts
|
|
2498
2694
|
function emitAuth(cfgAuth) {
|
|
2499
2695
|
const strategy = cfgAuth?.strategy ?? "none";
|
|
@@ -2908,19 +3104,124 @@ export async function getByPk(
|
|
|
2908
3104
|
*/
|
|
2909
3105
|
export async function listRecords(
|
|
2910
3106
|
ctx: OperationContext,
|
|
2911
|
-
params: { limit?: number; offset?: number; include?: any }
|
|
2912
|
-
): Promise<{ data?: any; error?: string; issues?: any;
|
|
3107
|
+
params: { where?: any; limit?: number; offset?: number; include?: any }
|
|
3108
|
+
): Promise<{ data?: any; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
2913
3109
|
try {
|
|
2914
|
-
const { limit = 50, offset = 0, include } = params;
|
|
3110
|
+
const { where: whereClause, limit = 50, offset = 0, include } = params;
|
|
3111
|
+
|
|
3112
|
+
// Build WHERE clause
|
|
3113
|
+
const whereParts: string[] = [];
|
|
3114
|
+
const whereParams: any[] = [];
|
|
3115
|
+
let paramIndex = 1;
|
|
3116
|
+
|
|
3117
|
+
// Add soft delete filter if applicable
|
|
3118
|
+
if (ctx.softDeleteColumn) {
|
|
3119
|
+
whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// Add user-provided where conditions
|
|
3123
|
+
if (whereClause && typeof whereClause === 'object') {
|
|
3124
|
+
for (const [key, value] of Object.entries(whereClause)) {
|
|
3125
|
+
if (value === undefined) {
|
|
3126
|
+
// Skip undefined values
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// Handle operator objects like { $gt: 5, $like: "%test%" }
|
|
3131
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
3132
|
+
for (const [op, opValue] of Object.entries(value)) {
|
|
3133
|
+
if (opValue === undefined) continue;
|
|
3134
|
+
|
|
3135
|
+
switch (op) {
|
|
3136
|
+
case '$eq':
|
|
3137
|
+
whereParts.push(\`"\${key}" = $\${paramIndex}\`);
|
|
3138
|
+
whereParams.push(opValue);
|
|
3139
|
+
paramIndex++;
|
|
3140
|
+
break;
|
|
3141
|
+
case '$ne':
|
|
3142
|
+
whereParts.push(\`"\${key}" != $\${paramIndex}\`);
|
|
3143
|
+
whereParams.push(opValue);
|
|
3144
|
+
paramIndex++;
|
|
3145
|
+
break;
|
|
3146
|
+
case '$gt':
|
|
3147
|
+
whereParts.push(\`"\${key}" > $\${paramIndex}\`);
|
|
3148
|
+
whereParams.push(opValue);
|
|
3149
|
+
paramIndex++;
|
|
3150
|
+
break;
|
|
3151
|
+
case '$gte':
|
|
3152
|
+
whereParts.push(\`"\${key}" >= $\${paramIndex}\`);
|
|
3153
|
+
whereParams.push(opValue);
|
|
3154
|
+
paramIndex++;
|
|
3155
|
+
break;
|
|
3156
|
+
case '$lt':
|
|
3157
|
+
whereParts.push(\`"\${key}" < $\${paramIndex}\`);
|
|
3158
|
+
whereParams.push(opValue);
|
|
3159
|
+
paramIndex++;
|
|
3160
|
+
break;
|
|
3161
|
+
case '$lte':
|
|
3162
|
+
whereParts.push(\`"\${key}" <= $\${paramIndex}\`);
|
|
3163
|
+
whereParams.push(opValue);
|
|
3164
|
+
paramIndex++;
|
|
3165
|
+
break;
|
|
3166
|
+
case '$in':
|
|
3167
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
3168
|
+
whereParts.push(\`"\${key}" = ANY($\${paramIndex})\`);
|
|
3169
|
+
whereParams.push(opValue);
|
|
3170
|
+
paramIndex++;
|
|
3171
|
+
}
|
|
3172
|
+
break;
|
|
3173
|
+
case '$nin':
|
|
3174
|
+
if (Array.isArray(opValue) && opValue.length > 0) {
|
|
3175
|
+
whereParts.push(\`"\${key}" != ALL($\${paramIndex})\`);
|
|
3176
|
+
whereParams.push(opValue);
|
|
3177
|
+
paramIndex++;
|
|
3178
|
+
}
|
|
3179
|
+
break;
|
|
3180
|
+
case '$like':
|
|
3181
|
+
whereParts.push(\`"\${key}" LIKE $\${paramIndex}\`);
|
|
3182
|
+
whereParams.push(opValue);
|
|
3183
|
+
paramIndex++;
|
|
3184
|
+
break;
|
|
3185
|
+
case '$ilike':
|
|
3186
|
+
whereParts.push(\`"\${key}" ILIKE $\${paramIndex}\`);
|
|
3187
|
+
whereParams.push(opValue);
|
|
3188
|
+
paramIndex++;
|
|
3189
|
+
break;
|
|
3190
|
+
case '$is':
|
|
3191
|
+
if (opValue === null) {
|
|
3192
|
+
whereParts.push(\`"\${key}" IS NULL\`);
|
|
3193
|
+
}
|
|
3194
|
+
break;
|
|
3195
|
+
case '$isNot':
|
|
3196
|
+
if (opValue === null) {
|
|
3197
|
+
whereParts.push(\`"\${key}" IS NOT NULL\`);
|
|
3198
|
+
}
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
} else if (value === null) {
|
|
3203
|
+
// Direct null value
|
|
3204
|
+
whereParts.push(\`"\${key}" IS NULL\`);
|
|
3205
|
+
} else {
|
|
3206
|
+
// Direct value (simple equality)
|
|
3207
|
+
whereParts.push(\`"\${key}" = $\${paramIndex}\`);
|
|
3208
|
+
whereParams.push(value);
|
|
3209
|
+
paramIndex++;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
2915
3215
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3216
|
+
// Add limit and offset params
|
|
3217
|
+
const limitParam = \`$\${paramIndex}\`;
|
|
3218
|
+
const offsetParam = \`$\${paramIndex + 1}\`;
|
|
3219
|
+
const allParams = [...whereParams, limit, offset];
|
|
2919
3220
|
|
|
2920
|
-
const text = \`SELECT * FROM "\${ctx.table}" \${
|
|
2921
|
-
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:",
|
|
3221
|
+
const text = \`SELECT * FROM "\${ctx.table}" \${whereSQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
3222
|
+
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
2922
3223
|
|
|
2923
|
-
const { rows } = await ctx.pg.query(text,
|
|
3224
|
+
const { rows } = await ctx.pg.query(text, allParams);
|
|
2924
3225
|
|
|
2925
3226
|
if (!include) {
|
|
2926
3227
|
log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
|
|
@@ -3838,6 +4139,7 @@ async function generate(configPath) {
|
|
|
3838
4139
|
files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
|
|
3839
4140
|
files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
3840
4141
|
files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
4142
|
+
files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
3841
4143
|
files.push({
|
|
3842
4144
|
path: join(serverDir, "include-builder.ts"),
|
|
3843
4145
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.
|
|
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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
25
|
-
"test": "bun test:init && bun test:gen && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
|
|
25
|
+
"test": "bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test:gen-with-tests && bun test:pull && bun test:typecheck && bun test:drizzle-e2e",
|
|
26
26
|
"test:init": "bun test/test-init.ts",
|
|
27
27
|
"test:gen": "bun test/test-gen.ts",
|
|
28
28
|
"test:gen-with-tests": "bun test/test-gen-with-tests.ts",
|