@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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +15 -5
  3. package/build/src/AuthorizationResultBasedKnexEntityLoader.js +24 -7
  4. package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +19 -11
  5. package/build/src/BasePostgresEntityDatabaseAdapter.js +37 -13
  6. package/build/src/BaseSQLQueryBuilder.d.ts +2 -2
  7. package/build/src/BaseSQLQueryBuilder.js +2 -2
  8. package/build/src/EnforcingKnexEntityLoader.d.ts +14 -28
  9. package/build/src/EnforcingKnexEntityLoader.js +17 -30
  10. package/build/src/PostgresEntityDatabaseAdapter.d.ts +7 -2
  11. package/build/src/PostgresEntityDatabaseAdapter.js +25 -15
  12. package/build/src/SQLOperator.d.ts +15 -14
  13. package/build/src/SQLOperator.js +11 -8
  14. package/build/src/internal/EntityKnexDataManager.d.ts +2 -10
  15. package/build/src/internal/EntityKnexDataManager.js +12 -22
  16. package/package.json +16 -16
  17. package/src/AuthorizationResultBasedKnexEntityLoader.ts +29 -14
  18. package/src/BasePostgresEntityDatabaseAdapter.ts +60 -29
  19. package/src/BaseSQLQueryBuilder.ts +2 -2
  20. package/src/EnforcingKnexEntityLoader.ts +20 -38
  21. package/src/PostgresEntityDatabaseAdapter.ts +66 -29
  22. package/src/SQLOperator.ts +23 -22
  23. package/src/__integration-tests__/PostgresEntityIntegration-test.ts +140 -116
  24. package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +0 -75
  25. package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +21 -28
  26. package/src/__tests__/EnforcingKnexEntityLoader-test.ts +0 -52
  27. package/src/__tests__/SQLOperator-test.ts +2 -29
  28. package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +26 -12
  29. package/src/internal/EntityKnexDataManager.ts +28 -31
  30. 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
- * Loads many objects matching the raw WHERE clause.
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
- const conditions = [baseWhere, cursorCondition].filter((it) => !!it);
227
- if (conditions.length === 0) {
228
- return sql `TRUE`;
219
+ if (!baseWhere) {
220
+ return cursorCondition ?? sql `TRUE`;
229
221
  }
230
- if (conditions.length === 1) {
231
- return conditions[0];
222
+ if (!cursorCondition) {
223
+ return baseWhere;
232
224
  }
233
- // Wrap baseWhere in parens if combining with cursor condition
234
- // We know we have exactly 2 conditions at this point
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.62.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 eslint src",
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.62.0",
33
- "knex": "^3.1.0"
29
+ "@expo/entity": "^0.64.0",
30
+ "knex": "^3.2.9"
34
31
  },
35
32
  "devDependencies": {
36
- "@expo/entity-testing-utils": "^0.62.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": "5.9.3"
37
+ "typescript": "6.0.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
41
  },
42
- "gitHead": "4965cc238882982e6315beca48a68679ed45456b"
42
+ "gitHead": "3f10a9e70eab45ae95acdae133055d8a3ec04ce8"
43
43
  }
@@ -401,24 +401,39 @@ export class AuthorizationResultBasedKnexEntityLoader<
401
401
  }
402
402
 
403
403
  /**
404
- * Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
405
- * @returns array of entity results that match the query, where result error can be UnauthorizedError
406
- * @throws Error when rawWhereClause or bindings are invalid
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
- * @deprecated Use loadManyBySQL instead for safer value bindings and more flexible query building.
409
+ * @returns count of entities matching the filters
409
410
  */
410
- async loadManyByRawWhereClauseAsync(
411
- rawWhereClause: string,
412
- bindings: any[] | object,
413
- querySelectionModifiers: EntityLoaderQuerySelectionModifiers<TFields, TSelectedFields> = {},
414
- ): Promise<readonly Result<TEntity>[]> {
415
- const fieldObjects = await this.knexDataManager.loadManyByRawWhereClauseAsync(
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
- rawWhereClause,
418
- bindings,
419
- querySelectionModifiers,
423
+ fieldEqualityOperands,
420
424
  );
421
- return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
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 raw WHERE clause.
220
+ * Fetch many objects matching the SQL fragment.
221
221
  *
222
222
  * @param queryContext - query context with which to perform the fetch
223
- * @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
224
- * @param bindings - array of positional bindings or object of named bindings
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 fetchManyByRawWhereClauseAsync(
227
+ async fetchManyBySQLFragmentAsync(
229
228
  queryContext: EntityQueryContext,
230
- rawWhereClause: string,
231
- bindings: any[] | object,
229
+ sqlFragment: SQLFragment<TFields>,
232
230
  querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
233
231
  ): Promise<readonly Readonly<TFields>[]> {
234
- const results = await this.fetchManyByRawWhereClauseInternalAsync(
232
+ const results = await this.fetchManyBySQLFragmentInternalAsync(
235
233
  queryContext.getQueryInterface(),
236
234
  this.entityConfiguration.tableName,
237
- rawWhereClause,
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 fetchManyByRawWhereClauseInternalAsync(
244
+ protected abstract fetchManyBySQLFragmentInternalAsync(
248
245
  queryInterface: Knex,
249
246
  tableName: string,
250
- rawWhereClause: string,
251
- bindings: object | any[],
247
+ sqlFragment: SQLFragment<TFields>,
252
248
  querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
253
249
  ): Promise<object[]>;
254
250
 
255
251
  /**
256
- * Fetch many objects matching the SQL fragment.
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 fetch
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
- * @param querySelectionModifiers - limit, offset, and orderByFragment for the query
261
- * @returns array of objects matching the query
299
+ * @returns count of objects matching the query
262
300
  */
263
- async fetchManyBySQLFragmentAsync(
301
+ async countBySQLFragmentAsync(
264
302
  queryContext: EntityQueryContext,
265
303
  sqlFragment: SQLFragment<TFields>,
266
- querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
267
- ): Promise<readonly Readonly<TFields>[]> {
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 fetchManyBySQLFragmentInternalAsync(
312
+ protected abstract countBySQLFragmentInternalAsync(
281
313
  queryInterface: Knex,
282
314
  tableName: string,
283
315
  sqlFragment: SQLFragment<TFields>,
284
- querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
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: NullsOrdering | undefined = undefined,
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: NullsOrdering | undefined = undefined,
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
- * Load entities with a raw SQL WHERE clause.
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
- * @example
111
- * Load entities with SQL function
112
- * ```typescript
113
- * const entitiesWithJsonKey = await ExampleEntity.loader(vc)
114
- * .loadManyByRawWhereClauseAsync(
115
- * "json_column->>'key_name' = ?",
116
- * ['value'],
117
- * );
118
- * ```
119
- *
120
- * @example
121
- * Load entities with tuple matching
122
- * ```typescript
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
- * @deprecated Use loadManyBySQL instead for safer value bindings and more flexible query building.
127
+ * @returns count of entities matching the query
137
128
  */
138
- async loadManyByRawWhereClauseAsync(
139
- rawWhereClause: string,
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
- protected async fetchManyByFieldEqualityConjunctionInternalAsync(
168
- queryInterface: Knex,
169
- tableName: string,
167
+ private applyFieldEqualityConjunctionWhereClause(
168
+ query: Knex.QueryBuilder,
170
169
  tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
171
170
  tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
172
- querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
173
- ): Promise<object[]> {
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
- query = query.where(whereObject);
185
+ result = result.where(whereObject);
188
186
  }
189
187
  if (nullTableFieldSingleValueEqualityOperands.length > 0) {
190
188
  for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
191
- query = query.whereNull(tableField);
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
- query = query.where((builder) => {
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
- query = this.applyQueryModifiersToQuery(query, querySelectionModifiers);
210
- return await wrapNativePostgresCallAsync(() => query);
207
+ return result;
211
208
  }
212
209
 
213
- protected async fetchManyByRawWhereClauseInternalAsync(
210
+ protected async fetchManyByFieldEqualityConjunctionInternalAsync(
214
211
  queryInterface: Knex,
215
212
  tableName: string,
216
- rawWhereClause: string,
217
- bindings: object | any[],
213
+ tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
214
+ tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
218
215
  querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
219
216
  ): Promise<object[]> {
220
- let query = queryInterface.select().from(tableName).whereRaw(rawWhereClause, bindings);
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 = queryInterface
232
- .select()
233
- .from(tableName)
234
- .whereRaw(
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<object[]> {
261
- return await wrapNativePostgresCallAsync(() =>
262
- queryInterface.update(object).into(tableName).where(tableIdField, id).returning('*'),
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(