@expo/entity-database-adapter-knex 0.62.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 +15 -5
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +24 -7
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +19 -11
- package/build/src/BasePostgresEntityDatabaseAdapter.js +37 -13
- package/build/src/BaseSQLQueryBuilder.d.ts +2 -2
- package/build/src/BaseSQLQueryBuilder.js +2 -2
- package/build/src/EnforcingKnexEntityLoader.d.ts +14 -28
- package/build/src/EnforcingKnexEntityLoader.js +17 -30
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +7 -2
- package/build/src/PostgresEntityDatabaseAdapter.js +25 -15
- package/build/src/SQLOperator.d.ts +15 -14
- package/build/src/SQLOperator.js +11 -8
- package/build/src/internal/EntityKnexDataManager.d.ts +2 -10
- package/build/src/internal/EntityKnexDataManager.js +12 -22
- package/package.json +16 -16
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +29 -14
- package/src/BasePostgresEntityDatabaseAdapter.ts +60 -29
- package/src/BaseSQLQueryBuilder.ts +2 -2
- package/src/EnforcingKnexEntityLoader.ts +20 -38
- package/src/PostgresEntityDatabaseAdapter.ts +66 -29
- package/src/SQLOperator.ts +23 -22
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +140 -116
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +0 -75
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +21 -28
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +0 -52
- package/src/__tests__/SQLOperator-test.ts +2 -29
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +26 -12
- package/src/internal/EntityKnexDataManager.ts +28 -31
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +1 -57
|
@@ -88,17 +88,9 @@ export declare class EntityKnexDataManager<TFields extends Record<string, any>,
|
|
|
88
88
|
* @returns array of objects matching the query
|
|
89
89
|
*/
|
|
90
90
|
loadManyByFieldEqualityConjunctionAsync<N extends keyof TFields>(queryContext: EntityQueryContext, fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[], querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
91
|
-
|
|
92
|
-
* Loads many objects matching the raw WHERE clause.
|
|
93
|
-
*
|
|
94
|
-
* @param queryContext - query context in which to perform the load
|
|
95
|
-
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
|
|
96
|
-
* @param bindings - array of positional bindings or object of named bindings
|
|
97
|
-
* @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
|
|
98
|
-
* @returns array of objects matching the query
|
|
99
|
-
*/
|
|
100
|
-
loadManyByRawWhereClauseAsync(queryContext: EntityQueryContext, rawWhereClause: string, bindings: readonly any[] | object, querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
91
|
+
countByFieldEqualityConjunctionAsync<N extends keyof TFields>(queryContext: EntityQueryContext, fieldEqualityOperands: readonly FieldEqualityCondition<TFields, N>[]): Promise<number>;
|
|
101
92
|
loadManyBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>, querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>): Promise<readonly Readonly<TFields>[]>;
|
|
93
|
+
countBySQLFragmentAsync(queryContext: EntityQueryContext, sqlFragment: SQLFragment<TFields>): Promise<number>;
|
|
102
94
|
/**
|
|
103
95
|
* Load a page of objects using cursor-based pagination with unified pagination specification.
|
|
104
96
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDatabaseAdapterPaginationCursorInvalidError, EntityMetricsLoadType, getDatabaseFieldForEntityField, timeAndLogLoadEventAsync, } from '@expo/entity';
|
|
1
|
+
import { EntityDatabaseAdapterPaginationCursorInvalidError, EntityMetricsLoadType, getDatabaseFieldForEntityField, timeAndLogCountEventAsync, timeAndLogLoadEventAsync, } from '@expo/entity';
|
|
2
2
|
import assert from 'assert';
|
|
3
3
|
import { NullsOrdering, OrderByOrdering } from "../BasePostgresEntityDatabaseAdapter.js";
|
|
4
4
|
import { PaginationStrategy } from "../PaginationStrategy.js";
|
|
@@ -42,23 +42,16 @@ export class EntityKnexDataManager {
|
|
|
42
42
|
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
|
|
43
43
|
return await timeAndLogLoadEventAsync(this.metricsAdapter, EntityMetricsLoadType.LOAD_MANY_EQUALITY_CONJUNCTION, this.entityClassName, queryContext)(this.databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands, querySelectionModifiers));
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*
|
|
48
|
-
* @param queryContext - query context in which to perform the load
|
|
49
|
-
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
|
|
50
|
-
* @param bindings - array of positional bindings or object of named bindings
|
|
51
|
-
* @param querySelectionModifiers - limit, offset, orderBy, and orderByRaw for the query
|
|
52
|
-
* @returns array of objects matching the query
|
|
53
|
-
*/
|
|
54
|
-
async loadManyByRawWhereClauseAsync(queryContext, rawWhereClause, bindings, querySelectionModifiers) {
|
|
55
|
-
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
|
|
56
|
-
return await timeAndLogLoadEventAsync(this.metricsAdapter, EntityMetricsLoadType.LOAD_MANY_RAW, this.entityClassName, queryContext)(this.databaseAdapter.fetchManyByRawWhereClauseAsync(queryContext, rawWhereClause, bindings, querySelectionModifiers));
|
|
45
|
+
async countByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands) {
|
|
46
|
+
return await timeAndLogCountEventAsync(this.metricsAdapter, EntityMetricsLoadType.COUNT_EQUALITY_CONJUNCTION, this.entityClassName, queryContext)(this.databaseAdapter.countByFieldEqualityConjunctionAsync(queryContext, fieldEqualityOperands));
|
|
57
47
|
}
|
|
58
48
|
async loadManyBySQLFragmentAsync(queryContext, sqlFragment, querySelectionModifiers) {
|
|
59
49
|
EntityKnexDataManager.validateOrderByClauses(querySelectionModifiers.orderBy);
|
|
60
50
|
return await timeAndLogLoadEventAsync(this.metricsAdapter, EntityMetricsLoadType.LOAD_MANY_SQL, this.entityClassName, queryContext)(this.databaseAdapter.fetchManyBySQLFragmentAsync(queryContext, sqlFragment, querySelectionModifiers));
|
|
61
51
|
}
|
|
52
|
+
async countBySQLFragmentAsync(queryContext, sqlFragment) {
|
|
53
|
+
return await timeAndLogCountEventAsync(this.metricsAdapter, EntityMetricsLoadType.COUNT_SQL, this.entityClassName, queryContext)(this.databaseAdapter.countBySQLFragmentAsync(queryContext, sqlFragment));
|
|
54
|
+
}
|
|
62
55
|
/**
|
|
63
56
|
* Load a page of objects using cursor-based pagination with unified pagination specification.
|
|
64
57
|
*
|
|
@@ -223,17 +216,14 @@ export class EntityKnexDataManager {
|
|
|
223
216
|
};
|
|
224
217
|
}
|
|
225
218
|
combineWhereConditions(baseWhere, cursorCondition) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return sql `TRUE`;
|
|
219
|
+
if (!baseWhere) {
|
|
220
|
+
return cursorCondition ?? sql `TRUE`;
|
|
229
221
|
}
|
|
230
|
-
if (
|
|
231
|
-
return
|
|
222
|
+
if (!cursorCondition) {
|
|
223
|
+
return baseWhere;
|
|
232
224
|
}
|
|
233
|
-
// Wrap baseWhere in parens
|
|
234
|
-
|
|
235
|
-
const [first, second] = conditions;
|
|
236
|
-
return sql `(${first}) AND ${second}`;
|
|
225
|
+
// Wrap baseWhere in parens when combining with cursor condition
|
|
226
|
+
return sql `(${baseWhere}) AND ${cursorCondition}`;
|
|
237
227
|
}
|
|
238
228
|
augmentOrderByIfNecessary(orderBy, idField) {
|
|
239
229
|
const clauses = orderBy ?? [];
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity-database-adapter-knex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"description": "Knex database adapter for @expo/entity",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"entity"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Expo",
|
|
5
10
|
"files": [
|
|
6
11
|
"build",
|
|
7
12
|
"!*.tsbuildinfo",
|
|
8
13
|
"!__*",
|
|
9
14
|
"src"
|
|
10
15
|
],
|
|
16
|
+
"type": "module",
|
|
11
17
|
"main": "build/src/index.js",
|
|
12
18
|
"types": "build/src/index.d.ts",
|
|
13
19
|
"scripts": {
|
|
14
20
|
"build": "tsc --build",
|
|
15
21
|
"prepack": "rm -rf build && yarn build",
|
|
16
22
|
"clean": "yarn build --clean",
|
|
17
|
-
"lint": "yarn run --top-level
|
|
23
|
+
"lint": "yarn run --top-level oxlint --type-aware src",
|
|
18
24
|
"lint-fix": "yarn lint --fix",
|
|
19
25
|
"test": "yarn test:all --rootDir $(pwd)",
|
|
20
26
|
"integration": "yarn integration:all --rootDir $(pwd)"
|
|
21
27
|
},
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=18"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"entity"
|
|
27
|
-
],
|
|
28
|
-
"author": "Expo",
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"type": "module",
|
|
31
28
|
"dependencies": {
|
|
32
|
-
"@expo/entity": "^0.
|
|
33
|
-
"knex": "^3.
|
|
29
|
+
"@expo/entity": "^0.64.0",
|
|
30
|
+
"knex": "^3.2.9"
|
|
34
31
|
},
|
|
35
32
|
"devDependencies": {
|
|
36
|
-
"@expo/entity-testing-utils": "^0.
|
|
33
|
+
"@expo/entity-testing-utils": "^0.64.0",
|
|
37
34
|
"@jest/globals": "30.3.0",
|
|
38
35
|
"pg": "8.20.0",
|
|
39
36
|
"ts-mockito": "2.6.1",
|
|
40
|
-
"typescript": "
|
|
37
|
+
"typescript": "6.0.3"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "3f10a9e70eab45ae95acdae133055d8a3ec04ce8"
|
|
43
43
|
}
|
|
@@ -401,24 +401,39 @@ export class AuthorizationResultBasedKnexEntityLoader<
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
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.
|
|
407
408
|
*
|
|
408
|
-
* @
|
|
409
|
+
* @returns count of entities matching the filters
|
|
409
410
|
*/
|
|
410
|
-
async
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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(
|
|
416
422
|
this.queryContext,
|
|
417
|
-
|
|
418
|
-
bindings,
|
|
419
|
-
querySelectionModifiers,
|
|
423
|
+
fieldEqualityOperands,
|
|
420
424
|
);
|
|
421
|
-
|
|
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);
|
|
422
437
|
}
|
|
423
438
|
|
|
424
439
|
/**
|
|
@@ -217,25 +217,22 @@ export abstract class BasePostgresEntityDatabaseAdapter<
|
|
|
217
217
|
): Promise<object[]>;
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
* Fetch many objects matching the
|
|
220
|
+
* Fetch many objects matching the SQL fragment.
|
|
221
221
|
*
|
|
222
222
|
* @param queryContext - query context with which to perform the fetch
|
|
223
|
-
* @param
|
|
224
|
-
* @param
|
|
225
|
-
* @param querySelectionModifiers - limit, offset, and orderBy for the query
|
|
223
|
+
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
224
|
+
* @param querySelectionModifiers - limit, offset, and orderByFragment for the query
|
|
226
225
|
* @returns array of objects matching the query
|
|
227
226
|
*/
|
|
228
|
-
async
|
|
227
|
+
async fetchManyBySQLFragmentAsync(
|
|
229
228
|
queryContext: EntityQueryContext,
|
|
230
|
-
|
|
231
|
-
bindings: any[] | object,
|
|
229
|
+
sqlFragment: SQLFragment<TFields>,
|
|
232
230
|
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
|
|
233
231
|
): Promise<readonly Readonly<TFields>[]> {
|
|
234
|
-
const results = await this.
|
|
232
|
+
const results = await this.fetchManyBySQLFragmentInternalAsync(
|
|
235
233
|
queryContext.getQueryInterface(),
|
|
236
234
|
this.entityConfiguration.tableName,
|
|
237
|
-
|
|
238
|
-
bindings,
|
|
235
|
+
sqlFragment,
|
|
239
236
|
this.convertToTableQueryModifiers(querySelectionModifiers),
|
|
240
237
|
);
|
|
241
238
|
|
|
@@ -244,45 +241,79 @@ export abstract class BasePostgresEntityDatabaseAdapter<
|
|
|
244
241
|
);
|
|
245
242
|
}
|
|
246
243
|
|
|
247
|
-
protected abstract
|
|
244
|
+
protected abstract fetchManyBySQLFragmentInternalAsync(
|
|
248
245
|
queryInterface: Knex,
|
|
249
246
|
tableName: string,
|
|
250
|
-
|
|
251
|
-
bindings: object | any[],
|
|
247
|
+
sqlFragment: SQLFragment<TFields>,
|
|
252
248
|
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
253
249
|
): Promise<object[]>;
|
|
254
250
|
|
|
255
251
|
/**
|
|
256
|
-
*
|
|
252
|
+
* Count objects matching the conjunction of where clauses constructed from
|
|
253
|
+
* specified field equality operands.
|
|
257
254
|
*
|
|
258
|
-
* @param queryContext - query context with which to perform the
|
|
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
|
|
259
298
|
* @param sqlFragment - SQLFragment for the WHERE clause of the query
|
|
260
|
-
* @
|
|
261
|
-
* @returns array of objects matching the query
|
|
299
|
+
* @returns count of objects matching the query
|
|
262
300
|
*/
|
|
263
|
-
async
|
|
301
|
+
async countBySQLFragmentAsync(
|
|
264
302
|
queryContext: EntityQueryContext,
|
|
265
303
|
sqlFragment: SQLFragment<TFields>,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const results = await this.fetchManyBySQLFragmentInternalAsync(
|
|
304
|
+
): Promise<number> {
|
|
305
|
+
return await this.countBySQLFragmentInternalAsync(
|
|
269
306
|
queryContext.getQueryInterface(),
|
|
270
307
|
this.entityConfiguration.tableName,
|
|
271
308
|
sqlFragment,
|
|
272
|
-
this.convertToTableQueryModifiers(querySelectionModifiers),
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
return results.map((result) =>
|
|
276
|
-
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
|
|
277
309
|
);
|
|
278
310
|
}
|
|
279
311
|
|
|
280
|
-
protected abstract
|
|
312
|
+
protected abstract countBySQLFragmentInternalAsync(
|
|
281
313
|
queryInterface: Knex,
|
|
282
314
|
tableName: string,
|
|
283
315
|
sqlFragment: SQLFragment<TFields>,
|
|
284
|
-
|
|
285
|
-
): Promise<object[]>;
|
|
316
|
+
): Promise<number>;
|
|
286
317
|
|
|
287
318
|
private convertToTableQueryModifiers(
|
|
288
319
|
querySelectionModifiers: PostgresQuerySelectionModifiers<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 ?? []),
|
|
@@ -105,47 +105,29 @@ export class EnforcingKnexEntityLoader<
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
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.
|
|
109
112
|
*
|
|
110
|
-
* @
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* const entities = await ExampleEntity.loader(vc)
|
|
124
|
-
* .loadManyByRawWhereClauseAsync(
|
|
125
|
-
* '(column_1, column_2) IN ((?, ?), (?, ?))',
|
|
126
|
-
* [value1, value2, value3, value4],
|
|
127
|
-
* );
|
|
128
|
-
* ```
|
|
129
|
-
* @param rawWhereClause - SQL WHERE clause. Interpolated values should be specified as ?-placeholders or :key_name
|
|
130
|
-
* @param bindings - values to bind to the placeholders in the WHERE clause
|
|
131
|
-
* @param querySelectionModifiers - limit, offset, and orderBy for the query.
|
|
132
|
-
* @returns entities matching the WHERE clause
|
|
133
|
-
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
|
134
|
-
* @throws Error when rawWhereClause or bindings are invalid
|
|
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.
|
|
135
126
|
*
|
|
136
|
-
* @
|
|
127
|
+
* @returns count of entities matching the query
|
|
137
128
|
*/
|
|
138
|
-
async
|
|
139
|
-
|
|
140
|
-
bindings: any[] | object,
|
|
141
|
-
querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
|
|
142
|
-
): Promise<readonly TEntity[]> {
|
|
143
|
-
const entityResults = await this.knexEntityLoader.loadManyByRawWhereClauseAsync(
|
|
144
|
-
rawWhereClause,
|
|
145
|
-
bindings,
|
|
146
|
-
querySelectionModifiers,
|
|
147
|
-
);
|
|
148
|
-
return entityResults.map((result) => result.enforceValue());
|
|
129
|
+
async countBySQLAsync(fragment: SQLFragment<Pick<TFields, TSelectedFields>>): Promise<number> {
|
|
130
|
+
return await this.knexEntityLoader.countBySQLAsync(fragment);
|
|
149
131
|
}
|
|
150
132
|
|
|
151
133
|
/**
|
|
@@ -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,41 +204,79 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
206
204
|
}
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
return await wrapNativePostgresCallAsync(() => query);
|
|
207
|
+
return result;
|
|
211
208
|
}
|
|
212
209
|
|
|
213
|
-
protected async
|
|
210
|
+
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
|
214
211
|
queryInterface: Knex,
|
|
215
212
|
tableName: string,
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
214
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
218
215
|
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
219
216
|
): Promise<object[]> {
|
|
220
|
-
let query =
|
|
217
|
+
let query = this.applyFieldEqualityConjunctionWhereClause(
|
|
218
|
+
queryInterface.select().from(tableName),
|
|
219
|
+
tableFieldSingleValueEqualityOperands,
|
|
220
|
+
tableFieldMultiValueEqualityOperands,
|
|
221
|
+
);
|
|
221
222
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
222
223
|
return await wrapNativePostgresCallAsync(() => query);
|
|
223
224
|
}
|
|
224
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
|
+
|
|
225
238
|
protected async fetchManyBySQLFragmentInternalAsync(
|
|
226
239
|
queryInterface: Knex,
|
|
227
240
|
tableName: string,
|
|
228
241
|
sqlFragment: SQLFragment<TFields>,
|
|
229
242
|
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
230
243
|
): Promise<object[]> {
|
|
231
|
-
let query =
|
|
232
|
-
.select()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
sqlFragment.sql,
|
|
236
|
-
sqlFragment.getKnexBindings((fieldName) =>
|
|
237
|
-
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
|
|
238
|
-
),
|
|
239
|
-
);
|
|
244
|
+
let query = this.applySQLFragmentWhereClause(
|
|
245
|
+
queryInterface.select().from(tableName),
|
|
246
|
+
sqlFragment,
|
|
247
|
+
);
|
|
240
248
|
query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
|
|
241
249
|
return await wrapNativePostgresCallAsync(() => query);
|
|
242
250
|
}
|
|
243
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
|
+
|
|
244
280
|
protected async insertInternalAsync(
|
|
245
281
|
queryInterface: Knex,
|
|
246
282
|
tableName: string,
|
|
@@ -257,10 +293,11 @@ export class PostgresEntityDatabaseAdapter<
|
|
|
257
293
|
tableIdField: string,
|
|
258
294
|
id: any,
|
|
259
295
|
object: object,
|
|
260
|
-
): Promise<
|
|
261
|
-
|
|
262
|
-
queryInterface.update(object).into(tableName).where(tableIdField, id)
|
|
296
|
+
): Promise<{ updatedRowCount: number }> {
|
|
297
|
+
const updatedRowCount = await wrapNativePostgresCallAsync(() =>
|
|
298
|
+
queryInterface.update(object).into(tableName).where(tableIdField, id),
|
|
263
299
|
);
|
|
300
|
+
return { updatedRowCount };
|
|
264
301
|
}
|
|
265
302
|
|
|
266
303
|
protected async deleteInternalAsync(
|