@expo/entity-database-adapter-knex 0.63.0 → 0.64.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 +1 -1
- package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +18 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +28 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +19 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +37 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +2 -2
- package/build/src/BaseSQLQueryBuilder.js +2 -2
- package/build/src/EnforcingKnexEntityLoader.d.ts +18 -0
- package/build/src/EnforcingKnexEntityLoader.js +22 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +4 -0
- package/build/src/PostgresEntityDatabaseAdapter.js +24 -10
- package/build/src/SQLOperator.d.ts +15 -14
- package/build/src/SQLOperator.js +11 -8
- package/build/src/internal/EntityKnexDataManager.d.ts +2 -0
- package/build/src/internal/EntityKnexDataManager.js +13 -10
- package/package.json +16 -16
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +36 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +67 -0
- package/src/BaseSQLQueryBuilder.ts +2 -2
- package/src/EnforcingKnexEntityLoader.ts +26 -0
- package/src/PostgresEntityDatabaseAdapter.ts +68 -20
- package/src/SQLOperator.ts +23 -22
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +97 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +17 -0
- package/src/__tests__/SQLOperator-test.ts +2 -29
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +25 -1
- package/src/internal/EntityKnexDataManager.ts +38 -9
|
@@ -400,6 +400,42 @@ export class AuthorizationResultBasedKnexEntityLoader<
|
|
|
400
400
|
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Count entities matching the conjunction of field equality operands.
|
|
405
|
+
* This does not perform authorization since count does not load full entities.
|
|
406
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
407
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
408
|
+
*
|
|
409
|
+
* @returns count of entities matching the filters
|
|
410
|
+
*/
|
|
411
|
+
async countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
|
412
|
+
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
|
413
|
+
): Promise<number> {
|
|
414
|
+
for (const fieldEqualityOperand of fieldEqualityOperands) {
|
|
415
|
+
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
|
|
416
|
+
? [fieldEqualityOperand.fieldValue]
|
|
417
|
+
: fieldEqualityOperand.fieldValues;
|
|
418
|
+
this.constructionUtils.validateFieldAndValues(fieldEqualityOperand.fieldName, fieldValues);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return await this.knexDataManager.countByFieldEqualityConjunctionAsync(
|
|
422
|
+
this.queryContext,
|
|
423
|
+
fieldEqualityOperands,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Count entities matching a SQL fragment.
|
|
429
|
+
* This does not perform authorization since count does not load full entities.
|
|
430
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
431
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
432
|
+
*
|
|
433
|
+
* @returns count of entities matching the query
|
|
434
|
+
*/
|
|
435
|
+
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
|
|
436
|
+
return await this.knexDataManager.countBySQLFragmentAsync(this.queryContext, fragment);
|
|
437
|
+
}
|
|
438
|
+
|
|
403
439
|
/**
|
|
404
440
|
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
|
|
405
441
|
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.
|
|
@@ -248,6 +248,73 @@ export abstract class BasePostgresEntityDatabaseAdapter<
|
|
|
248
248
|
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
249
249
|
): Promise<object[]>;
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Count objects matching the conjunction of where clauses constructed from
|
|
253
|
+
* specified field equality operands.
|
|
254
|
+
*
|
|
255
|
+
* @param queryContext - query context with which to perform the count
|
|
256
|
+
* @param fieldEqualityOperands - list of field equality where clause operand specifications
|
|
257
|
+
* @returns count of objects matching the query
|
|
258
|
+
*/
|
|
259
|
+
async countByFieldEqualityConjunctionAsync<N extends keyof TFields>(
|
|
260
|
+
queryContext: EntityQueryContext,
|
|
261
|
+
fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[],
|
|
262
|
+
): Promise<number> {
|
|
263
|
+
const tableFieldSingleValueOperands: TableFieldSingleValueEqualityCondition[] = [];
|
|
264
|
+
const tableFieldMultipleValueOperands: TableFieldMultiValueEqualityCondition[] = [];
|
|
265
|
+
for (const operand of fieldEqualityOperands) {
|
|
266
|
+
if (isSingleValueFieldEqualityCondition(operand)) {
|
|
267
|
+
tableFieldSingleValueOperands.push({
|
|
268
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
269
|
+
tableValue: operand.fieldValue,
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
tableFieldMultipleValueOperands.push({
|
|
273
|
+
tableField: getDatabaseFieldForEntityField(this.entityConfiguration, operand.fieldName),
|
|
274
|
+
tableValues: operand.fieldValues,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return await this.countByFieldEqualityConjunctionInternalAsync(
|
|
280
|
+
queryContext.getQueryInterface(),
|
|
281
|
+
this.entityConfiguration.tableName,
|
|
282
|
+
tableFieldSingleValueOperands,
|
|
283
|
+
tableFieldMultipleValueOperands,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
protected abstract countByFieldEqualityConjunctionInternalAsync(
|
|
288
|
+
queryInterface: Knex,
|
|
289
|
+
tableName: string,
|
|
290
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
291
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
292
|
+
): Promise<number>;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Count objects matching the SQL fragment.
|
|
296
|
+
*
|
|
297
|
+
* @param queryContext - query context with which to perform the count
|
|
298
|
+
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
299
|
+
* @returns count of objects matching the query
|
|
300
|
+
*/
|
|
301
|
+
async countBySQLFragmentAsync(
|
|
302
|
+
queryContext: EntityQueryContext,
|
|
303
|
+
sqlFragment: SQLFragment<TFields>,
|
|
304
|
+
): Promise<number> {
|
|
305
|
+
return await this.countBySQLFragmentInternalAsync(
|
|
306
|
+
queryContext.getQueryInterface(),
|
|
307
|
+
this.entityConfiguration.tableName,
|
|
308
|
+
sqlFragment,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
protected abstract countBySQLFragmentInternalAsync(
|
|
313
|
+
queryInterface: Knex,
|
|
314
|
+
tableName: string,
|
|
315
|
+
sqlFragment: SQLFragment<TFields>,
|
|
316
|
+
): Promise<number>;
|
|
317
|
+
|
|
251
318
|
private convertToTableQueryModifiers(
|
|
252
319
|
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
253
320
|
): TableQuerySelectionModifiers<TFields> {
|
|
@@ -47,7 +47,7 @@ export abstract class BaseSQLQueryBuilder<
|
|
|
47
47
|
orderBy(
|
|
48
48
|
fieldName: TSelectedFields,
|
|
49
49
|
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
|
50
|
-
nulls
|
|
50
|
+
nulls?: NullsOrdering,
|
|
51
51
|
): this {
|
|
52
52
|
this.modifiers.orderBy = [...(this.modifiers.orderBy ?? []), { fieldName, order, nulls }];
|
|
53
53
|
return this;
|
|
@@ -74,7 +74,7 @@ export abstract class BaseSQLQueryBuilder<
|
|
|
74
74
|
orderBySQL(
|
|
75
75
|
fragment: SQLFragment<Pick<TFields, TSelectedFields>>,
|
|
76
76
|
order: OrderByOrdering = OrderByOrdering.ASCENDING,
|
|
77
|
-
nulls
|
|
77
|
+
nulls?: NullsOrdering,
|
|
78
78
|
): this {
|
|
79
79
|
this.modifiers.orderBy = [
|
|
80
80
|
...(this.modifiers.orderBy ?? []),
|
|
@@ -104,6 +104,32 @@ export class EnforcingKnexEntityLoader<
|
|
|
104
104
|
return entityResults.map((result) => result.enforceValue());
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Count entities matching the conjunction of field equality operands.
|
|
109
|
+
* This does not perform authorization since count does not load full entities.
|
|
110
|
+
* Note that this should be used with the same caution as loadManyByFieldEqualityConjunctionAsync
|
|
111
|
+
* regarding indexing since counts can be expensive on large datasets without appropriate indexes.
|
|
112
|
+
*
|
|
113
|
+
* @returns count of entities matching the filters
|
|
114
|
+
*/
|
|
115
|
+
async countByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
|
|
116
|
+
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
|
|
117
|
+
): Promise<number> {
|
|
118
|
+
return await this.knexEntityLoader.countByFieldEqualityConjunctionAsync(fieldEqualityOperands);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Count entities matching a SQL fragment.
|
|
123
|
+
* This does not perform authorization since count does not load full entity rows.
|
|
124
|
+
* Note that this should be used with the same caution as loadManyBySQL regarding indexing
|
|
125
|
+
* since counts can be expensive on large datasets without appropriate indexes.
|
|
126
|
+
*
|
|
127
|
+
* @returns count of entities matching the query
|
|
128
|
+
*/
|
|
129
|
+
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
|
|
130
|
+
return await this.knexEntityLoader.countBySQLAsync(fragment);
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/**
|
|
108
134
|
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
|
|
109
135
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityConfiguration, FieldTransformer, FieldTransformerMap } from '@expo/entity';
|
|
2
|
-
import { getDatabaseFieldForEntityField } from '@expo/entity';
|
|
2
|
+
import { getDatabaseFieldForEntityField, RESERVED_ENTITY_COUNT_QUERY_ALIAS } from '@expo/entity';
|
|
3
3
|
import type { Knex } from 'knex';
|
|
4
4
|
|
|
5
5
|
import type {
|
|
@@ -91,7 +91,7 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
91
91
|
.select()
|
|
92
92
|
.from(tableName)
|
|
93
93
|
.whereRaw(`(??) = ANY(?)`, [
|
|
94
|
-
tableColumns[0]
|
|
94
|
+
tableColumns[0]!,
|
|
95
95
|
tableTuples.map((tableTuple) => tableTuple[0]),
|
|
96
96
|
]),
|
|
97
97
|
);
|
|
@@ -164,14 +164,12 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
164
164
|
return ret;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
tableName: string,
|
|
167
|
+
private applyFieldEqualityConjunctionWhereClause(
|
|
168
|
+
query: Knex.QueryBuilder,
|
|
170
169
|
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
171
170
|
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let query = queryInterface.select().from(tableName);
|
|
171
|
+
): Knex.QueryBuilder {
|
|
172
|
+
let result = query;
|
|
175
173
|
|
|
176
174
|
if (tableFieldSingleValueEqualityOperands.length > 0) {
|
|
177
175
|
const whereObject: { [key: string]: any } = {};
|
|
@@ -184,11 +182,11 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
184
182
|
for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) {
|
|
185
183
|
whereObject[tableField] = tableValue;
|
|
186
184
|
}
|
|
187
|
-
|
|
185
|
+
result = result.where(whereObject);
|
|
188
186
|
}
|
|
189
187
|
if (nullTableFieldSingleValueEqualityOperands.length > 0) {
|
|
190
188
|
for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
|
|
191
|
-
|
|
189
|
+
result = result.whereNull(tableField);
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
}
|
|
@@ -196,7 +194,7 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
196
194
|
if (tableFieldMultiValueEqualityOperands.length > 0) {
|
|
197
195
|
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
|
|
198
196
|
const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null);
|
|
199
|
-
|
|
197
|
+
result = result.where((builder) => {
|
|
200
198
|
builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]);
|
|
201
199
|
// there was at least one null, allow null in this equality clause
|
|
202
200
|
if (nonNullTableValues.length !== tableValues.length) {
|
|
@@ -206,29 +204,79 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
206
204
|
}
|
|
207
205
|
}
|
|
208
206
|
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
|
211
|
+
queryInterface: Knex,
|
|
212
|
+
tableName: string,
|
|
213
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
214
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
215
|
+
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
216
|
+
): Promise<object[]> {
|
|
217
|
+
let query = this.applyFieldEqualityConjunctionWhereClause(
|
|
218
|
+
queryInterface.select().from(tableName),
|
|
219
|
+
tableFieldSingleValueEqualityOperands,
|
|
220
|
+
tableFieldMultiValueEqualityOperands,
|
|
221
|
+
);
|
|
209
222
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
210
223
|
return await wrapNativePostgresCallAsync(() => query);
|
|
211
224
|
}
|
|
212
225
|
|
|
226
|
+
private applySQLFragmentWhereClause(
|
|
227
|
+
query: Knex.QueryBuilder,
|
|
228
|
+
sqlFragment: SQLFragment<TFields>,
|
|
229
|
+
): Knex.QueryBuilder {
|
|
230
|
+
return query.whereRaw(
|
|
231
|
+
sqlFragment.sql,
|
|
232
|
+
sqlFragment.getKnexBindings((fieldName) =>
|
|
233
|
+
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
213
238
|
protected async fetchManyBySQLFragmentInternalAsync(
|
|
214
239
|
queryInterface: Knex,
|
|
215
240
|
tableName: string,
|
|
216
241
|
sqlFragment: SQLFragment<TFields>,
|
|
217
242
|
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
218
243
|
): Promise<object[]> {
|
|
219
|
-
let query =
|
|
220
|
-
.select()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
sqlFragment.sql,
|
|
224
|
-
sqlFragment.getKnexBindings((fieldName) =>
|
|
225
|
-
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
|
|
226
|
-
),
|
|
227
|
-
);
|
|
244
|
+
let query = this.applySQLFragmentWhereClause(
|
|
245
|
+
queryInterface.select().from(tableName),
|
|
246
|
+
sqlFragment,
|
|
247
|
+
);
|
|
228
248
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
229
249
|
return await wrapNativePostgresCallAsync(() => query);
|
|
230
250
|
}
|
|
231
251
|
|
|
252
|
+
protected async countByFieldEqualityConjunctionInternalAsync(
|
|
253
|
+
queryInterface: Knex,
|
|
254
|
+
tableName: string,
|
|
255
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
256
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
257
|
+
): Promise<number> {
|
|
258
|
+
const query = this.applyFieldEqualityConjunctionWhereClause(
|
|
259
|
+
queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName),
|
|
260
|
+
tableFieldSingleValueEqualityOperands,
|
|
261
|
+
tableFieldMultiValueEqualityOperands,
|
|
262
|
+
);
|
|
263
|
+
const result = await wrapNativePostgresCallAsync(() => query);
|
|
264
|
+
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected async countBySQLFragmentInternalAsync(
|
|
268
|
+
queryInterface: Knex,
|
|
269
|
+
tableName: string,
|
|
270
|
+
sqlFragment: SQLFragment<TFields>,
|
|
271
|
+
): Promise<number> {
|
|
272
|
+
const query = this.applySQLFragmentWhereClause(
|
|
273
|
+
queryInterface.count('*', { as: RESERVED_ENTITY_COUNT_QUERY_ALIAS }).from(tableName),
|
|
274
|
+
sqlFragment,
|
|
275
|
+
);
|
|
276
|
+
const result = await wrapNativePostgresCallAsync(() => query);
|
|
277
|
+
return parseInt(String(result[0][RESERVED_ENTITY_COUNT_QUERY_ALIAS]), 10);
|
|
278
|
+
}
|
|
279
|
+
|
|
232
280
|
protected async insertInternalAsync(
|
|
233
281
|
queryInterface: Knex,
|
|
234
282
|
tableName: string,
|
package/src/SQLOperator.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'assert';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Supported SQL value types that can be safely parameterized.
|
|
@@ -12,7 +13,6 @@ export type SupportedSQLValue =
|
|
|
12
13
|
| Date
|
|
13
14
|
| Buffer
|
|
14
15
|
| bigint
|
|
15
|
-
| undefined // Will be treated as NULL
|
|
16
16
|
| readonly SupportedSQLValue[] // For IN clauses and array types
|
|
17
17
|
| Readonly<{ [key: string]: unknown }>; // For JSON/JSONB columns
|
|
18
18
|
|
|
@@ -41,7 +41,7 @@ export class SQLFragment<TFields extends Record<string, any>> {
|
|
|
41
41
|
*/
|
|
42
42
|
getKnexBindings(
|
|
43
43
|
getColumnForField: (fieldName: keyof TFields) => string,
|
|
44
|
-
): readonly
|
|
44
|
+
): readonly Knex.RawBinding[] {
|
|
45
45
|
return this.bindings.map((b) => {
|
|
46
46
|
switch (b.type) {
|
|
47
47
|
case 'entityField':
|
|
@@ -49,7 +49,10 @@ export class SQLFragment<TFields extends Record<string, any>> {
|
|
|
49
49
|
case 'identifier':
|
|
50
50
|
return b.name;
|
|
51
51
|
case 'value':
|
|
52
|
-
|
|
52
|
+
// Needs a cast since bigint is supported by knex postgres dialect but not all dialects, and thus isn't included
|
|
53
|
+
// in the type. Because we only use the postgres dialect in this adapter, it's safe to allow it here.
|
|
54
|
+
// https://github.com/knex/knex/issues/5013#issuecomment-3368744254
|
|
55
|
+
return b.value as Knex.RawBinding;
|
|
53
56
|
}
|
|
54
57
|
});
|
|
55
58
|
}
|
|
@@ -133,8 +136,8 @@ export class SQLFragment<TFields extends Record<string, any>> {
|
|
|
133
136
|
* Handles all SupportedSQLValue types.
|
|
134
137
|
*/
|
|
135
138
|
private static formatDebugValue(value: SupportedSQLValue): string {
|
|
136
|
-
// Handle null
|
|
137
|
-
if (value === null
|
|
139
|
+
// Handle null
|
|
140
|
+
if (value === null) {
|
|
138
141
|
return 'NULL';
|
|
139
142
|
}
|
|
140
143
|
|
|
@@ -303,7 +306,7 @@ export function sql<TFields extends Record<string, any>>(
|
|
|
303
306
|
strings.forEach((string, i) => {
|
|
304
307
|
sqlString += string;
|
|
305
308
|
if (i < values.length) {
|
|
306
|
-
const value = values[i]
|
|
309
|
+
const value = values[i]!;
|
|
307
310
|
|
|
308
311
|
if (value instanceof SQLFragment) {
|
|
309
312
|
// Handle nested SQL fragments
|
|
@@ -344,7 +347,7 @@ type PickSupportedSQLValueKeys<T> = {
|
|
|
344
347
|
}[keyof T];
|
|
345
348
|
|
|
346
349
|
type PickStringValueKeys<T> = {
|
|
347
|
-
[K in keyof T]: T[K] extends string | null
|
|
350
|
+
[K in keyof T]: T[K] extends string | null ? K : never;
|
|
348
351
|
}[keyof T];
|
|
349
352
|
|
|
350
353
|
type JsonSerializable =
|
|
@@ -368,13 +371,13 @@ export class SQLChainableFragment<
|
|
|
368
371
|
> extends SQLFragment<TFields> {
|
|
369
372
|
/**
|
|
370
373
|
* Generates an equality condition (`= value`).
|
|
371
|
-
* Automatically converts `null
|
|
374
|
+
* Automatically converts `null` to `IS NULL`.
|
|
372
375
|
*
|
|
373
376
|
* @param value - The value to compare against
|
|
374
377
|
* @returns A {@link SQLFragment} representing the equality condition
|
|
375
378
|
*/
|
|
376
|
-
eq(value: TValue | null
|
|
377
|
-
if (value === null
|
|
379
|
+
eq(value: TValue | null): SQLFragment<TFields> {
|
|
380
|
+
if (value === null) {
|
|
378
381
|
return this.isNull();
|
|
379
382
|
}
|
|
380
383
|
return sql`${this} = ${value}`;
|
|
@@ -382,13 +385,13 @@ export class SQLChainableFragment<
|
|
|
382
385
|
|
|
383
386
|
/**
|
|
384
387
|
* Generates an inequality condition (`!= value`).
|
|
385
|
-
* Automatically converts `null
|
|
388
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
386
389
|
*
|
|
387
390
|
* @param value - The value to compare against
|
|
388
391
|
* @returns A {@link SQLFragment} representing the inequality condition
|
|
389
392
|
*/
|
|
390
|
-
neq(value: TValue | null
|
|
391
|
-
if (value === null
|
|
393
|
+
neq(value: TValue | null): SQLFragment<TFields> {
|
|
394
|
+
if (value === null) {
|
|
392
395
|
return this.isNotNull();
|
|
393
396
|
}
|
|
394
397
|
return sql`${this} != ${value}`;
|
|
@@ -635,9 +638,7 @@ type ExtractFragmentFields<T> = T extends SQLFragment<infer F> ? F : never;
|
|
|
635
638
|
// Conditional value types for expression overloads.
|
|
636
639
|
// Uses SQLChainableFragment<any, ...> so that TExpr alone drives inference (single type param).
|
|
637
640
|
type FragmentValueNullable<TFragment> =
|
|
638
|
-
TFragment extends SQLChainableFragment<any, infer TValue>
|
|
639
|
-
? TValue | null | undefined
|
|
640
|
-
: SupportedSQLValue;
|
|
641
|
+
TFragment extends SQLChainableFragment<any, infer TValue> ? TValue | null : SupportedSQLValue;
|
|
641
642
|
|
|
642
643
|
type FragmentValue<TFragment> =
|
|
643
644
|
TFragment extends SQLChainableFragment<any, infer TValue> ? TValue : SupportedSQLValue;
|
|
@@ -950,7 +951,7 @@ function isNotNullHelper<TFields extends Record<string, any>>(
|
|
|
950
951
|
|
|
951
952
|
/**
|
|
952
953
|
* Generates an equality condition (`= value`) from a fragment.
|
|
953
|
-
* Automatically converts `null
|
|
954
|
+
* Automatically converts `null` to `IS NULL`.
|
|
954
955
|
*
|
|
955
956
|
* @param fragment - A SQLFragment or SQLChainableFragment to compare
|
|
956
957
|
* @param value - The value to compare against
|
|
@@ -961,7 +962,7 @@ function eqHelper<TFragment extends SQLFragment<any>>(
|
|
|
961
962
|
): SQLFragment<ExtractFragmentFields<TFragment>>;
|
|
962
963
|
/**
|
|
963
964
|
* Generates an equality condition (`= value`) from a field name.
|
|
964
|
-
* Automatically converts `null
|
|
965
|
+
* Automatically converts `null` to `IS NULL`.
|
|
965
966
|
*
|
|
966
967
|
* @param fieldName - The entity field name to compare
|
|
967
968
|
* @param value - The value to compare against
|
|
@@ -979,7 +980,7 @@ function eqHelper<TFields extends Record<string, any>>(
|
|
|
979
980
|
|
|
980
981
|
/**
|
|
981
982
|
* Generates an inequality condition (`!= value`) from a fragment.
|
|
982
|
-
* Automatically converts `null
|
|
983
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
983
984
|
*
|
|
984
985
|
* @param fragment - A SQLFragment or SQLChainableFragment to compare
|
|
985
986
|
* @param value - The value to compare against
|
|
@@ -990,7 +991,7 @@ function neqHelper<TFragment extends SQLFragment<any>>(
|
|
|
990
991
|
): SQLFragment<ExtractFragmentFields<TFragment>>;
|
|
991
992
|
/**
|
|
992
993
|
* Generates an inequality condition (`!= value`) from a field name.
|
|
993
|
-
* Automatically converts `null
|
|
994
|
+
* Automatically converts `null` to `IS NOT NULL`.
|
|
994
995
|
*
|
|
995
996
|
* @param fieldName - The entity field name to compare
|
|
996
997
|
* @param value - The value to compare against
|
|
@@ -1521,12 +1522,12 @@ export const SQLExpression = {
|
|
|
1521
1522
|
isNotNull: isNotNullHelper,
|
|
1522
1523
|
|
|
1523
1524
|
/**
|
|
1524
|
-
* Equality operator. Automatically converts null
|
|
1525
|
+
* Equality operator. Automatically converts null to IS NULL.
|
|
1525
1526
|
*/
|
|
1526
1527
|
eq: eqHelper,
|
|
1527
1528
|
|
|
1528
1529
|
/**
|
|
1529
|
-
* Inequality operator. Automatically converts null
|
|
1530
|
+
* Inequality operator. Automatically converts null to IS NOT NULL.
|
|
1530
1531
|
*/
|
|
1531
1532
|
neq: neqHelper,
|
|
1532
1533
|
|
|
@@ -959,6 +959,103 @@ describe('postgres entity integration', () => {
|
|
|
959
959
|
});
|
|
960
960
|
});
|
|
961
961
|
|
|
962
|
+
describe('counting with countBySQLAsync', () => {
|
|
963
|
+
it('counts entities matching a SQL fragment', async () => {
|
|
964
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
965
|
+
|
|
966
|
+
await enforceAsyncResult(
|
|
967
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
968
|
+
.setField('name', 'Alice')
|
|
969
|
+
.setField('hasACat', true)
|
|
970
|
+
.setField('hasADog', false)
|
|
971
|
+
.createAsync(),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
await enforceAsyncResult(
|
|
975
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
976
|
+
.setField('name', 'Bob')
|
|
977
|
+
.setField('hasACat', false)
|
|
978
|
+
.setField('hasADog', true)
|
|
979
|
+
.createAsync(),
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
await enforceAsyncResult(
|
|
983
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
984
|
+
.setField('name', 'Charlie')
|
|
985
|
+
.setField('hasACat', true)
|
|
986
|
+
.setField('hasADog', true)
|
|
987
|
+
.createAsync(),
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
const catOwnerCount = await PostgresTestEntity.knexLoader(vc1).countBySQLAsync(
|
|
991
|
+
sql`has_a_cat = ${true}`,
|
|
992
|
+
);
|
|
993
|
+
expect(catOwnerCount).toBe(2);
|
|
994
|
+
|
|
995
|
+
const dogOwnerCount = await PostgresTestEntity.knexLoader(vc1).countBySQLAsync(
|
|
996
|
+
sql`has_a_dog = ${true}`,
|
|
997
|
+
);
|
|
998
|
+
expect(dogOwnerCount).toBe(2);
|
|
999
|
+
|
|
1000
|
+
const allCount = await PostgresTestEntity.knexLoader(vc1).countBySQLAsync(sql`TRUE`);
|
|
1001
|
+
expect(allCount).toBe(3);
|
|
1002
|
+
|
|
1003
|
+
const noneCount = await PostgresTestEntity.knexLoader(vc1).countBySQLAsync(sql`FALSE`);
|
|
1004
|
+
expect(noneCount).toBe(0);
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
describe('counting with countByFieldEqualityConjunctionAsync', () => {
|
|
1009
|
+
it('counts entities matching field equality conditions', async () => {
|
|
1010
|
+
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
1011
|
+
|
|
1012
|
+
await enforceAsyncResult(
|
|
1013
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1014
|
+
.setField('name', 'hello')
|
|
1015
|
+
.setField('hasACat', false)
|
|
1016
|
+
.setField('hasADog', true)
|
|
1017
|
+
.createAsync(),
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
await enforceAsyncResult(
|
|
1021
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1022
|
+
.setField('name', 'world')
|
|
1023
|
+
.setField('hasACat', false)
|
|
1024
|
+
.setField('hasADog', true)
|
|
1025
|
+
.createAsync(),
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
await enforceAsyncResult(
|
|
1029
|
+
PostgresTestEntity.creatorWithAuthorizationResults(vc1)
|
|
1030
|
+
.setField('name', 'wat')
|
|
1031
|
+
.setField('hasACat', false)
|
|
1032
|
+
.setField('hasADog', false)
|
|
1033
|
+
.createAsync(),
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
const count1 = await PostgresTestEntity.knexLoader(vc1).countByFieldEqualityConjunctionAsync([
|
|
1037
|
+
{ fieldName: 'hasACat', fieldValue: false },
|
|
1038
|
+
{ fieldName: 'hasADog', fieldValue: true },
|
|
1039
|
+
]);
|
|
1040
|
+
expect(count1).toBe(2);
|
|
1041
|
+
|
|
1042
|
+
const count2 = await PostgresTestEntity.knexLoader(vc1).countByFieldEqualityConjunctionAsync([
|
|
1043
|
+
{ fieldName: 'hasADog', fieldValues: [true, false] },
|
|
1044
|
+
]);
|
|
1045
|
+
expect(count2).toBe(3);
|
|
1046
|
+
|
|
1047
|
+
const count3 = await PostgresTestEntity.knexLoader(vc1).countByFieldEqualityConjunctionAsync([
|
|
1048
|
+
{ fieldName: 'name', fieldValue: 'hello' },
|
|
1049
|
+
]);
|
|
1050
|
+
expect(count3).toBe(1);
|
|
1051
|
+
|
|
1052
|
+
const count4 = await PostgresTestEntity.knexLoader(vc1).countByFieldEqualityConjunctionAsync(
|
|
1053
|
+
[],
|
|
1054
|
+
);
|
|
1055
|
+
expect(count4).toBe(3);
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
|
|
962
1059
|
describe('conjunction field equality loading', () => {
|
|
963
1060
|
it('supports single fieldValue and multiple fieldValues', async () => {
|
|
964
1061
|
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
|
|
@@ -89,6 +89,23 @@ class TestEntityDatabaseAdapter extends BasePostgresEntityDatabaseAdapter<
|
|
|
89
89
|
return this.fetchEqualityConditionResults;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
protected async countByFieldEqualityConjunctionInternalAsync(
|
|
93
|
+
_queryInterface: any,
|
|
94
|
+
_tableName: string,
|
|
95
|
+
_tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
96
|
+
_tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
97
|
+
): Promise<number> {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected async countBySQLFragmentInternalAsync(
|
|
102
|
+
_queryInterface: any,
|
|
103
|
+
_tableName: string,
|
|
104
|
+
_sqlFragment: any,
|
|
105
|
+
): Promise<number> {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
protected async insertInternalAsync(
|
|
93
110
|
_queryInterface: any,
|
|
94
111
|
_tableName: string,
|
|
@@ -258,12 +258,11 @@ describe('SQLOperator', () => {
|
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
it('handles all SupportedSQLValue types in getDebugString', () => {
|
|
261
|
-
const fragment = new SQLFragment('INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
261
|
+
const fragment = new SQLFragment('INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
262
262
|
{ type: 'value', value: 'string' },
|
|
263
263
|
{ type: 'value', value: 123 },
|
|
264
264
|
{ type: 'value', value: true },
|
|
265
265
|
{ type: 'value', value: null },
|
|
266
|
-
{ type: 'value', value: undefined },
|
|
267
266
|
{ type: 'value', value: new Date('2024-01-01T00:00:00.000Z') },
|
|
268
267
|
{ type: 'value', value: Buffer.from('hello') },
|
|
269
268
|
{ type: 'value', value: BigInt(999) },
|
|
@@ -272,7 +271,7 @@ describe('SQLOperator', () => {
|
|
|
272
271
|
|
|
273
272
|
const text = fragment.getDebugString();
|
|
274
273
|
expect(text).toBe(
|
|
275
|
-
"INSERT INTO test VALUES ('string', 123, TRUE, NULL,
|
|
274
|
+
"INSERT INTO test VALUES ('string', 123, TRUE, NULL, '2024-01-01T00:00:00.000Z', '\\x68656c6c6f', 999, ARRAY[1, 2, 3])",
|
|
276
275
|
);
|
|
277
276
|
});
|
|
278
277
|
|
|
@@ -763,13 +762,6 @@ describe('SQLOperator', () => {
|
|
|
763
762
|
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
764
763
|
});
|
|
765
764
|
|
|
766
|
-
it('handles undefined in equality check', () => {
|
|
767
|
-
const fragment = SQLExpression.eq('nullableField', undefined);
|
|
768
|
-
|
|
769
|
-
expect(fragment.sql).toBe('?? IS NULL');
|
|
770
|
-
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
765
|
it('accepts a SQLFragment expression', () => {
|
|
774
766
|
const fragment = SQLExpression.eq(sql<TestFields>`${entityField('stringField')}`, 'active');
|
|
775
767
|
expect(fragment.sql).toBe('?? = ?');
|
|
@@ -801,13 +793,6 @@ describe('SQLOperator', () => {
|
|
|
801
793
|
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
802
794
|
});
|
|
803
795
|
|
|
804
|
-
it('handles undefined in inequality check', () => {
|
|
805
|
-
const fragment = SQLExpression.neq('nullableField', undefined);
|
|
806
|
-
|
|
807
|
-
expect(fragment.sql).toBe('?? IS NOT NULL');
|
|
808
|
-
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
809
|
-
});
|
|
810
|
-
|
|
811
796
|
it('accepts a SQLFragment expression', () => {
|
|
812
797
|
const fragment = SQLExpression.neq(
|
|
813
798
|
sql<TestFields>`${entityField('stringField')}`,
|
|
@@ -1131,12 +1116,6 @@ describe('SQLOperator', () => {
|
|
|
1131
1116
|
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
|
|
1132
1117
|
});
|
|
1133
1118
|
|
|
1134
|
-
it('eq(undefined) uses IS NULL', () => {
|
|
1135
|
-
const fragment = makeExpr<string>(stringFieldFragment()).eq(undefined);
|
|
1136
|
-
expect(fragment.sql).toBe('?? IS NULL');
|
|
1137
|
-
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
1119
|
it('neq(value)', () => {
|
|
1141
1120
|
const fragment = makeExpr<string>(stringFieldFragment()).neq('deleted');
|
|
1142
1121
|
expect(fragment.sql).toBe('?? != ?');
|
|
@@ -1149,12 +1128,6 @@ describe('SQLOperator', () => {
|
|
|
1149
1128
|
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
|
|
1150
1129
|
});
|
|
1151
1130
|
|
|
1152
|
-
it('neq(undefined) uses IS NOT NULL', () => {
|
|
1153
|
-
const fragment = makeExpr<string>(stringFieldFragment()).neq(undefined);
|
|
1154
|
-
expect(fragment.sql).toBe('?? IS NOT NULL');
|
|
1155
|
-
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
1131
|
it('gt(value)', () => {
|
|
1159
1132
|
const fragment = makeExpr<number>(intFieldFragment()).gt(10);
|
|
1160
1133
|
expect(fragment.sql).toBe('?? > ?');
|