@expo/entity-database-adapter-knex 0.54.0 → 0.57.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/AuthorizationResultBasedKnexEntityLoader.d.ts +279 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
- package/build/src/BaseSQLQueryBuilder.js +87 -0
- package/build/src/BaseSQLQueryBuilder.js.map +1 -0
- package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
- package/build/src/EnforcingKnexEntityLoader.js +166 -0
- package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
- package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
- package/build/src/KnexEntityLoaderFactory.js +39 -0
- package/build/src/KnexEntityLoaderFactory.js.map +1 -0
- package/build/src/PaginationStrategy.d.ts +30 -0
- package/build/src/PaginationStrategy.js +35 -0
- package/build/src/PaginationStrategy.js.map +1 -0
- package/build/src/PostgresEntity.d.ts +25 -0
- package/build/src/PostgresEntity.js +39 -0
- package/build/src/PostgresEntity.js.map +1 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
- package/build/src/PostgresEntityDatabaseAdapter.js +32 -11
- package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
- package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
- package/build/src/ReadonlyPostgresEntity.js +39 -0
- package/build/src/ReadonlyPostgresEntity.js.map +1 -0
- package/build/src/SQLOperator.d.ts +261 -0
- package/build/src/SQLOperator.js +464 -0
- package/build/src/SQLOperator.js.map +1 -0
- package/build/src/index.d.ts +15 -0
- package/build/src/index.js +15 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
- package/build/src/internal/EntityKnexDataManager.js +453 -0
- package/build/src/internal/EntityKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexDataManager.d.ts +3 -0
- package/build/src/internal/getKnexDataManager.js +19 -0
- package/build/src/internal/getKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
- package/build/src/internal/utilityTypes.d.ts +5 -0
- package/build/src/internal/utilityTypes.js +5 -0
- package/build/src/internal/utilityTypes.js.map +1 -0
- package/build/src/internal/weakMaps.d.ts +9 -0
- package/build/src/internal/weakMaps.js +20 -0
- package/build/src/internal/weakMaps.js.map +1 -0
- package/build/src/knexLoader.d.ts +18 -0
- package/build/src/knexLoader.js +31 -0
- package/build/src/knexLoader.js.map +1 -0
- package/package.json +6 -5
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +538 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
- package/src/BaseSQLQueryBuilder.ts +114 -0
- package/src/EnforcingKnexEntityLoader.ts +271 -0
- package/src/KnexEntityLoaderFactory.ts +130 -0
- package/src/PaginationStrategy.ts +32 -0
- package/src/PostgresEntity.ts +118 -0
- package/src/PostgresEntityDatabaseAdapter.ts +78 -24
- package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
- package/src/ReadonlyPostgresEntity.ts +115 -0
- package/src/SQLOperator.ts +603 -0
- package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
- package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
- package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
- package/src/__tests__/EntityFields-test.ts +1 -1
- package/src/__tests__/PostgresEntity-test.ts +172 -0
- package/src/__tests__/ReadonlyEntity-test.ts +32 -0
- package/src/__tests__/SQLOperator-test.ts +831 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
- package/src/__tests__/fixtures/TestEntity.ts +131 -0
- package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
- package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
- package/src/index.ts +15 -0
- package/src/internal/EntityKnexDataManager.ts +832 -0
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
- package/src/internal/__tests__/weakMaps-test.ts +25 -0
- package/src/internal/getKnexDataManager.ts +43 -0
- package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
- package/src/internal/utilityTypes.ts +11 -0
- package/src/internal/weakMaps.ts +19 -0
- package/src/knexLoader.ts +110 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported SQL value types that can be safely parameterized.
|
|
3
|
+
* This ensures type safety and prevents passing unsupported types to SQL queries.
|
|
4
|
+
*/
|
|
5
|
+
export type SupportedSQLValue =
|
|
6
|
+
| string
|
|
7
|
+
| number
|
|
8
|
+
| boolean
|
|
9
|
+
| null
|
|
10
|
+
| Date
|
|
11
|
+
| Buffer
|
|
12
|
+
| bigint
|
|
13
|
+
| undefined // Will be treated as NULL
|
|
14
|
+
| readonly SupportedSQLValue[] // For IN clauses and array types
|
|
15
|
+
| Readonly<{ [key: string]: unknown }>; // For JSON/JSONB columns
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Types of bindings that can be used in SQL queries.
|
|
19
|
+
*/
|
|
20
|
+
export type SQLBinding<TFields extends Record<string, any>> =
|
|
21
|
+
| { type: 'value'; value: SupportedSQLValue }
|
|
22
|
+
| { type: 'identifier'; name: string }
|
|
23
|
+
| { type: 'entityField'; fieldName: keyof TFields };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SQL Fragment class that safely handles parameterized queries.
|
|
27
|
+
*/
|
|
28
|
+
export class SQLFragment<TFields extends Record<string, any>> {
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly sql: string,
|
|
31
|
+
public readonly bindings: readonly SQLBinding<TFields>[],
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get bindings in the format expected by Knex.
|
|
36
|
+
* Knex expects a flat array where both identifiers and values are mixed in order.
|
|
37
|
+
*
|
|
38
|
+
* @param getColumnForField - function that resolves an entity field name to its database column name
|
|
39
|
+
*/
|
|
40
|
+
getKnexBindings(
|
|
41
|
+
getColumnForField: (fieldName: keyof TFields) => string,
|
|
42
|
+
): readonly (string | SupportedSQLValue)[] {
|
|
43
|
+
return this.bindings.map((b) => {
|
|
44
|
+
switch (b.type) {
|
|
45
|
+
case 'entityField':
|
|
46
|
+
return getColumnForField(b.fieldName);
|
|
47
|
+
case 'identifier':
|
|
48
|
+
return b.name;
|
|
49
|
+
case 'value':
|
|
50
|
+
return b.value;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Combine SQL fragments
|
|
57
|
+
*/
|
|
58
|
+
append(other: SQLFragment<TFields>): SQLFragment<TFields> {
|
|
59
|
+
return joinSQLFragments([this, other], ' ');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Join multiple SQL fragments with a comma separator.
|
|
64
|
+
* Useful for combining column lists, value lists, etc.
|
|
65
|
+
*
|
|
66
|
+
* @param fragments - Array of SQL fragments to join
|
|
67
|
+
* @returns - A new SQLFragment with the fragments joined by a comma and space
|
|
68
|
+
*/
|
|
69
|
+
static joinWithCommaSeparator<TFields extends Record<string, any>>(
|
|
70
|
+
...fragments: readonly SQLFragment<TFields>[]
|
|
71
|
+
): SQLFragment<TFields> {
|
|
72
|
+
return joinSQLFragments(fragments, ', ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Concatenate multiple SQL fragments with space separator.
|
|
77
|
+
* Useful for combining SQL clauses like WHERE, ORDER BY, etc.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const where = sql`WHERE age > ${18}`;
|
|
82
|
+
* const orderBy = sql`ORDER BY name`;
|
|
83
|
+
* const query = SQLFragment.concat(sql`SELECT * FROM users`, where, orderBy);
|
|
84
|
+
* // Generates: "SELECT * FROM users WHERE age > ? ORDER BY name"
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
static concat<TFields extends Record<string, any>>(
|
|
88
|
+
...fragments: readonly SQLFragment<TFields>[]
|
|
89
|
+
): SQLFragment<TFields> {
|
|
90
|
+
return joinSQLFragments(fragments, ' ');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get a debug representation of the query with values inline
|
|
95
|
+
* WARNING: This is for debugging only. Never execute the returned string directly.
|
|
96
|
+
*/
|
|
97
|
+
getDebugString(): string {
|
|
98
|
+
let debugString = this.sql;
|
|
99
|
+
let bindingIndex = 0;
|
|
100
|
+
|
|
101
|
+
// Replace ?? and ? placeholders with actual values for debugging
|
|
102
|
+
debugString = debugString.replace(/\?\?|\?/g, (match) => {
|
|
103
|
+
if (bindingIndex >= this.bindings.length) {
|
|
104
|
+
return match;
|
|
105
|
+
}
|
|
106
|
+
const binding = this.bindings[bindingIndex];
|
|
107
|
+
if (!binding) {
|
|
108
|
+
return match;
|
|
109
|
+
}
|
|
110
|
+
bindingIndex++;
|
|
111
|
+
|
|
112
|
+
if (match === '??' && binding.type === 'identifier') {
|
|
113
|
+
// For identifiers, show them quoted as they would appear
|
|
114
|
+
return `"${binding.name.replace(/"/g, '""')}"`;
|
|
115
|
+
} else if (match === '??' && binding.type === 'entityField') {
|
|
116
|
+
// For entity fields, show the entity field name as the identifier for debugging
|
|
117
|
+
return `"${binding.fieldName.toString()}"`;
|
|
118
|
+
} else if (match === '?' && binding.type === 'value') {
|
|
119
|
+
return SQLFragment.formatDebugValue(binding.value);
|
|
120
|
+
} else {
|
|
121
|
+
// Mismatch between placeholder type and binding type
|
|
122
|
+
return match;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return debugString;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format a value for debug output based on its type.
|
|
131
|
+
* Handles all SupportedSQLValue types.
|
|
132
|
+
*/
|
|
133
|
+
private static formatDebugValue(value: SupportedSQLValue): string {
|
|
134
|
+
// Handle null and undefined
|
|
135
|
+
if (value === null || value === undefined) {
|
|
136
|
+
return 'NULL';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle primitives
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
142
|
+
}
|
|
143
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
144
|
+
return String(value);
|
|
145
|
+
}
|
|
146
|
+
if (typeof value === 'boolean') {
|
|
147
|
+
return value ? 'TRUE' : 'FALSE';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle Date
|
|
151
|
+
if (value instanceof Date) {
|
|
152
|
+
return `'${value.toISOString()}'`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle Buffer
|
|
156
|
+
if (Buffer.isBuffer(value)) {
|
|
157
|
+
return `'\\x${value.toString('hex')}'`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle arrays (for IN clauses or array columns)
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return `ARRAY[${value.map((v) => this.formatDebugValue(v)).join(', ')}]`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle objects (for JSON/JSONB columns)
|
|
166
|
+
if (typeof value === 'object' && SQLFragment.isPlainObjectForDebug(value)) {
|
|
167
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'::jsonb`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fallback (should never reach here with SupportedSQLValue but because this is used
|
|
171
|
+
// for debugging, there might be other values that we want to know about)
|
|
172
|
+
return `UnsupportedSQLValue[${String(value)}]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static isPlainObjectForDebug(obj: object): boolean {
|
|
176
|
+
const proto = Object.getPrototypeOf(obj);
|
|
177
|
+
// Ensure it doesn't have a custom prototype (like a class would)
|
|
178
|
+
if (proto === null) {
|
|
179
|
+
return true; // Created via Object.create(null)
|
|
180
|
+
}
|
|
181
|
+
// Check if constructor is the base Object function
|
|
182
|
+
return proto.constructor === Object;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper for SQL identifiers (table/column names).
|
|
188
|
+
* Stores the raw identifier name to be escaped by Knex using ?? placeholder.
|
|
189
|
+
*/
|
|
190
|
+
export class SQLIdentifier {
|
|
191
|
+
constructor(public readonly name: string) {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Helper for referencing entity fields that can be used in SQL queries. This allows for type-safe references to fields of an entity
|
|
196
|
+
* and does automatic translation to DB field names.
|
|
197
|
+
*/
|
|
198
|
+
export class SQLEntityField<TFields extends Record<string, any>> {
|
|
199
|
+
constructor(public readonly fieldName: keyof TFields) {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Helper for raw SQL that should not be parameterized
|
|
204
|
+
* WARNING: Only use this with trusted input to avoid SQL injection
|
|
205
|
+
*/
|
|
206
|
+
export class SQLUnsafeRaw {
|
|
207
|
+
constructor(public readonly rawSql: string) {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a SQL identifier (table/column name) that will be escaped by Knex using ??.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* identifier('users') // Will be escaped as "users" in PostgreSQL
|
|
216
|
+
* identifier('my"table') // Will be escaped as "my""table" in PostgreSQL
|
|
217
|
+
* identifier('column"; DROP TABLE users; --') // Will be safely escaped
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function identifier(name: string): SQLIdentifier {
|
|
221
|
+
return new SQLIdentifier(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a reference to an entity field that can be used in SQL queries. This allows for type-safe references to fields of an entity
|
|
226
|
+
* and does automatic translation to DB field names and will be escaped by Knex using ??.
|
|
227
|
+
*
|
|
228
|
+
* @param fieldName - The entity field name to reference.
|
|
229
|
+
*/
|
|
230
|
+
export function entityField<TFields extends Record<string, any>>(
|
|
231
|
+
fieldName: keyof TFields,
|
|
232
|
+
): SQLEntityField<TFields> {
|
|
233
|
+
return new SQLEntityField(fieldName);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Insert raw SQL that will not be parameterized
|
|
238
|
+
* WARNING: This bypasses SQL injection protection. Only use with trusted input.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* // Dynamic column names
|
|
243
|
+
* const sortColumn = 'created_at';
|
|
244
|
+
* const query = sql`ORDER BY ${unsafeRaw(sortColumn)} DESC`;
|
|
245
|
+
*
|
|
246
|
+
* // Dynamic SQL expressions
|
|
247
|
+
* const query = sql`WHERE ${unsafeRaw('EXTRACT(year FROM created_at)')} = ${2024}`;
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
export function unsafeRaw(sqlString: string): SQLUnsafeRaw {
|
|
251
|
+
return new SQLUnsafeRaw(sqlString);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Tagged template literal function for SQL queries
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* const age = 18;
|
|
260
|
+
* const query = sql`age >= ${age} AND status = ${'active'}`;
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
export function sql<TFields extends Record<string, any>>(
|
|
264
|
+
strings: TemplateStringsArray,
|
|
265
|
+
...values: readonly (
|
|
266
|
+
| SupportedSQLValue
|
|
267
|
+
| SQLFragment<TFields>
|
|
268
|
+
| SQLIdentifier
|
|
269
|
+
| SQLUnsafeRaw
|
|
270
|
+
| SQLEntityField<TFields>
|
|
271
|
+
)[]
|
|
272
|
+
): SQLFragment<TFields> {
|
|
273
|
+
let sqlString = '';
|
|
274
|
+
const bindings: SQLBinding<TFields>[] = [];
|
|
275
|
+
|
|
276
|
+
strings.forEach((string, i) => {
|
|
277
|
+
sqlString += string;
|
|
278
|
+
if (i < values.length) {
|
|
279
|
+
const value = values[i];
|
|
280
|
+
|
|
281
|
+
if (value instanceof SQLFragment) {
|
|
282
|
+
// Handle nested SQL fragments
|
|
283
|
+
sqlString += value.sql;
|
|
284
|
+
bindings.push(...value.bindings);
|
|
285
|
+
} else if (value instanceof SQLIdentifier) {
|
|
286
|
+
// Handle identifiers (table/column names) with ?? placeholder
|
|
287
|
+
sqlString += '??';
|
|
288
|
+
bindings.push({ type: 'identifier', name: value.name });
|
|
289
|
+
} else if (value instanceof SQLEntityField) {
|
|
290
|
+
// Handle entity field references by treating them as identifiers
|
|
291
|
+
sqlString += '??';
|
|
292
|
+
bindings.push({ type: 'entityField', fieldName: value.fieldName });
|
|
293
|
+
} else if (value instanceof SQLUnsafeRaw) {
|
|
294
|
+
// Handle raw SQL (WARNING: no parameterization)
|
|
295
|
+
sqlString += value.rawSql;
|
|
296
|
+
} else if (Array.isArray(value)) {
|
|
297
|
+
// Handle IN clauses
|
|
298
|
+
sqlString += `(${value.map(() => '?').join(', ')})`;
|
|
299
|
+
bindings.push(...value.map((v): SQLBinding<TFields> => ({ type: 'value', value: v })));
|
|
300
|
+
} else {
|
|
301
|
+
// Regular value binding
|
|
302
|
+
sqlString += '?';
|
|
303
|
+
bindings.push({ type: 'value', value });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return new SQLFragment(sqlString, bindings);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type PickSupportedSQLValueKeys<T> = {
|
|
312
|
+
[K in keyof T]: T[K] extends SupportedSQLValue ? K : never;
|
|
313
|
+
}[keyof T];
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Common SQL helper functions for building queries
|
|
317
|
+
*/
|
|
318
|
+
export const SQLFragmentHelpers = {
|
|
319
|
+
/**
|
|
320
|
+
* IN clause helper
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* const query = SQLFragmentHelpers.inArray<MyFields, 'id'>('status', ['active', 'pending']);
|
|
325
|
+
* // Generates: ?? IN (?, ?) with entityField binding for 'status' and value bindings
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
inArray<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
329
|
+
fieldName: N,
|
|
330
|
+
values: readonly TFields[N][],
|
|
331
|
+
): SQLFragment<TFields> {
|
|
332
|
+
if (values.length === 0) {
|
|
333
|
+
// Handle empty array case - always false
|
|
334
|
+
return sql`1 = 0`;
|
|
335
|
+
}
|
|
336
|
+
return sql`${entityField(fieldName)} IN ${values}`;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* NOT IN clause helper
|
|
341
|
+
*/
|
|
342
|
+
notInArray<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
343
|
+
fieldName: N,
|
|
344
|
+
values: readonly TFields[N][],
|
|
345
|
+
): SQLFragment<TFields> {
|
|
346
|
+
if (values.length === 0) {
|
|
347
|
+
// Handle empty array case - always true
|
|
348
|
+
return sql`1 = 1`;
|
|
349
|
+
}
|
|
350
|
+
return sql`${entityField(fieldName)} NOT IN ${values}`;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* BETWEEN helper
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* const query = SQLFragmentHelpers.between<MyFields, 'id'>('age', 18, 65);
|
|
359
|
+
* // Generates: ?? BETWEEN ? AND ? with entityField binding for 'age' and value bindings
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
between<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
363
|
+
fieldName: N,
|
|
364
|
+
min: TFields[N],
|
|
365
|
+
max: TFields[N],
|
|
366
|
+
): SQLFragment<TFields> {
|
|
367
|
+
return sql`${entityField(fieldName)} BETWEEN ${min} AND ${max}`;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* NOT BETWEEN helper
|
|
372
|
+
*/
|
|
373
|
+
notBetween<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
374
|
+
fieldName: N,
|
|
375
|
+
min: TFields[N],
|
|
376
|
+
max: TFields[N],
|
|
377
|
+
): SQLFragment<TFields> {
|
|
378
|
+
return sql`${entityField(fieldName)} NOT BETWEEN ${min} AND ${max}`;
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* LIKE helper with automatic escaping
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* const query = SQLFragmentHelpers.like<MyFields, 'id'>('name', '%John%');
|
|
387
|
+
* // Generates: ?? LIKE ? with entityField binding for 'name' and value binding
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
like<TFields extends Record<string, any>>(
|
|
391
|
+
fieldName: keyof TFields,
|
|
392
|
+
pattern: string,
|
|
393
|
+
): SQLFragment<TFields> {
|
|
394
|
+
return sql`${entityField(fieldName)} LIKE ${pattern}`;
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* NOT LIKE helper
|
|
399
|
+
*/
|
|
400
|
+
notLike<TFields extends Record<string, any>>(
|
|
401
|
+
fieldName: keyof TFields,
|
|
402
|
+
pattern: string,
|
|
403
|
+
): SQLFragment<TFields> {
|
|
404
|
+
return sql`${entityField(fieldName)} NOT LIKE ${pattern}`;
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* ILIKE helper for case-insensitive matching
|
|
409
|
+
*/
|
|
410
|
+
ilike<TFields extends Record<string, any>>(
|
|
411
|
+
fieldName: keyof TFields,
|
|
412
|
+
pattern: string,
|
|
413
|
+
): SQLFragment<TFields> {
|
|
414
|
+
return sql`${entityField(fieldName)} ILIKE ${pattern}`;
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* NOT ILIKE helper for case-insensitive non-matching
|
|
419
|
+
*/
|
|
420
|
+
notIlike<TFields extends Record<string, any>>(
|
|
421
|
+
fieldName: keyof TFields,
|
|
422
|
+
pattern: string,
|
|
423
|
+
): SQLFragment<TFields> {
|
|
424
|
+
return sql`${entityField(fieldName)} NOT ILIKE ${pattern}`;
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* NULL check helper
|
|
429
|
+
*/
|
|
430
|
+
isNull<TFields extends Record<string, any>>(fieldName: keyof TFields): SQLFragment<TFields> {
|
|
431
|
+
return sql`${entityField(fieldName)} IS NULL`;
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* NOT NULL check helper
|
|
436
|
+
*/
|
|
437
|
+
isNotNull<TFields extends Record<string, any>>(fieldName: keyof TFields): SQLFragment<TFields> {
|
|
438
|
+
return sql`${entityField(fieldName)} IS NOT NULL`;
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Single-equals-equality operator
|
|
443
|
+
*/
|
|
444
|
+
eq<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
445
|
+
fieldName: N,
|
|
446
|
+
value: TFields[N],
|
|
447
|
+
): SQLFragment<TFields> {
|
|
448
|
+
if (value === null || value === undefined) {
|
|
449
|
+
return SQLFragmentHelpers.isNull(fieldName);
|
|
450
|
+
}
|
|
451
|
+
return sql`${entityField(fieldName)} = ${value}`;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Single-equals-inequality operator
|
|
456
|
+
*/
|
|
457
|
+
neq<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
458
|
+
fieldName: N,
|
|
459
|
+
value: TFields[N],
|
|
460
|
+
): SQLFragment<TFields> {
|
|
461
|
+
if (value === null || value === undefined) {
|
|
462
|
+
return SQLFragmentHelpers.isNotNull(fieldName);
|
|
463
|
+
}
|
|
464
|
+
return sql`${entityField(fieldName)} != ${value}`;
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Greater-than comparison operator
|
|
469
|
+
*/
|
|
470
|
+
gt<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
471
|
+
fieldName: N,
|
|
472
|
+
value: TFields[N],
|
|
473
|
+
): SQLFragment<TFields> {
|
|
474
|
+
return sql`${entityField(fieldName)} > ${value}`;
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Greater-than-or-equal-to comparison operator
|
|
479
|
+
*/
|
|
480
|
+
gte<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
481
|
+
fieldName: N,
|
|
482
|
+
value: TFields[N],
|
|
483
|
+
): SQLFragment<TFields> {
|
|
484
|
+
return sql`${entityField(fieldName)} >= ${value}`;
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Less-than comparison operator
|
|
489
|
+
*/
|
|
490
|
+
lt<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
491
|
+
fieldName: N,
|
|
492
|
+
value: TFields[N],
|
|
493
|
+
): SQLFragment<TFields> {
|
|
494
|
+
return sql`${entityField(fieldName)} < ${value}`;
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Less-than-or-equal-to comparison operator
|
|
499
|
+
*/
|
|
500
|
+
lte<TFields extends Record<string, any>, N extends PickSupportedSQLValueKeys<TFields>>(
|
|
501
|
+
fieldName: N,
|
|
502
|
+
value: TFields[N],
|
|
503
|
+
): SQLFragment<TFields> {
|
|
504
|
+
return sql`${entityField(fieldName)} <= ${value}`;
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* JSON contains operator (\@\>)
|
|
509
|
+
*/
|
|
510
|
+
jsonContains<TFields extends Record<string, any>>(
|
|
511
|
+
fieldName: keyof TFields,
|
|
512
|
+
value: unknown,
|
|
513
|
+
): SQLFragment<TFields> {
|
|
514
|
+
return sql`${entityField(fieldName)} @> ${JSON.stringify(value)}::jsonb`;
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* JSON contained by operator (\<\@\)
|
|
519
|
+
*/
|
|
520
|
+
jsonContainedBy<TFields extends Record<string, any>>(
|
|
521
|
+
fieldName: keyof TFields,
|
|
522
|
+
value: unknown,
|
|
523
|
+
): SQLFragment<TFields> {
|
|
524
|
+
return sql`${entityField(fieldName)} <@ ${JSON.stringify(value)}::jsonb`;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* JSON path extraction helper (-\>)
|
|
529
|
+
*/
|
|
530
|
+
jsonPath<TFields extends Record<string, any>>(
|
|
531
|
+
fieldName: keyof TFields,
|
|
532
|
+
path: string,
|
|
533
|
+
): SQLFragment<TFields> {
|
|
534
|
+
return sql`${entityField(fieldName)}->${path}`;
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* JSON path text extraction helper (-\>\>)
|
|
539
|
+
*/
|
|
540
|
+
jsonPathText<TFields extends Record<string, any>>(
|
|
541
|
+
fieldName: keyof TFields,
|
|
542
|
+
path: string,
|
|
543
|
+
): SQLFragment<TFields> {
|
|
544
|
+
return sql`${entityField(fieldName)}->>${path}`;
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Logical AND of multiple fragments
|
|
549
|
+
*/
|
|
550
|
+
and<TFields extends Record<string, any>>(
|
|
551
|
+
...conditions: readonly SQLFragment<TFields>[]
|
|
552
|
+
): SQLFragment<TFields> {
|
|
553
|
+
if (conditions.length === 0) {
|
|
554
|
+
return sql`1 = 1`;
|
|
555
|
+
}
|
|
556
|
+
return joinSQLFragments(
|
|
557
|
+
conditions.map((c) => SQLFragmentHelpers.group(c)),
|
|
558
|
+
' AND ',
|
|
559
|
+
);
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Logical OR of multiple fragments
|
|
564
|
+
*/
|
|
565
|
+
or<TFields extends Record<string, any>>(
|
|
566
|
+
...conditions: readonly SQLFragment<TFields>[]
|
|
567
|
+
): SQLFragment<TFields> {
|
|
568
|
+
if (conditions.length === 0) {
|
|
569
|
+
return sql`1 = 0`;
|
|
570
|
+
}
|
|
571
|
+
return joinSQLFragments(
|
|
572
|
+
conditions.map((c) => SQLFragmentHelpers.group(c)),
|
|
573
|
+
' OR ',
|
|
574
|
+
);
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Logical NOT of a fragment
|
|
579
|
+
*/
|
|
580
|
+
not<TFields extends Record<string, any>>(condition: SQLFragment<TFields>): SQLFragment<TFields> {
|
|
581
|
+
return new SQLFragment('NOT (' + condition.sql + ')', condition.bindings);
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Parentheses helper for grouping conditions
|
|
586
|
+
*/
|
|
587
|
+
group<TFields extends Record<string, any>>(
|
|
588
|
+
condition: SQLFragment<TFields>,
|
|
589
|
+
): SQLFragment<TFields> {
|
|
590
|
+
return new SQLFragment('(' + condition.sql + ')', condition.bindings);
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Internal helper function to join SQL fragments with a specified separator
|
|
595
|
+
function joinSQLFragments<TFields extends Record<string, any>>(
|
|
596
|
+
fragments: readonly SQLFragment<TFields>[],
|
|
597
|
+
separator: string,
|
|
598
|
+
): SQLFragment<TFields> {
|
|
599
|
+
return new SQLFragment(
|
|
600
|
+
fragments.map((f) => f.sql).join(separator),
|
|
601
|
+
fragments.flatMap((f) => f.bindings),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
@@ -90,7 +90,7 @@ describe(createWithUniqueConstraintRecoveryAsync, () => {
|
|
|
90
90
|
name: 'unique',
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
const createdEntities = await vc1.
|
|
93
|
+
const createdEntities = await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
94
94
|
'postgres',
|
|
95
95
|
async (queryContext) => {
|
|
96
96
|
if (parallel) {
|
|
@@ -154,38 +154,32 @@ describe(createWithUniqueConstraintRecoveryAsync, () => {
|
|
|
154
154
|
let createdEntities: [PostgresUniqueTestEntity, PostgresUniqueTestEntity];
|
|
155
155
|
if (parallel) {
|
|
156
156
|
createdEntities = await Promise.all([
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
PostgresUniqueTestEntity.createWithNameAsync,
|
|
180
|
-
args,
|
|
181
|
-
queryContext,
|
|
182
|
-
);
|
|
183
|
-
},
|
|
184
|
-
),
|
|
157
|
+
vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
|
|
158
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
159
|
+
vc1,
|
|
160
|
+
PostgresUniqueTestEntity,
|
|
161
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
162
|
+
args,
|
|
163
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
164
|
+
args,
|
|
165
|
+
queryContext,
|
|
166
|
+
);
|
|
167
|
+
}),
|
|
168
|
+
vc1.runInTransactionForDatabaseAdapterFlavorAsync('postgres', async (queryContext) => {
|
|
169
|
+
return await createWithUniqueConstraintRecoveryAsync(
|
|
170
|
+
vc1,
|
|
171
|
+
PostgresUniqueTestEntity,
|
|
172
|
+
PostgresUniqueTestEntity.getByNameAsync,
|
|
173
|
+
args,
|
|
174
|
+
PostgresUniqueTestEntity.createWithNameAsync,
|
|
175
|
+
args,
|
|
176
|
+
queryContext,
|
|
177
|
+
);
|
|
178
|
+
}),
|
|
185
179
|
]);
|
|
186
180
|
} else {
|
|
187
181
|
createdEntities = [
|
|
188
|
-
await vc1.
|
|
182
|
+
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
189
183
|
'postgres',
|
|
190
184
|
async (queryContext) => {
|
|
191
185
|
return await createWithUniqueConstraintRecoveryAsync(
|
|
@@ -199,7 +193,7 @@ describe(createWithUniqueConstraintRecoveryAsync, () => {
|
|
|
199
193
|
);
|
|
200
194
|
},
|
|
201
195
|
),
|
|
202
|
-
await vc1.
|
|
196
|
+
await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
|
|
203
197
|
'postgres',
|
|
204
198
|
async (queryContext) => {
|
|
205
199
|
return await createWithUniqueConstraintRecoveryAsync(
|