@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.
Files changed (91) hide show
  1. package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +279 -0
  2. package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
  3. package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
  4. package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
  5. package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
  6. package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
  7. package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
  8. package/build/src/BaseSQLQueryBuilder.js +87 -0
  9. package/build/src/BaseSQLQueryBuilder.js.map +1 -0
  10. package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
  11. package/build/src/EnforcingKnexEntityLoader.js +166 -0
  12. package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
  13. package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
  14. package/build/src/KnexEntityLoaderFactory.js +39 -0
  15. package/build/src/KnexEntityLoaderFactory.js.map +1 -0
  16. package/build/src/PaginationStrategy.d.ts +30 -0
  17. package/build/src/PaginationStrategy.js +35 -0
  18. package/build/src/PaginationStrategy.js.map +1 -0
  19. package/build/src/PostgresEntity.d.ts +25 -0
  20. package/build/src/PostgresEntity.js +39 -0
  21. package/build/src/PostgresEntity.js.map +1 -0
  22. package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
  23. package/build/src/PostgresEntityDatabaseAdapter.js +32 -11
  24. package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
  25. package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
  26. package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
  27. package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
  28. package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
  29. package/build/src/ReadonlyPostgresEntity.js +39 -0
  30. package/build/src/ReadonlyPostgresEntity.js.map +1 -0
  31. package/build/src/SQLOperator.d.ts +261 -0
  32. package/build/src/SQLOperator.js +464 -0
  33. package/build/src/SQLOperator.js.map +1 -0
  34. package/build/src/index.d.ts +15 -0
  35. package/build/src/index.js +15 -0
  36. package/build/src/index.js.map +1 -1
  37. package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
  38. package/build/src/internal/EntityKnexDataManager.js +453 -0
  39. package/build/src/internal/EntityKnexDataManager.js.map +1 -0
  40. package/build/src/internal/getKnexDataManager.d.ts +3 -0
  41. package/build/src/internal/getKnexDataManager.js +19 -0
  42. package/build/src/internal/getKnexDataManager.js.map +1 -0
  43. package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
  44. package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
  45. package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
  46. package/build/src/internal/utilityTypes.d.ts +5 -0
  47. package/build/src/internal/utilityTypes.js +5 -0
  48. package/build/src/internal/utilityTypes.js.map +1 -0
  49. package/build/src/internal/weakMaps.d.ts +9 -0
  50. package/build/src/internal/weakMaps.js +20 -0
  51. package/build/src/internal/weakMaps.js.map +1 -0
  52. package/build/src/knexLoader.d.ts +18 -0
  53. package/build/src/knexLoader.js +31 -0
  54. package/build/src/knexLoader.js.map +1 -0
  55. package/package.json +6 -5
  56. package/src/AuthorizationResultBasedKnexEntityLoader.ts +538 -0
  57. package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
  58. package/src/BaseSQLQueryBuilder.ts +114 -0
  59. package/src/EnforcingKnexEntityLoader.ts +271 -0
  60. package/src/KnexEntityLoaderFactory.ts +130 -0
  61. package/src/PaginationStrategy.ts +32 -0
  62. package/src/PostgresEntity.ts +118 -0
  63. package/src/PostgresEntityDatabaseAdapter.ts +78 -24
  64. package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
  65. package/src/ReadonlyPostgresEntity.ts +115 -0
  66. package/src/SQLOperator.ts +603 -0
  67. package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
  68. package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
  69. package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
  70. package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
  71. package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
  72. package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
  73. package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
  74. package/src/__tests__/EntityFields-test.ts +1 -1
  75. package/src/__tests__/PostgresEntity-test.ts +172 -0
  76. package/src/__tests__/ReadonlyEntity-test.ts +32 -0
  77. package/src/__tests__/SQLOperator-test.ts +831 -0
  78. package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
  79. package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
  80. package/src/__tests__/fixtures/TestEntity.ts +131 -0
  81. package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
  82. package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
  83. package/src/index.ts +15 -0
  84. package/src/internal/EntityKnexDataManager.ts +832 -0
  85. package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
  86. package/src/internal/__tests__/weakMaps-test.ts +25 -0
  87. package/src/internal/getKnexDataManager.ts +43 -0
  88. package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
  89. package/src/internal/utilityTypes.ts +11 -0
  90. package/src/internal/weakMaps.ts +19 -0
  91. 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.runInTransactionForDatabaseAdaptorFlavorAsync(
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
- await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
158
- 'postgres',
159
- async (queryContext) => {
160
- return await createWithUniqueConstraintRecoveryAsync(
161
- vc1,
162
- PostgresUniqueTestEntity,
163
- PostgresUniqueTestEntity.getByNameAsync,
164
- args,
165
- PostgresUniqueTestEntity.createWithNameAsync,
166
- args,
167
- queryContext,
168
- );
169
- },
170
- ),
171
- await vc1.runInTransactionForDatabaseAdaptorFlavorAsync(
172
- 'postgres',
173
- async (queryContext) => {
174
- return await createWithUniqueConstraintRecoveryAsync(
175
- vc1,
176
- PostgresUniqueTestEntity,
177
- PostgresUniqueTestEntity.getByNameAsync,
178
- args,
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.runInTransactionForDatabaseAdaptorFlavorAsync(
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.runInTransactionForDatabaseAdaptorFlavorAsync(
196
+ await vc1.runInTransactionForDatabaseAdapterFlavorAsync(
203
197
  'postgres',
204
198
  async (queryContext) => {
205
199
  return await createWithUniqueConstraintRecoveryAsync(