@expo/entity-database-adapter-knex 0.58.0 → 0.60.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/build/src/SQLOperator.d.ts +135 -3
- package/build/src/SQLOperator.js +293 -37
- package/build/src/SQLOperator.js.map +1 -1
- package/build/src/internal/EntityKnexDataManager.js +2 -2
- package/build/src/internal/EntityKnexDataManager.js.map +1 -1
- package/package.json +4 -4
- package/src/SQLOperator.ts +352 -38
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +27 -0
- package/src/__tests__/SQLOperator-test.ts +428 -4
- package/src/internal/EntityKnexDataManager.ts +2 -2
|
@@ -85,6 +85,15 @@ export declare class SQLEntityField<TFields extends Record<string, any>> {
|
|
|
85
85
|
readonly fieldName: keyof TFields;
|
|
86
86
|
constructor(fieldName: keyof TFields);
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Helper for passing an array as a single bound parameter (e.g. for PostgreSQL's = ANY(?)).
|
|
90
|
+
* Unlike bare arrays interpolated in the sql template (which expand to (?, ?, ?) for IN clauses),
|
|
91
|
+
* this binds the entire array as one parameter, letting knex handle the array encoding.
|
|
92
|
+
*/
|
|
93
|
+
export declare class SQLArrayValue {
|
|
94
|
+
readonly values: readonly SupportedSQLValue[];
|
|
95
|
+
constructor(values: readonly SupportedSQLValue[]);
|
|
96
|
+
}
|
|
88
97
|
/**
|
|
89
98
|
* Helper for raw SQL that should not be parameterized
|
|
90
99
|
* WARNING: Only use this with trusted input to avoid SQL injection
|
|
@@ -111,6 +120,18 @@ export declare function identifier(name: string): SQLIdentifier;
|
|
|
111
120
|
* @param fieldName - The entity field name to reference.
|
|
112
121
|
*/
|
|
113
122
|
export declare function entityField<TFields extends Record<string, any>>(fieldName: keyof TFields): SQLEntityField<TFields>;
|
|
123
|
+
/**
|
|
124
|
+
* Wrap an array so it is bound as a single parameter rather than expanded for IN clauses.
|
|
125
|
+
* Generates PostgreSQL's = ANY(?) syntax.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* const statuses = ['active', 'pending'];
|
|
130
|
+
* const query = sql`${entityField('status')} = ANY(${arrayValue(statuses)})`;
|
|
131
|
+
* // Generates: ?? = ANY(?) with the array bound as a single parameter
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export declare function arrayValue(values: readonly SupportedSQLValue[]): SQLArrayValue;
|
|
114
135
|
/**
|
|
115
136
|
* Insert raw SQL that will not be parameterized
|
|
116
137
|
* WARNING: This bypasses SQL injection protection. Only use with trusted input.
|
|
@@ -126,6 +147,17 @@ export declare function entityField<TFields extends Record<string, any>>(fieldNa
|
|
|
126
147
|
* ```
|
|
127
148
|
*/
|
|
128
149
|
export declare function unsafeRaw(sqlString: string): SQLUnsafeRaw;
|
|
150
|
+
/**
|
|
151
|
+
* Wraps a SQLFragment or entity field name into an SQLExpression for fluent comparison usage.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* expression<MyFields>('age').gte(18)
|
|
156
|
+
* expression<MyFields>('name').ilike('%john%')
|
|
157
|
+
* expression(sql`${entityField('status')}`).eq('active')
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export declare function expression<TFields extends Record<string, any>>(fragmentOrFieldName: SQLFragment<TFields> | keyof TFields): SQLExpression<TFields>;
|
|
129
161
|
/**
|
|
130
162
|
* Tagged template literal function for SQL queries
|
|
131
163
|
*
|
|
@@ -135,7 +167,7 @@ export declare function unsafeRaw(sqlString: string): SQLUnsafeRaw;
|
|
|
135
167
|
* const query = sql`age >= ${age} AND status = ${'active'}`;
|
|
136
168
|
* ```
|
|
137
169
|
*/
|
|
138
|
-
export declare function sql<TFields extends Record<string, any>>(strings: TemplateStringsArray, ...values: readonly (SupportedSQLValue | SQLFragment<TFields> | SQLIdentifier | SQLUnsafeRaw | SQLEntityField<TFields>)[]): SQLFragment<TFields>;
|
|
170
|
+
export declare function sql<TFields extends Record<string, any>>(strings: TemplateStringsArray, ...values: readonly (SupportedSQLValue | SQLFragment<TFields> | SQLIdentifier | SQLUnsafeRaw | SQLEntityField<TFields> | SQLArrayValue)[]): SQLFragment<TFields>;
|
|
139
171
|
type PickSupportedSQLValueKeys<T> = {
|
|
140
172
|
[K in keyof T]: T[K] extends SupportedSQLValue ? K : never;
|
|
141
173
|
}[keyof T];
|
|
@@ -145,6 +177,41 @@ type PickStringValueKeys<T> = {
|
|
|
145
177
|
type JsonSerializable = string | number | boolean | null | undefined | readonly JsonSerializable[] | {
|
|
146
178
|
readonly [key: string]: JsonSerializable;
|
|
147
179
|
};
|
|
180
|
+
/**
|
|
181
|
+
* An SQL expression that supports fluent comparison methods.
|
|
182
|
+
* Extends SQLFragment so it can be used anywhere a SQLFragment is accepted.
|
|
183
|
+
* The fluent methods return plain SQLFragment instances since they produce
|
|
184
|
+
* complete conditions, not further chainable expressions.
|
|
185
|
+
*/
|
|
186
|
+
export declare class SQLExpression<TFields extends Record<string, any>> extends SQLFragment<TFields> {
|
|
187
|
+
eq(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
188
|
+
neq(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
189
|
+
gt(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
190
|
+
gte(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
191
|
+
lt(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
192
|
+
lte(value: SupportedSQLValue): SQLFragment<TFields>;
|
|
193
|
+
isNull(): SQLFragment<TFields>;
|
|
194
|
+
isNotNull(): SQLFragment<TFields>;
|
|
195
|
+
like(pattern: string): SQLFragment<TFields>;
|
|
196
|
+
notLike(pattern: string): SQLFragment<TFields>;
|
|
197
|
+
ilike(pattern: string): SQLFragment<TFields>;
|
|
198
|
+
notIlike(pattern: string): SQLFragment<TFields>;
|
|
199
|
+
inArray(values: readonly SupportedSQLValue[]): SQLFragment<TFields>;
|
|
200
|
+
notInArray(values: readonly SupportedSQLValue[]): SQLFragment<TFields>;
|
|
201
|
+
anyArray(values: readonly SupportedSQLValue[]): SQLFragment<TFields>;
|
|
202
|
+
between(min: SupportedSQLValue, max: SupportedSQLValue): SQLFragment<TFields>;
|
|
203
|
+
notBetween(min: SupportedSQLValue, max: SupportedSQLValue): SQLFragment<TFields>;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Allowed PostgreSQL type names for the cast() helper.
|
|
207
|
+
* Only these types can be used to prevent SQL injection through type name interpolation.
|
|
208
|
+
*/
|
|
209
|
+
declare const ALLOWED_CAST_TYPES: readonly ["int", "integer", "int2", "int4", "int8", "smallint", "bigint", "numeric", "decimal", "real", "double precision", "float", "float4", "float8", "text", "varchar", "char", "character varying", "boolean", "bool", "date", "time", "timestamp", "timestamptz", "interval", "json", "jsonb", "uuid", "bytea"];
|
|
210
|
+
/**
|
|
211
|
+
* Allowed PostgreSQL type names for the cast() helper.
|
|
212
|
+
* Only these types can be used to prevent SQL injection through type name interpolation.
|
|
213
|
+
*/
|
|
214
|
+
export type PostgresCastType = (typeof ALLOWED_CAST_TYPES)[number];
|
|
148
215
|
/**
|
|
149
216
|
* Common SQL helper functions for building queries
|
|
150
217
|
*/
|
|
@@ -159,6 +226,18 @@ export declare const SQLFragmentHelpers: {
|
|
|
159
226
|
* ```
|
|
160
227
|
*/
|
|
161
228
|
inArray<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(fieldName: N, values: readonly TFields[N][]): SQLFragment<TFields>;
|
|
229
|
+
/**
|
|
230
|
+
* = ANY() clause helper. Binds the array as a single parameter instead of expanding it.
|
|
231
|
+
* Semantically equivalent to IN for most cases, but retains a consistent query shape for
|
|
232
|
+
* query metrics.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* const query = SQLFragmentHelpers.anyArray('status', ['active', 'pending']);
|
|
237
|
+
* // Generates: ?? = ANY(?) with entityField binding for 'status' and a single array value binding
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
anyArray<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(fieldName: N, values: readonly TFields[N][]): SQLFragment<TFields>;
|
|
162
241
|
/**
|
|
163
242
|
* NOT IN clause helper
|
|
164
243
|
*/
|
|
@@ -241,12 +320,65 @@ export declare const SQLFragmentHelpers: {
|
|
|
241
320
|
jsonContainedBy<TFields extends Record<string, any>>(fieldName: keyof TFields, value: JsonSerializable): SQLFragment<TFields>;
|
|
242
321
|
/**
|
|
243
322
|
* JSON path extraction helper (-\>)
|
|
323
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
244
324
|
*/
|
|
245
|
-
jsonPath<TFields extends Record<string, any>>(fieldName: keyof TFields, path: string):
|
|
325
|
+
jsonPath<TFields extends Record<string, any>>(fieldName: keyof TFields, path: string): SQLExpression<TFields>;
|
|
246
326
|
/**
|
|
247
327
|
* JSON path text extraction helper (-\>\>)
|
|
328
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
329
|
+
*/
|
|
330
|
+
jsonPathText<TFields extends Record<string, any>>(fieldName: keyof TFields, path: string): SQLExpression<TFields>;
|
|
331
|
+
/**
|
|
332
|
+
* JSON deep path extraction helper (#\>)
|
|
333
|
+
* Extracts a JSON sub-object at the specified key path, returning jsonb.
|
|
334
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
335
|
+
*
|
|
336
|
+
* @param fieldName - The entity field containing JSON/JSONB data
|
|
337
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
338
|
+
*/
|
|
339
|
+
jsonDeepPath<TFields extends Record<string, any>>(fieldName: keyof TFields, path: readonly string[]): SQLExpression<TFields>;
|
|
340
|
+
/**
|
|
341
|
+
* JSON deep path text extraction helper (#\>\>)
|
|
342
|
+
* Extracts a JSON sub-object at the specified key path as text.
|
|
343
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
344
|
+
*
|
|
345
|
+
* @param fieldName - The entity field containing JSON/JSONB data
|
|
346
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
347
|
+
*/
|
|
348
|
+
jsonDeepPathText<TFields extends Record<string, any>>(fieldName: keyof TFields, path: readonly string[]): SQLExpression<TFields>;
|
|
349
|
+
/**
|
|
350
|
+
* SQL type cast helper (::type)
|
|
351
|
+
* Casts an expression to a PostgreSQL type.
|
|
352
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
353
|
+
*
|
|
354
|
+
* @param fragment - An SQLFragment or SQLExpression to cast
|
|
355
|
+
* @param typeName - The PostgreSQL type name (e.g., 'int', 'text', 'timestamptz')
|
|
356
|
+
*/
|
|
357
|
+
cast<TFields extends Record<string, any>>(fragmentOrExpression: SQLFragment<TFields>, typeName: PostgresCastType): SQLExpression<TFields>;
|
|
358
|
+
/**
|
|
359
|
+
* COALESCE helper
|
|
360
|
+
* Returns the first non-null value from the given expressions/values.
|
|
361
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
362
|
+
*/
|
|
363
|
+
coalesce<TFields extends Record<string, any>>(...args: readonly (SQLFragment<TFields> | SupportedSQLValue)[]): SQLExpression<TFields>;
|
|
364
|
+
/**
|
|
365
|
+
* LOWER helper
|
|
366
|
+
* Converts a string expression to lowercase.
|
|
367
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
368
|
+
*/
|
|
369
|
+
lower<TFields extends Record<string, any>>(expressionOrFieldName: SQLFragment<TFields> | keyof TFields): SQLExpression<TFields>;
|
|
370
|
+
/**
|
|
371
|
+
* UPPER helper
|
|
372
|
+
* Converts a string expression to uppercase.
|
|
373
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
374
|
+
*/
|
|
375
|
+
upper<TFields extends Record<string, any>>(expressionOrFieldName: SQLFragment<TFields> | keyof TFields): SQLExpression<TFields>;
|
|
376
|
+
/**
|
|
377
|
+
* TRIM helper
|
|
378
|
+
* Removes leading and trailing whitespace from a string expression.
|
|
379
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
248
380
|
*/
|
|
249
|
-
|
|
381
|
+
trim<TFields extends Record<string, any>>(expressionOrFieldName: SQLFragment<TFields> | keyof TFields): SQLExpression<TFields>;
|
|
250
382
|
/**
|
|
251
383
|
* Logical AND of multiple fragments
|
|
252
384
|
*/
|
package/build/src/SQLOperator.js
CHANGED
|
@@ -3,10 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SQLFragmentHelpers = exports.SQLUnsafeRaw = exports.SQLEntityField = exports.SQLIdentifier = exports.SQLFragment = void 0;
|
|
6
|
+
exports.SQLFragmentHelpers = exports.SQLExpression = exports.SQLUnsafeRaw = exports.SQLArrayValue = exports.SQLEntityField = exports.SQLIdentifier = exports.SQLFragment = void 0;
|
|
7
7
|
exports.identifier = identifier;
|
|
8
8
|
exports.entityField = entityField;
|
|
9
|
+
exports.arrayValue = arrayValue;
|
|
9
10
|
exports.unsafeRaw = unsafeRaw;
|
|
11
|
+
exports.expression = expression;
|
|
10
12
|
exports.sql = sql;
|
|
11
13
|
const assert_1 = __importDefault(require("assert"));
|
|
12
14
|
/**
|
|
@@ -175,6 +177,18 @@ class SQLEntityField {
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
exports.SQLEntityField = SQLEntityField;
|
|
180
|
+
/**
|
|
181
|
+
* Helper for passing an array as a single bound parameter (e.g. for PostgreSQL's = ANY(?)).
|
|
182
|
+
* Unlike bare arrays interpolated in the sql template (which expand to (?, ?, ?) for IN clauses),
|
|
183
|
+
* this binds the entire array as one parameter, letting knex handle the array encoding.
|
|
184
|
+
*/
|
|
185
|
+
class SQLArrayValue {
|
|
186
|
+
values;
|
|
187
|
+
constructor(values) {
|
|
188
|
+
this.values = values;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.SQLArrayValue = SQLArrayValue;
|
|
178
192
|
/**
|
|
179
193
|
* Helper for raw SQL that should not be parameterized
|
|
180
194
|
* WARNING: Only use this with trusted input to avoid SQL injection
|
|
@@ -208,6 +222,20 @@ function identifier(name) {
|
|
|
208
222
|
function entityField(fieldName) {
|
|
209
223
|
return new SQLEntityField(fieldName);
|
|
210
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Wrap an array so it is bound as a single parameter rather than expanded for IN clauses.
|
|
227
|
+
* Generates PostgreSQL's = ANY(?) syntax.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const statuses = ['active', 'pending'];
|
|
232
|
+
* const query = sql`${entityField('status')} = ANY(${arrayValue(statuses)})`;
|
|
233
|
+
* // Generates: ?? = ANY(?) with the array bound as a single parameter
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function arrayValue(values) {
|
|
237
|
+
return new SQLArrayValue(values);
|
|
238
|
+
}
|
|
211
239
|
/**
|
|
212
240
|
* Insert raw SQL that will not be parameterized
|
|
213
241
|
* WARNING: This bypasses SQL injection protection. Only use with trusted input.
|
|
@@ -225,6 +253,23 @@ function entityField(fieldName) {
|
|
|
225
253
|
function unsafeRaw(sqlString) {
|
|
226
254
|
return new SQLUnsafeRaw(sqlString);
|
|
227
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Wraps a SQLFragment or entity field name into an SQLExpression for fluent comparison usage.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* expression<MyFields>('age').gte(18)
|
|
262
|
+
* expression<MyFields>('name').ilike('%john%')
|
|
263
|
+
* expression(sql`${entityField('status')}`).eq('active')
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function expression(fragmentOrFieldName) {
|
|
267
|
+
if (fragmentOrFieldName instanceof SQLFragment) {
|
|
268
|
+
return new SQLExpression(fragmentOrFieldName.sql, fragmentOrFieldName.bindings);
|
|
269
|
+
}
|
|
270
|
+
const fragment = sql `${entityField(fragmentOrFieldName)}`;
|
|
271
|
+
return new SQLExpression(fragment.sql, fragment.bindings);
|
|
272
|
+
}
|
|
228
273
|
/**
|
|
229
274
|
* Tagged template literal function for SQL queries
|
|
230
275
|
*
|
|
@@ -256,6 +301,11 @@ function sql(strings, ...values) {
|
|
|
256
301
|
sqlString += '??';
|
|
257
302
|
bindings.push({ type: 'entityField', fieldName: value.fieldName });
|
|
258
303
|
}
|
|
304
|
+
else if (value instanceof SQLArrayValue) {
|
|
305
|
+
// Handle array as a single bound parameter (for = ANY(?), etc.)
|
|
306
|
+
sqlString += '?';
|
|
307
|
+
bindings.push({ type: 'value', value: value.values });
|
|
308
|
+
}
|
|
259
309
|
else if (value instanceof SQLUnsafeRaw) {
|
|
260
310
|
// Handle raw SQL (WARNING: no parameterization)
|
|
261
311
|
sqlString += value.rawSql;
|
|
@@ -274,6 +324,117 @@ function sql(strings, ...values) {
|
|
|
274
324
|
});
|
|
275
325
|
return new SQLFragment(sqlString, bindings);
|
|
276
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* An SQL expression that supports fluent comparison methods.
|
|
329
|
+
* Extends SQLFragment so it can be used anywhere a SQLFragment is accepted.
|
|
330
|
+
* The fluent methods return plain SQLFragment instances since they produce
|
|
331
|
+
* complete conditions, not further chainable expressions.
|
|
332
|
+
*/
|
|
333
|
+
class SQLExpression extends SQLFragment {
|
|
334
|
+
eq(value) {
|
|
335
|
+
if (value === null || value === undefined) {
|
|
336
|
+
return this.isNull();
|
|
337
|
+
}
|
|
338
|
+
return sql `${this} = ${value}`;
|
|
339
|
+
}
|
|
340
|
+
neq(value) {
|
|
341
|
+
if (value === null || value === undefined) {
|
|
342
|
+
return this.isNotNull();
|
|
343
|
+
}
|
|
344
|
+
return sql `${this} != ${value}`;
|
|
345
|
+
}
|
|
346
|
+
gt(value) {
|
|
347
|
+
return sql `${this} > ${value}`;
|
|
348
|
+
}
|
|
349
|
+
gte(value) {
|
|
350
|
+
return sql `${this} >= ${value}`;
|
|
351
|
+
}
|
|
352
|
+
lt(value) {
|
|
353
|
+
return sql `${this} < ${value}`;
|
|
354
|
+
}
|
|
355
|
+
lte(value) {
|
|
356
|
+
return sql `${this} <= ${value}`;
|
|
357
|
+
}
|
|
358
|
+
isNull() {
|
|
359
|
+
return sql `${this} IS NULL`;
|
|
360
|
+
}
|
|
361
|
+
isNotNull() {
|
|
362
|
+
return sql `${this} IS NOT NULL`;
|
|
363
|
+
}
|
|
364
|
+
like(pattern) {
|
|
365
|
+
return sql `${this} LIKE ${pattern}`;
|
|
366
|
+
}
|
|
367
|
+
notLike(pattern) {
|
|
368
|
+
return sql `${this} NOT LIKE ${pattern}`;
|
|
369
|
+
}
|
|
370
|
+
ilike(pattern) {
|
|
371
|
+
return sql `${this} ILIKE ${pattern}`;
|
|
372
|
+
}
|
|
373
|
+
notIlike(pattern) {
|
|
374
|
+
return sql `${this} NOT ILIKE ${pattern}`;
|
|
375
|
+
}
|
|
376
|
+
inArray(values) {
|
|
377
|
+
if (values.length === 0) {
|
|
378
|
+
return sql `FALSE`;
|
|
379
|
+
}
|
|
380
|
+
return sql `${this} IN ${values}`;
|
|
381
|
+
}
|
|
382
|
+
notInArray(values) {
|
|
383
|
+
if (values.length === 0) {
|
|
384
|
+
return sql `TRUE`;
|
|
385
|
+
}
|
|
386
|
+
return sql `${this} NOT IN ${values}`;
|
|
387
|
+
}
|
|
388
|
+
anyArray(values) {
|
|
389
|
+
if (values.length === 0) {
|
|
390
|
+
return sql `FALSE`;
|
|
391
|
+
}
|
|
392
|
+
return sql `${this} = ANY(${arrayValue(values)})`;
|
|
393
|
+
}
|
|
394
|
+
between(min, max) {
|
|
395
|
+
return sql `${this} BETWEEN ${min} AND ${max}`;
|
|
396
|
+
}
|
|
397
|
+
notBetween(min, max) {
|
|
398
|
+
return sql `${this} NOT BETWEEN ${min} AND ${max}`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.SQLExpression = SQLExpression;
|
|
402
|
+
/**
|
|
403
|
+
* Allowed PostgreSQL type names for the cast() helper.
|
|
404
|
+
* Only these types can be used to prevent SQL injection through type name interpolation.
|
|
405
|
+
*/
|
|
406
|
+
const ALLOWED_CAST_TYPES = [
|
|
407
|
+
'int',
|
|
408
|
+
'integer',
|
|
409
|
+
'int2',
|
|
410
|
+
'int4',
|
|
411
|
+
'int8',
|
|
412
|
+
'smallint',
|
|
413
|
+
'bigint',
|
|
414
|
+
'numeric',
|
|
415
|
+
'decimal',
|
|
416
|
+
'real',
|
|
417
|
+
'double precision',
|
|
418
|
+
'float',
|
|
419
|
+
'float4',
|
|
420
|
+
'float8',
|
|
421
|
+
'text',
|
|
422
|
+
'varchar',
|
|
423
|
+
'char',
|
|
424
|
+
'character varying',
|
|
425
|
+
'boolean',
|
|
426
|
+
'bool',
|
|
427
|
+
'date',
|
|
428
|
+
'time',
|
|
429
|
+
'timestamp',
|
|
430
|
+
'timestamptz',
|
|
431
|
+
'interval',
|
|
432
|
+
'json',
|
|
433
|
+
'jsonb',
|
|
434
|
+
'uuid',
|
|
435
|
+
'bytea',
|
|
436
|
+
];
|
|
437
|
+
const ALLOWED_CAST_TYPES_SET = new Set(ALLOWED_CAST_TYPES);
|
|
277
438
|
/**
|
|
278
439
|
* Common SQL helper functions for building queries
|
|
279
440
|
*/
|
|
@@ -288,21 +449,27 @@ exports.SQLFragmentHelpers = {
|
|
|
288
449
|
* ```
|
|
289
450
|
*/
|
|
290
451
|
inArray(fieldName, values) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
452
|
+
return expression(fieldName).inArray(values);
|
|
453
|
+
},
|
|
454
|
+
/**
|
|
455
|
+
* = ANY() clause helper. Binds the array as a single parameter instead of expanding it.
|
|
456
|
+
* Semantically equivalent to IN for most cases, but retains a consistent query shape for
|
|
457
|
+
* query metrics.
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```ts
|
|
461
|
+
* const query = SQLFragmentHelpers.anyArray('status', ['active', 'pending']);
|
|
462
|
+
* // Generates: ?? = ANY(?) with entityField binding for 'status' and a single array value binding
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
anyArray(fieldName, values) {
|
|
466
|
+
return expression(fieldName).anyArray(values);
|
|
296
467
|
},
|
|
297
468
|
/**
|
|
298
469
|
* NOT IN clause helper
|
|
299
470
|
*/
|
|
300
471
|
notInArray(fieldName, values) {
|
|
301
|
-
|
|
302
|
-
// Handle empty array case - always true
|
|
303
|
-
return sql `1 = 1`;
|
|
304
|
-
}
|
|
305
|
-
return sql `${entityField(fieldName)} NOT IN ${values}`;
|
|
472
|
+
return expression(fieldName).notInArray(values);
|
|
306
473
|
},
|
|
307
474
|
/**
|
|
308
475
|
* BETWEEN helper
|
|
@@ -314,13 +481,13 @@ exports.SQLFragmentHelpers = {
|
|
|
314
481
|
* ```
|
|
315
482
|
*/
|
|
316
483
|
between(fieldName, min, max) {
|
|
317
|
-
return
|
|
484
|
+
return expression(fieldName).between(min, max);
|
|
318
485
|
},
|
|
319
486
|
/**
|
|
320
487
|
* NOT BETWEEN helper
|
|
321
488
|
*/
|
|
322
489
|
notBetween(fieldName, min, max) {
|
|
323
|
-
return
|
|
490
|
+
return expression(fieldName).notBetween(min, max);
|
|
324
491
|
},
|
|
325
492
|
/**
|
|
326
493
|
* LIKE helper with automatic escaping
|
|
@@ -332,79 +499,73 @@ exports.SQLFragmentHelpers = {
|
|
|
332
499
|
* ```
|
|
333
500
|
*/
|
|
334
501
|
like(fieldName, pattern) {
|
|
335
|
-
return
|
|
502
|
+
return expression(fieldName).like(pattern);
|
|
336
503
|
},
|
|
337
504
|
/**
|
|
338
505
|
* NOT LIKE helper
|
|
339
506
|
*/
|
|
340
507
|
notLike(fieldName, pattern) {
|
|
341
|
-
return
|
|
508
|
+
return expression(fieldName).notLike(pattern);
|
|
342
509
|
},
|
|
343
510
|
/**
|
|
344
511
|
* ILIKE helper for case-insensitive matching
|
|
345
512
|
*/
|
|
346
513
|
ilike(fieldName, pattern) {
|
|
347
|
-
return
|
|
514
|
+
return expression(fieldName).ilike(pattern);
|
|
348
515
|
},
|
|
349
516
|
/**
|
|
350
517
|
* NOT ILIKE helper for case-insensitive non-matching
|
|
351
518
|
*/
|
|
352
519
|
notIlike(fieldName, pattern) {
|
|
353
|
-
return
|
|
520
|
+
return expression(fieldName).notIlike(pattern);
|
|
354
521
|
},
|
|
355
522
|
/**
|
|
356
523
|
* NULL check helper
|
|
357
524
|
*/
|
|
358
525
|
isNull(fieldName) {
|
|
359
|
-
return
|
|
526
|
+
return expression(fieldName).isNull();
|
|
360
527
|
},
|
|
361
528
|
/**
|
|
362
529
|
* NOT NULL check helper
|
|
363
530
|
*/
|
|
364
531
|
isNotNull(fieldName) {
|
|
365
|
-
return
|
|
532
|
+
return expression(fieldName).isNotNull();
|
|
366
533
|
},
|
|
367
534
|
/**
|
|
368
535
|
* Single-equals-equality operator
|
|
369
536
|
*/
|
|
370
537
|
eq(fieldName, value) {
|
|
371
|
-
|
|
372
|
-
return exports.SQLFragmentHelpers.isNull(fieldName);
|
|
373
|
-
}
|
|
374
|
-
return sql `${entityField(fieldName)} = ${value}`;
|
|
538
|
+
return expression(fieldName).eq(value);
|
|
375
539
|
},
|
|
376
540
|
/**
|
|
377
541
|
* Single-equals-inequality operator
|
|
378
542
|
*/
|
|
379
543
|
neq(fieldName, value) {
|
|
380
|
-
|
|
381
|
-
return exports.SQLFragmentHelpers.isNotNull(fieldName);
|
|
382
|
-
}
|
|
383
|
-
return sql `${entityField(fieldName)} != ${value}`;
|
|
544
|
+
return expression(fieldName).neq(value);
|
|
384
545
|
},
|
|
385
546
|
/**
|
|
386
547
|
* Greater-than comparison operator
|
|
387
548
|
*/
|
|
388
549
|
gt(fieldName, value) {
|
|
389
|
-
return
|
|
550
|
+
return expression(fieldName).gt(value);
|
|
390
551
|
},
|
|
391
552
|
/**
|
|
392
553
|
* Greater-than-or-equal-to comparison operator
|
|
393
554
|
*/
|
|
394
555
|
gte(fieldName, value) {
|
|
395
|
-
return
|
|
556
|
+
return expression(fieldName).gte(value);
|
|
396
557
|
},
|
|
397
558
|
/**
|
|
398
559
|
* Less-than comparison operator
|
|
399
560
|
*/
|
|
400
561
|
lt(fieldName, value) {
|
|
401
|
-
return
|
|
562
|
+
return expression(fieldName).lt(value);
|
|
402
563
|
},
|
|
403
564
|
/**
|
|
404
565
|
* Less-than-or-equal-to comparison operator
|
|
405
566
|
*/
|
|
406
567
|
lte(fieldName, value) {
|
|
407
|
-
return
|
|
568
|
+
return expression(fieldName).lte(value);
|
|
408
569
|
},
|
|
409
570
|
/**
|
|
410
571
|
* JSON contains operator (\@\>)
|
|
@@ -426,22 +587,108 @@ exports.SQLFragmentHelpers = {
|
|
|
426
587
|
},
|
|
427
588
|
/**
|
|
428
589
|
* JSON path extraction helper (-\>)
|
|
590
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
429
591
|
*/
|
|
430
592
|
jsonPath(fieldName, path) {
|
|
431
|
-
return sql `${entityField(fieldName)}->${path}
|
|
593
|
+
return expression(sql `${entityField(fieldName)}->${path}`);
|
|
432
594
|
},
|
|
433
595
|
/**
|
|
434
596
|
* JSON path text extraction helper (-\>\>)
|
|
597
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
435
598
|
*/
|
|
436
599
|
jsonPathText(fieldName, path) {
|
|
437
|
-
return sql `${entityField(fieldName)}->>${path}
|
|
600
|
+
return expression(sql `${entityField(fieldName)}->>${path}`);
|
|
601
|
+
},
|
|
602
|
+
/**
|
|
603
|
+
* JSON deep path extraction helper (#\>)
|
|
604
|
+
* Extracts a JSON sub-object at the specified key path, returning jsonb.
|
|
605
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
606
|
+
*
|
|
607
|
+
* @param fieldName - The entity field containing JSON/JSONB data
|
|
608
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
609
|
+
*/
|
|
610
|
+
jsonDeepPath(fieldName, path) {
|
|
611
|
+
const pathLiteral = `{${path.map(quotePostgresArrayElement).join(',')}}`;
|
|
612
|
+
return expression(sql `${entityField(fieldName)} #> ${pathLiteral}`);
|
|
613
|
+
},
|
|
614
|
+
/**
|
|
615
|
+
* JSON deep path text extraction helper (#\>\>)
|
|
616
|
+
* Extracts a JSON sub-object at the specified key path as text.
|
|
617
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
618
|
+
*
|
|
619
|
+
* @param fieldName - The entity field containing JSON/JSONB data
|
|
620
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
621
|
+
*/
|
|
622
|
+
jsonDeepPathText(fieldName, path) {
|
|
623
|
+
const pathLiteral = `{${path.map(quotePostgresArrayElement).join(',')}}`;
|
|
624
|
+
return expression(sql `${entityField(fieldName)} #>> ${pathLiteral}`);
|
|
625
|
+
},
|
|
626
|
+
/**
|
|
627
|
+
* SQL type cast helper (::type)
|
|
628
|
+
* Casts an expression to a PostgreSQL type.
|
|
629
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
630
|
+
*
|
|
631
|
+
* @param fragment - An SQLFragment or SQLExpression to cast
|
|
632
|
+
* @param typeName - The PostgreSQL type name (e.g., 'int', 'text', 'timestamptz')
|
|
633
|
+
*/
|
|
634
|
+
cast(fragmentOrExpression, typeName) {
|
|
635
|
+
(0, assert_1.default)(ALLOWED_CAST_TYPES_SET.has(typeName), `cast: unsupported type name "${typeName}". Allowed types: ${[...ALLOWED_CAST_TYPES_SET].join(', ')}`);
|
|
636
|
+
return expression(sql `(${fragmentOrExpression})::${unsafeRaw(typeName)}`);
|
|
637
|
+
},
|
|
638
|
+
/**
|
|
639
|
+
* COALESCE helper
|
|
640
|
+
* Returns the first non-null value from the given expressions/values.
|
|
641
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
642
|
+
*/
|
|
643
|
+
coalesce(...args) {
|
|
644
|
+
const fragments = args.map((arg) => {
|
|
645
|
+
if (arg instanceof SQLFragment) {
|
|
646
|
+
return arg;
|
|
647
|
+
}
|
|
648
|
+
return sql `${arg}`;
|
|
649
|
+
});
|
|
650
|
+
const inner = SQLFragment.joinWithCommaSeparator(...fragments);
|
|
651
|
+
return expression(sql `COALESCE(${inner})`);
|
|
652
|
+
},
|
|
653
|
+
/**
|
|
654
|
+
* LOWER helper
|
|
655
|
+
* Converts a string expression to lowercase.
|
|
656
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
657
|
+
*/
|
|
658
|
+
lower(expressionOrFieldName) {
|
|
659
|
+
const inner = expressionOrFieldName instanceof SQLFragment
|
|
660
|
+
? expressionOrFieldName
|
|
661
|
+
: sql `${entityField(expressionOrFieldName)}`;
|
|
662
|
+
return expression(sql `LOWER(${inner})`);
|
|
663
|
+
},
|
|
664
|
+
/**
|
|
665
|
+
* UPPER helper
|
|
666
|
+
* Converts a string expression to uppercase.
|
|
667
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
668
|
+
*/
|
|
669
|
+
upper(expressionOrFieldName) {
|
|
670
|
+
const inner = expressionOrFieldName instanceof SQLFragment
|
|
671
|
+
? expressionOrFieldName
|
|
672
|
+
: sql `${entityField(expressionOrFieldName)}`;
|
|
673
|
+
return expression(sql `UPPER(${inner})`);
|
|
674
|
+
},
|
|
675
|
+
/**
|
|
676
|
+
* TRIM helper
|
|
677
|
+
* Removes leading and trailing whitespace from a string expression.
|
|
678
|
+
* Returns an SQLExpression so that fluent comparison methods can be chained.
|
|
679
|
+
*/
|
|
680
|
+
trim(expressionOrFieldName) {
|
|
681
|
+
const inner = expressionOrFieldName instanceof SQLFragment
|
|
682
|
+
? expressionOrFieldName
|
|
683
|
+
: sql `${entityField(expressionOrFieldName)}`;
|
|
684
|
+
return expression(sql `TRIM(${inner})`);
|
|
438
685
|
},
|
|
439
686
|
/**
|
|
440
687
|
* Logical AND of multiple fragments
|
|
441
688
|
*/
|
|
442
689
|
and(...conditions) {
|
|
443
690
|
if (conditions.length === 0) {
|
|
444
|
-
return sql `
|
|
691
|
+
return sql `TRUE`;
|
|
445
692
|
}
|
|
446
693
|
return joinSQLFragments(conditions.map((c) => exports.SQLFragmentHelpers.group(c)), ' AND ');
|
|
447
694
|
},
|
|
@@ -450,7 +697,7 @@ exports.SQLFragmentHelpers = {
|
|
|
450
697
|
*/
|
|
451
698
|
or(...conditions) {
|
|
452
699
|
if (conditions.length === 0) {
|
|
453
|
-
return sql `
|
|
700
|
+
return sql `FALSE`;
|
|
454
701
|
}
|
|
455
702
|
return joinSQLFragments(conditions.map((c) => exports.SQLFragmentHelpers.group(c)), ' OR ');
|
|
456
703
|
},
|
|
@@ -458,17 +705,26 @@ exports.SQLFragmentHelpers = {
|
|
|
458
705
|
* Logical NOT of a fragment
|
|
459
706
|
*/
|
|
460
707
|
not(condition) {
|
|
461
|
-
return
|
|
708
|
+
return sql `NOT (${condition})`;
|
|
462
709
|
},
|
|
463
710
|
/**
|
|
464
711
|
* Parentheses helper for grouping conditions
|
|
465
712
|
*/
|
|
466
713
|
group(condition) {
|
|
467
|
-
return
|
|
714
|
+
return sql `(${condition})`;
|
|
468
715
|
},
|
|
469
716
|
};
|
|
470
717
|
// Internal helper function to join SQL fragments with a specified separator
|
|
471
718
|
function joinSQLFragments(fragments, separator) {
|
|
472
719
|
return new SQLFragment(fragments.map((f) => f.sql).join(separator), fragments.flatMap((f) => f.bindings));
|
|
473
720
|
}
|
|
721
|
+
// Internal helper to properly quote elements for PostgreSQL array literals.
|
|
722
|
+
// Elements containing special characters (commas, braces, quotes, backslashes, whitespace)
|
|
723
|
+
// or empty strings must be double-quoted with internal escaping.
|
|
724
|
+
function quotePostgresArrayElement(element) {
|
|
725
|
+
if (element === '' || /[,{}"\\\s]/.test(element)) {
|
|
726
|
+
return `"${element.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
727
|
+
}
|
|
728
|
+
return element;
|
|
729
|
+
}
|
|
474
730
|
//# sourceMappingURL=SQLOperator.js.map
|