@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.
@@ -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: 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 ?? []),
@@ -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
- 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,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 = queryInterface
220
- .select()
221
- .from(tableName)
222
- .whereRaw(
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,
@@ -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 SupportedSQLValue[] {
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
- return b.value;
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 and undefined
137
- if (value === null || value === undefined) {
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 | undefined ? K : never;
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`/`undefined` to `IS 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 | undefined): SQLFragment<TFields> {
377
- if (value === null || value === undefined) {
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`/`undefined` to `IS NOT 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 | undefined): SQLFragment<TFields> {
391
- if (value === null || value === undefined) {
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`/`undefined` to `IS 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`/`undefined` to `IS 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`/`undefined` to `IS NOT 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`/`undefined` to `IS NOT 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/undefined to IS 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/undefined to IS NOT 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, NULL, '2024-01-01T00:00:00.000Z', '\\x68656c6c6f', 999, ARRAY[1, 2, 3])",
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('?? > ?');