Skip to content

Commit 7e414b6

Browse files
committed
feat: add count equivalent for knex loaer methods
1 parent 2240e94 commit 7e414b6

16 files changed

Lines changed: 572 additions & 32 deletions

packages/entity-database-adapter-knex-testing-utils/src/StubPostgresDatabaseAdapter.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,30 @@ export class StubPostgresDatabaseAdapter<
201201
throw new Error('SQL fragments not supported for StubDatabaseAdapter');
202202
}
203203

204+
protected async countByFieldEqualityConjunctionInternalAsync(
205+
queryInterface: any,
206+
tableName: string,
207+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
208+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
209+
): Promise<number> {
210+
const results = await this.fetchManyByFieldEqualityConjunctionInternalAsync(
211+
queryInterface,
212+
tableName,
213+
tableFieldSingleValueEqualityOperands,
214+
tableFieldMultiValueEqualityOperands,
215+
{ orderBy: undefined, offset: undefined, limit: undefined },
216+
);
217+
return results.length;
218+
}
219+
220+
protected countBySQLFragmentInternalAsync(
221+
_queryInterface: any,
222+
_tableName: string,
223+
_sqlFragment: SQLFragment<TFields>,
224+
): Promise<number> {
225+
throw new Error('SQL fragment count not supported for StubDatabaseAdapter');
226+
}
227+
204228
private generateRandomID(): any {
205229
const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
206230
invariant(

packages/entity-database-adapter-knex-testing-utils/src/__tests__/StubPostgresDatabaseAdapter-test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,81 @@ describe(StubPostgresDatabaseAdapter, () => {
489489
});
490490
});
491491

492+
describe('countByFieldEqualityConjunctionAsync', () => {
493+
it('counts matching records', async () => {
494+
const queryContext = instance(mock(EntityQueryContext));
495+
const databaseAdapter = new StubPostgresDatabaseAdapter<TestFields, 'customIdField'>(
496+
testEntityConfiguration,
497+
StubPostgresDatabaseAdapter.convertFieldObjectsToDataStore(
498+
testEntityConfiguration,
499+
new Map([
500+
[
501+
testEntityConfiguration.tableName,
502+
[
503+
{
504+
customIdField: 'hello',
505+
testIndexedField: 'h1',
506+
intField: 3,
507+
stringField: 'a',
508+
dateField: new Date(),
509+
nullableField: null,
510+
},
511+
{
512+
customIdField: 'world',
513+
testIndexedField: 'h2',
514+
intField: 3,
515+
stringField: 'b',
516+
dateField: new Date(),
517+
nullableField: null,
518+
},
519+
{
520+
customIdField: 'other',
521+
testIndexedField: 'h3',
522+
intField: 5,
523+
stringField: 'c',
524+
dateField: new Date(),
525+
nullableField: null,
526+
},
527+
],
528+
],
529+
]),
530+
),
531+
);
532+
533+
const count = await databaseAdapter.countByFieldEqualityConjunctionAsync(queryContext, [
534+
{ fieldName: 'intField', fieldValue: 3 },
535+
]);
536+
expect(count).toBe(2);
537+
538+
const countAll = await databaseAdapter.countByFieldEqualityConjunctionAsync(queryContext, []);
539+
expect(countAll).toBe(3);
540+
541+
const countMultiValue = await databaseAdapter.countByFieldEqualityConjunctionAsync(
542+
queryContext,
543+
[{ fieldName: 'customIdField', fieldValues: ['hello', 'world'] }],
544+
);
545+
expect(countMultiValue).toBe(2);
546+
547+
const countNone = await databaseAdapter.countByFieldEqualityConjunctionAsync(queryContext, [
548+
{ fieldName: 'intField', fieldValue: 999 },
549+
]);
550+
expect(countNone).toBe(0);
551+
});
552+
});
553+
554+
describe('countBySQLFragmentAsync', () => {
555+
it('throws because it is unsupported', async () => {
556+
const queryContext = instance(mock(EntityQueryContext));
557+
const databaseAdapter = new StubPostgresDatabaseAdapter<TestFields, 'customIdField'>(
558+
testEntityConfiguration,
559+
new Map(),
560+
);
561+
await expect(databaseAdapter.countBySQLFragmentAsync(queryContext, sql``)).rejects.toThrow(
562+
'SQL fragment count not supported for StubDatabaseAdapter',
563+
);
564+
});
565+
});
566+
492567
describe('fetchManyBySQLFragmentAsync', () => {
493568
it('throws because it is unsupported', async () => {
494569
const queryContext = instance(mock(EntityQueryContext));

packages/entity-database-adapter-knex/src/AuthorizationResultBasedKnexEntityLoader.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,42 @@ export class AuthorizationResultBasedKnexEntityLoader<
400400
return await this.constructionUtils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects);
401401
}
402402

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+
403439
/**
404440
* Authorization-result-based version of the EnforcingKnexEntityLoader method by the same name.
405441
* @returns SQL query builder for building and executing SQL queries that when executed returns entity results where result error can be UnauthorizedError.

packages/entity-database-adapter-knex/src/BasePostgresEntityDatabaseAdapter.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,73 @@ export abstract class BasePostgresEntityDatabaseAdapter<
248248
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
249249
): Promise<object[]>;
250250

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+
251318
private convertToTableQueryModifiers(
252319
querySelectionModifiers: PostgresQuerySelectionModifiers<TFields>,
253320
): TableQuerySelectionModifiers<TFields> {

packages/entity-database-adapter-knex/src/EnforcingKnexEntityLoader.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,32 @@ export class EnforcingKnexEntityLoader<
104104
return entityResults.map((result) => result.enforceValue());
105105
}
106106

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+
107133
/**
108134
* Load entities using a SQL query builder. When executed, all queries will enforce authorization and throw if not authorized.
109135
*

0 commit comments

Comments
 (0)