@travetto/model-query 8.0.1 → 8.0.3

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 CHANGED
@@ -36,12 +36,6 @@ export interface ModelQuerySupport {
36
36
  * @param failOnMany Should the query fail on more than one result found, defaults to true
37
37
  */
38
38
  queryOne<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, failOnMany?: boolean): Promise<T>;
39
- /**
40
- * Find the count of matching documents by query.
41
- * @param cls The model class
42
- * @param query The query to count for
43
- */
44
- queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number>;
45
39
  }
46
40
  ```
47
41
 
@@ -127,12 +121,45 @@ export interface ModelQuerySuggestSupport extends ModelQuerySupport {
127
121
  }
128
122
  ```
129
123
 
124
+ ### Aggregate
125
+ This contract provides the ability to run strongly-typed field-level aggregations (such as sum, avg, min, and max) on models with optional query filtering.
126
+
127
+ **Code: Query Aggregate**
128
+ ```typescript
129
+ export interface ModelQueryAggregateSupport extends ModelQuerySupport {
130
+ /**
131
+ * Run an aggregation on a field
132
+ * @param modelClass The model class to aggregate
133
+ * @param operation The operation to perform ('sum', 'avg', 'min', 'max')
134
+ * @param field The field to aggregate on
135
+ * @param query Additional query filtering
136
+ */
137
+ aggregateFieldByQuery<
138
+ T extends ModelType,
139
+ Op extends AggregateOperation,
140
+ F extends (Op extends AggregateNumericOperation ? ValidNumericFields<T> : ValidComparableFields<T>)
141
+ >(
142
+ modelClass: Class<T>,
143
+ operation: Op,
144
+ field: F,
145
+ query?: ModelQuery<T>
146
+ ): Promise<AggregateResultType<T, F>>;
147
+
148
+ /**
149
+ * Find the count of matching documents by query.
150
+ * @param cls The model class
151
+ * @param query The query to count for
152
+ */
153
+ countByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number>;
154
+ }
155
+ ```
156
+
130
157
  ## Implementations
131
- |Service|Query|QueryCrud|QueryFacet|
132
- |-------|-----|---------|----------|
133
- |[Elasticsearch Model Source](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch#readme "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.")|X|X|X|
134
- |[MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.")|X'|X'|X'|
135
- |[SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.")|X'|X'|X'|
158
+ |Service|Query|QueryCrud|QueryFacet|QueryAggregate|
159
+ |-------|-----|---------|----------|--------------|
160
+ |[Elasticsearch Model Source](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch#readme "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.")|X|X|X|X|
161
+ |[MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.")|X'|X'|X'|X'|
162
+ |[SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.")|X'|X'|X'|X'|
136
163
 
137
164
  ## Querying
138
165
  One of the complexities of abstracting multiple storage mechanisms, is providing a consistent query language. The query language the module uses is a derivation of [mongodb](https://mongodb.com)'s query language, with some restrictions, additions, and caveats. Additionally, given the nature of typescript, all queries are statically typed, and will catch type errors at compile time.
@@ -227,9 +254,15 @@ In addition to the provided contracts, the module also provides common utilities
227
254
  ```typescript
228
255
  import { Config } from '@travetto/config';
229
256
  import { Injectable } from '@travetto/di';
230
- import type { ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySuggestSupport } from '@travetto/model-query';
257
+ import type {
258
+ ModelQueryAggregateSupport,
259
+ ModelQueryCrudSupport,
260
+ ModelQueryFacetSupport,
261
+ ModelQuerySuggestSupport
262
+ } from '@travetto/model-query';
231
263
  import { Suite } from '@travetto/test';
232
264
 
265
+ import { ModelQueryAggregateSuite } from '@travetto/model-query/support/test/aggregate.ts';
233
266
  import { ModelQueryCrudSuite } from '@travetto/model-query/support/test/crud.ts';
234
267
  import { ModelQueryFacetSuite } from '@travetto/model-query/support/test/facet.ts';
235
268
  import { ModelQueryPolymorphismSuite } from '@travetto/model-query/support/test/polymorphism.ts';
@@ -242,7 +275,9 @@ import { QueryModelService } from './query-service.ts';
242
275
  class CustomModelConfig {}
243
276
 
244
277
  @Injectable()
245
- class CustomModelService extends QueryModelService implements ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySuggestSupport {}
278
+ class CustomModelService
279
+ extends QueryModelService
280
+ implements ModelQueryAggregateSupport, ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySuggestSupport {}
246
281
 
247
282
  @Suite()
248
283
  class CustomQuerySuite extends ModelQuerySuite {
@@ -250,6 +285,12 @@ class CustomQuerySuite extends ModelQuerySuite {
250
285
  configClass = CustomModelConfig;
251
286
  }
252
287
 
288
+ @Suite()
289
+ class CustomQueryAggregateSuite extends ModelQueryAggregateSuite {
290
+ serviceClass = CustomModelService;
291
+ configClass = CustomModelConfig;
292
+ }
293
+
253
294
  @Suite()
254
295
  class CustomQueryCrudSuite extends ModelQueryCrudSuite {
255
296
  serviceClass = CustomModelService;
package/__index__.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export * from './src/model/indexes.ts';
2
2
  export * from './src/model/query.ts';
3
3
  export * from './src/model/where-clause.ts';
4
+ export * from './src/types/aggregate.ts';
4
5
  export * from './src/types/crud.ts';
5
6
  export * from './src/types/facet.ts';
6
7
  export * from './src/types/query.ts';
7
8
  export * from './src/types/suggest.ts';
9
+ export * from './src/util/aggregate.ts';
8
10
  export * from './src/util/crud.ts';
9
11
  export * from './src/util/facet.ts';
10
12
  export * from './src/util/query.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-query",
3
- "version": "8.0.1",
3
+ "version": "8.0.3",
4
4
  "description": "Datastore abstraction for advanced query support.",
5
5
  "keywords": [
6
6
  "datastore",
@@ -31,11 +31,11 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@travetto/di": "^8.0.1",
34
- "@travetto/model": "^8.0.1",
35
- "@travetto/schema": "^8.0.1"
34
+ "@travetto/model": "^8.0.3",
35
+ "@travetto/schema": "^8.0.2"
36
36
  },
37
37
  "peerDependencies": {
38
- "@travetto/test": "^8.0.1"
38
+ "@travetto/test": "^8.0.2"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/test": {
@@ -6,6 +6,8 @@ export type QueryPrimitiveArray = QueryPrimitive[];
6
6
  export type DistanceUnit = 'mi' | 'm' | 'km' | 'ft' | 'rad';
7
7
  export type RetainQueryPrimitiveFields<T> = Pick<T, ValidFields<T, QueryPrimitive>>;
8
8
  export type ValidStringFields<T> = KeyPaths<T, String | string | string[] | String[] | undefined>;
9
+ export type ValidNumericFields<T> = KeyPaths<T, number | bigint | Number | undefined>;
10
+ export type ValidComparableFields<T> = KeyPaths<T, number | bigint | Number | Date | undefined>;
9
11
 
10
12
  type General<T> = {
11
13
  $eq?: T;
@@ -0,0 +1,55 @@
1
+ import type { ModelType } from '@travetto/model';
2
+ import type { Class } from '@travetto/runtime';
3
+
4
+ import type { ModelQuery } from '../model/query.ts';
5
+ import type { ValidComparableFields, ValidNumericFields } from '../model/where-clause.ts';
6
+ import type { ModelQuerySupport } from './query.ts';
7
+
8
+ export type AggregateNumericOperation = 'sum' | 'avg';
9
+ export type AggregateComparableOperation = 'min' | 'max';
10
+ export type AggregateOperation = AggregateNumericOperation | AggregateComparableOperation;
11
+
12
+ export type PathType<T, Path extends string> = Path extends `${infer Head}.${infer Tail}`
13
+ ? Head extends keyof T
14
+ ? PathType<NonNullable<T[Head]> extends (infer Element)[] ? Element : NonNullable<T[Head]>, Tail>
15
+ : never
16
+ : Path extends keyof T
17
+ ? NonNullable<T[Path]> extends (infer Element)[]
18
+ ? Element
19
+ : NonNullable<T[Path]>
20
+ : never;
21
+
22
+ export type AggregateResultType<T, F extends string> = [PathType<T, F>] extends [never]
23
+ ? number | Date | bigint | undefined
24
+ : PathType<T, F> | undefined;
25
+
26
+ /**
27
+ * The contract for a model service with aggregate support
28
+ * @concrete
29
+ */
30
+ export interface ModelQueryAggregateSupport extends ModelQuerySupport {
31
+ /**
32
+ * Run an aggregation on a field
33
+ * @param modelClass The model class to aggregate
34
+ * @param operation The operation to perform ('sum', 'avg', 'min', 'max')
35
+ * @param field The field to aggregate on
36
+ * @param query Additional query filtering
37
+ */
38
+ aggregateFieldByQuery<
39
+ T extends ModelType,
40
+ Op extends AggregateOperation,
41
+ F extends (Op extends AggregateNumericOperation ? ValidNumericFields<T> : ValidComparableFields<T>)
42
+ >(
43
+ modelClass: Class<T>,
44
+ operation: Op,
45
+ field: F,
46
+ query?: ModelQuery<T>
47
+ ): Promise<AggregateResultType<T, F>>;
48
+
49
+ /**
50
+ * Find the count of matching documents by query.
51
+ * @param cls The model class
52
+ * @param query The query to count for
53
+ */
54
+ countByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number>;
55
+ }
@@ -21,10 +21,4 @@ export interface ModelQuerySupport {
21
21
  * @param failOnMany Should the query fail on more than one result found, defaults to true
22
22
  */
23
23
  queryOne<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, failOnMany?: boolean): Promise<T>;
24
- /**
25
- * Find the count of matching documents by query.
26
- * @param cls The model class
27
- * @param query The query to count for
28
- */
29
- queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number>;
30
24
  }
@@ -0,0 +1,29 @@
1
+ import type { ModelType } from '@travetto/model';
2
+ import { castTo, type Class, hasFunction } from '@travetto/runtime';
3
+ import { DataUtil, SchemaRegistryIndex } from '@travetto/schema';
4
+
5
+ import type { AggregateOperation, AggregateResultType, ModelQueryAggregateSupport } from '../types/aggregate.ts';
6
+
7
+ export class ModelQueryAggregateUtil {
8
+ /**
9
+ * Type guard for determining if service supports query aggregate operations
10
+ */
11
+ static isSupported = hasFunction<ModelQueryAggregateSupport>('aggregateFieldByQuery');
12
+
13
+ /**
14
+ * Resolve and coerce the aggregate query result using DataUtil.coerceType
15
+ * @param modelClass The model class being queried
16
+ * @param operation The aggregate operation
17
+ * @param field The field being aggregated
18
+ * @param rawValue The raw result value from the datastore
19
+ */
20
+ static resolveResult<T extends ModelType, Op extends AggregateOperation, F extends string>(
21
+ modelClass: Class<T>,
22
+ operation: Op,
23
+ field: F,
24
+ rawValue: unknown
25
+ ): AggregateResultType<T, F> {
26
+ const leafFieldType = SchemaRegistryIndex.getNestedFieldConfig(modelClass, field)!.type;
27
+ return castTo(DataUtil.coerceType(rawValue, leafFieldType, false) ?? undefined);
28
+ }
29
+ }
@@ -2,6 +2,7 @@
2
2
  import { DocFileUtil, type DocJSXElement, type DocJSXElementByFn, d } from '@travetto/doc';
3
3
  import { castKey, Runtime, toConcrete } from '@travetto/runtime';
4
4
 
5
+ import type { ModelQueryAggregateSupport } from '../src/types/aggregate.ts';
5
6
  import type { ModelQueryCrudSupport } from '../src/types/crud.ts';
6
7
  import type { ModelQueryFacetSupport } from '../src/types/facet.ts';
7
8
  import type { ModelQuerySupport } from '../src/types/query.ts';
@@ -11,6 +12,7 @@ const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'>
11
12
  d.codeLink(title, Runtime.getSourceFile(target), new RegExp(`\\binterface\\s+${target.name}`));
12
13
 
13
14
  export const Links = {
15
+ QueryAggregate: toLink('Aggregate', toConcrete<ModelQueryAggregateSupport>()),
14
16
  QueryCrud: toLink('Query Crud', toConcrete<ModelQueryCrudSupport>()),
15
17
  QueryFacet: toLink('Facet', toConcrete<ModelQueryFacetSupport>()),
16
18
  QuerySuggest: toLink('Suggest', toConcrete<ModelQuerySuggestSupport>()),
@@ -21,7 +23,7 @@ export const ModelQueryTypes = (fn: Function): DocJSXElement[] => {
21
23
  const { content } = DocFileUtil.readSource(fn);
22
24
  const found: DocJSXElementByFn<'CodeLink'>[] = [];
23
25
  const seen = new Set<string>();
24
- for (const [, key] of content.matchAll(/Model(Query(Suggest|Facet|Crud)?)Support/g)) {
26
+ for (const [, key] of content.matchAll(/Model(Query(Aggregate|Suggest|Facet|Crud)?)Support/g)) {
25
27
  if (!seen.has(key) && key in Links) {
26
28
  seen.add(key);
27
29
  const link = Links[castKey(key)];
@@ -0,0 +1,107 @@
1
+ import assert from 'node:assert';
2
+
3
+ import type { ModelCrudSupport } from '@travetto/model';
4
+ import { Suite, Test } from '@travetto/test';
5
+
6
+ import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
7
+
8
+ import type { ModelQueryAggregateSupport } from '../../src/types/aggregate.ts';
9
+ import { Aged, BigIntModel, Person } from './model.ts';
10
+
11
+ @Suite()
12
+ export abstract class ModelQueryAggregateSuite extends BaseModelSuite<ModelQueryAggregateSupport & ModelCrudSupport> {
13
+ @Test('verify basic aggregations')
14
+ async testAggregate() {
15
+ const people = [
16
+ Person.from({ name: 'Bob', age: 20, gender: 'm', address: { street1: '1st St' } }),
17
+ Person.from({ name: 'Alice', age: 30, gender: 'f', address: { street1: '2nd St' } }),
18
+ Person.from({ name: 'Charlie', age: 40, gender: 'm', address: { street1: '3rd St' } }),
19
+ Person.from({ name: 'Dana', age: 50, gender: 'f', address: { street1: '4th St' } })
20
+ ];
21
+
22
+ const service = await this.service;
23
+ const saved = await this.saveAll(Person, people);
24
+ assert(saved === 4);
25
+
26
+ const sumResult = await service.aggregateFieldByQuery(Person, 'sum', 'age');
27
+ assert(sumResult === 140);
28
+
29
+ const averageResult = await service.aggregateFieldByQuery(Person, 'avg', 'age');
30
+ assert(averageResult === 35);
31
+
32
+ const minimumResult = await service.aggregateFieldByQuery(Person, 'min', 'age');
33
+ assert(minimumResult === 20);
34
+
35
+ const maximumResult = await service.aggregateFieldByQuery(Person, 'max', 'age');
36
+ assert(maximumResult === 50);
37
+
38
+ // Filtered aggregations
39
+ const filteredSumResult = await service.aggregateFieldByQuery(Person, 'sum', 'age', {
40
+ where: {
41
+ age: { $gte: 30 }
42
+ }
43
+ });
44
+ assert(filteredSumResult === 120);
45
+
46
+ const filteredAverageResult = await service.aggregateFieldByQuery(Person, 'avg', 'age', {
47
+ where: {
48
+ gender: 'f'
49
+ }
50
+ });
51
+ assert(filteredAverageResult === 40);
52
+
53
+ // No matches
54
+ const noMatchResult = await service.aggregateFieldByQuery(Person, 'sum', 'age', {
55
+ where: {
56
+ age: { $gt: 1000 }
57
+ }
58
+ });
59
+ assert(noMatchResult === undefined);
60
+ }
61
+
62
+ @Test('verify date aggregations')
63
+ async testDateAggregate() {
64
+ const service = await this.service;
65
+
66
+ const firstDate = new Date(2020, 0, 1);
67
+ const secondDate = new Date(2021, 0, 1);
68
+ const thirdDate = new Date(2022, 0, 1);
69
+
70
+ await this.saveAll(Aged, [
71
+ Aged.from({ createdAt: firstDate }),
72
+ Aged.from({ createdAt: secondDate }),
73
+ Aged.from({ createdAt: thirdDate })
74
+ ]);
75
+
76
+ const minimumDateResult = await service.aggregateFieldByQuery(Aged, 'min', 'createdAt');
77
+ assert(minimumDateResult instanceof Date);
78
+ assert(minimumDateResult.getTime() === firstDate.getTime());
79
+
80
+ const maximumDateResult = await service.aggregateFieldByQuery(Aged, 'max', 'createdAt');
81
+ assert(maximumDateResult instanceof Date);
82
+ assert(maximumDateResult.getTime() === thirdDate.getTime());
83
+ }
84
+
85
+ @Test('verify bigint aggregations')
86
+ async testBigIntAggregate() {
87
+ const service = await this.service;
88
+
89
+ await this.saveAll(BigIntModel, [
90
+ BigIntModel.from({ largeNumber: 100n }),
91
+ BigIntModel.from({ largeNumber: 200n }),
92
+ BigIntModel.from({ largeNumber: 300n })
93
+ ]);
94
+
95
+ const sumResult = await service.aggregateFieldByQuery(BigIntModel, 'sum', 'largeNumber');
96
+ assert(sumResult === 600n);
97
+
98
+ const minimumResult = await service.aggregateFieldByQuery(BigIntModel, 'min', 'largeNumber');
99
+ assert(minimumResult === 100n);
100
+
101
+ const maximumResult = await service.aggregateFieldByQuery(BigIntModel, 'max', 'largeNumber');
102
+ assert(maximumResult === 300n);
103
+
104
+ const averageResult = await service.aggregateFieldByQuery(BigIntModel, 'avg', 'largeNumber');
105
+ assert(averageResult === 200n);
106
+ }
107
+ }
@@ -1,12 +1,13 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { Model, type ModelCrudSupport, NotFoundError, UniqueError } from '@travetto/model';
4
- import { castTo } from '@travetto/runtime';
3
+ import { Model, type ModelCrudSupport, type ModelType, NotFoundError, UniqueError } from '@travetto/model';
4
+ import { castTo, type Class } from '@travetto/runtime';
5
5
  import { Suite, Test } from '@travetto/test';
6
6
 
7
7
  import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
8
8
 
9
- import { QueryIndex } from '../../__index__.ts';
9
+ import type { WhereClause } from '../../__index__.ts';
10
+ import { QueryIndex } from '../../src/model/indexes.ts';
10
11
  import type { ModelQueryCrudSupport } from '../../src/types/crud.ts';
11
12
  import { Address, BigIntModel, Person, Todo } from './model.ts';
12
13
 
@@ -25,6 +26,11 @@ class UniqueUser2 {
25
26
  export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudSupport & ModelCrudSupport> {
26
27
  supportsUniqueIndexes = true;
27
28
 
29
+ async countByQuery<T extends ModelType>(cls: Class<T>, query: WhereClause<T>): Promise<number> {
30
+ const svc = await this.service;
31
+ return svc.query(cls, { where: query, limit: 10000 }).then(v => v.length);
32
+ }
33
+
28
34
  @Test({ skip: self => !castTo<ModelQueryCrudSuite>(self).supportsUniqueIndexes })
29
35
  async testUnique() {
30
36
  const svc = await this.service;
@@ -147,13 +153,13 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
147
153
 
148
154
  assert(c === 2);
149
155
 
150
- assert((await svc.queryCount(Person, {})) === 3);
156
+ assert((await this.countByQuery(Person, {})) === 3);
151
157
 
152
158
  const c2 = await svc.deleteByQuery(Person, { where: { age: { $lte: 3 } } });
153
159
 
154
160
  assert(c2 === 3);
155
161
 
156
- assert((await svc.queryCount(Person, {})) === 0);
162
+ assert((await this.countByQuery(Person, {})) === 0);
157
163
  }
158
164
 
159
165
  @Test()
@@ -177,7 +183,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
177
183
 
178
184
  assert(count === 5);
179
185
 
180
- assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 5);
186
+ assert((await this.countByQuery(Person, { gender: 'm' })) === 5);
181
187
 
182
188
  const c = await svc.updatePartialByQuery(Person, { where: { age: { $gt: 3 } } }, { gender: 'f' });
183
189
 
@@ -185,14 +191,14 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
185
191
 
186
192
  assert(c === 2);
187
193
 
188
- assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 3);
194
+ assert((await this.countByQuery(Person, { gender: 'm' })) === 3);
189
195
 
190
196
  const c2 = await svc.updatePartialByQuery(Person, { where: { gender: 'm' } }, { gender: 'f' });
191
197
 
192
198
  assert(c2 === 3);
193
199
 
194
- assert((await svc.queryCount(Person, { where: { gender: 'f' } })) === 5);
195
- assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 0);
200
+ assert((await this.countByQuery(Person, { gender: 'f' })) === 5);
201
+ assert((await this.countByQuery(Person, { gender: 'm' })) === 0);
196
202
  }
197
203
 
198
204
  @Test()
@@ -237,7 +243,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
237
243
  assert(withOptional.every(x => x.optionalBigInt !== undefined));
238
244
 
239
245
  // Test count with bigint condition
240
- const countResult = await svc.queryCount(BigIntModel, { where: { largeNumber: { $gte: 1000n } } });
246
+ const countResult = await this.countByQuery(BigIntModel, { largeNumber: { $gte: 1000n } });
241
247
  assert(countResult === 2);
242
248
  }
243
249
 
@@ -285,7 +291,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
285
291
  assert(deleted === 2);
286
292
 
287
293
  // Verify deletions
288
- const remaining = await svc.queryCount(BigIntModel, {});
294
+ const remaining = await this.countByQuery(BigIntModel, {});
289
295
  assert(remaining === 3);
290
296
 
291
297
  const all = await svc.query(BigIntModel, {});
@@ -7,16 +7,20 @@ import { Suite, Test } from '@travetto/test';
7
7
  import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
8
8
  import { Doctor, Engineer, Firefighter, Worker } from '@travetto/model/support/test/polymorphism.ts';
9
9
 
10
+ import type { ModelQueryAggregateSupport } from '../../src/types/aggregate.ts';
10
11
  import type { ModelQueryCrudSupport } from '../../src/types/crud.ts';
11
12
  import type { ModelQueryFacetSupport } from '../../src/types/facet.ts';
12
13
  import type { ModelQuerySupport } from '../../src/types/query.ts';
13
14
  import type { ModelQuerySuggestSupport } from '../../src/types/suggest.ts';
15
+ import { ModelQueryAggregateUtil } from '../../src/util/aggregate.ts';
14
16
  import { ModelQueryCrudUtil } from '../../src/util/crud.ts';
15
17
  import { ModelQueryFacetUtil } from '../../src/util/facet.ts';
16
18
  import { ModelQuerySuggestUtil } from '../../src/util/suggest.ts';
17
19
 
18
20
  @Suite()
19
- export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQuerySupport & ModelCrudSupport> {
21
+ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<
22
+ ModelQuerySupport & ModelQueryAggregateSupport & ModelCrudSupport
23
+ > {
20
24
  @Test()
21
25
  async testQuery() {
22
26
  const svc = await this.service;
@@ -34,9 +38,9 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
34
38
  assert((await svc.query(Doctor, {})).length === 2);
35
39
  assert((await svc.query(Engineer, {})).length === 1);
36
40
 
37
- assert((await svc.queryCount(Worker, { where: { name: 'bob' } })) === 1);
38
- assert((await svc.queryCount(Doctor, { where: { name: 'bob' } })) === 1);
39
- assert((await svc.queryCount(Engineer, { where: { name: 'bob' } })) === 0);
41
+ assert((await svc.countByQuery(Worker, { where: { name: 'bob' } })) === 1);
42
+ assert((await svc.countByQuery(Doctor, { where: { name: 'bob' } })) === 1);
43
+ assert((await svc.countByQuery(Engineer, { where: { name: 'bob' } })) === 0);
40
44
 
41
45
  assert((await svc.queryOne(Worker, { where: { name: 'bob' } })) instanceof Doctor);
42
46
  await assert.rejects(() => svc.queryOne(Firefighter, { where: { name: 'bob' } }), NotFoundError);
@@ -44,7 +48,7 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
44
48
 
45
49
  @Test({ skip: ModelQueryPolymorphismSuite.ifNot(ModelQueryCrudUtil.isSupported) })
46
50
  async testCrudQuery() {
47
- const svc: ModelQueryCrudSupport & ModelQuerySupport = castTo(await this.service);
51
+ const svc: ModelQueryCrudSupport & ModelQueryAggregateSupport & ModelQuerySupport = castTo(await this.service);
48
52
  const [doc, doc2, fire, eng] = [
49
53
  Doctor.from({ name: 'bob', specialty: 'feet' }),
50
54
  Doctor.from({ name: 'nob', specialty: 'eyes' }),
@@ -58,7 +62,7 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
58
62
  const c = await svc.updatePartialByQuery(Doctor, { where: { specialty: 'feet' } }, { specialty: 'eyes' });
59
63
  assert(c === 1);
60
64
 
61
- assert((await svc.queryCount(Doctor, { where: { specialty: 'eyes' } })) === 2);
65
+ assert((await svc.countByQuery(Doctor, { where: { specialty: 'eyes' } })) === 2);
62
66
 
63
67
  const removed = await svc.deleteByQuery(Worker, { where: { name: 'rob' } });
64
68
 
@@ -109,4 +113,24 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
109
113
  const docFacet = await svc.facetByQuery(Doctor, 'specialty');
110
114
  assert.deepStrictEqual(docFacet, [{ count: 2, key: 'eyes' }]);
111
115
  }
116
+
117
+ @Test({ skip: ModelQueryPolymorphismSuite.ifNot(ModelQueryAggregateUtil.isSupported) })
118
+ async testAggregateQuery() {
119
+ const service: ModelQueryAggregateSupport & ModelQuerySupport = castTo(await this.service);
120
+ const [doctorOne, doctorTwo, firefighterOne, firefighterTwo] = [
121
+ Doctor.from({ name: 'bob', specialty: 'eyes' }),
122
+ Doctor.from({ name: 'nob', specialty: 'eyes' }),
123
+ Firefighter.from({ name: 'rob', firehouse: 20 }),
124
+ Firefighter.from({ name: 'fob', firehouse: 30 })
125
+ ];
126
+
127
+ await this.saveAll(Worker, [doctorOne, doctorTwo, firefighterOne, firefighterTwo]);
128
+ assert((await this.getSize(Worker)) === 4);
129
+
130
+ const sumFirehouse = await service.aggregateFieldByQuery(Firefighter, 'sum', 'firehouse');
131
+ assert(sumFirehouse === 50);
132
+
133
+ const averageFirehouse = await service.aggregateFieldByQuery(Firefighter, 'avg', 'firehouse');
134
+ assert(averageFirehouse === 25);
135
+ }
112
136
  }
@@ -6,11 +6,12 @@ import { Suite, Test } from '@travetto/test';
6
6
 
7
7
  import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
8
8
 
9
+ import type { ModelQueryAggregateSupport } from '../../__index__.ts';
9
10
  import type { ModelQuerySupport } from '../../src/types/query.ts';
10
11
  import { Aged, Location, Names, Note, Person, PersonFamily, Recipe, SimpleList, WithNestedLists, WithNestedNestedLists } from './model.ts';
11
12
 
12
13
  @Suite()
13
- export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport & ModelCrudSupport> {
14
+ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport & ModelQueryAggregateSupport & ModelCrudSupport> {
14
15
  supportsGeo = true;
15
16
 
16
17
  @Test()
@@ -38,7 +39,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
38
39
  const one2 = await svc.queryOne(Person, { where: { id: { $eq: people[0].id } } });
39
40
  assert(one2.id === people[0].id);
40
41
 
41
- const none = await svc.queryCount(Person, { where: { id: { $ne: people[0].id } } });
42
+ const none = await svc.countByQuery(Person, { where: { id: { $ne: people[0].id } } });
42
43
  assert(none === 3);
43
44
 
44
45
  const noneIds = await svc.query(Person, { where: { id: { $ne: people[0].id } } });
@@ -331,7 +332,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
331
332
 
332
333
  await this.saveAll(Location, toAdd);
333
334
 
334
- assert((await svc.queryCount(Location, {})) === 25);
335
+ assert((await svc.countByQuery(Location, {})) === 25);
335
336
 
336
337
  const result = await svc.query(Location, {
337
338
  limit: 100,
@@ -382,7 +383,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
382
383
  })
383
384
  );
384
385
 
385
- const out = await service.queryCount(Note, {
386
+ const out = await service.countByQuery(Note, {
386
387
  where: {
387
388
  entities: {
388
389
  id
@@ -413,7 +414,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
413
414
  )
414
415
  );
415
416
 
416
- const simple = await service.queryCount(Aged, {
417
+ const simple = await service.countByQuery(Aged, {
417
418
  where: {
418
419
  createdAt: {
419
420
  $gt: new Date()
@@ -422,7 +423,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
422
423
  });
423
424
  assert(simple === 5);
424
425
 
425
- const simple2 = await service.queryCount(Aged, {
426
+ const simple2 = await service.countByQuery(Aged, {
426
427
  where: {
427
428
  createdAt: {
428
429
  $gt: '-1d'
@@ -431,7 +432,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
431
432
  });
432
433
  assert(simple2 === 6);
433
434
 
434
- const simple3 = await service.queryCount(Aged, {
435
+ const simple3 = await service.countByQuery(Aged, {
435
436
  where: {
436
437
  createdAt: {
437
438
  $gt: '-1296m', // -1.9d
@@ -441,7 +442,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
441
442
  });
442
443
  assert(simple3 === 3);
443
444
 
444
- const simple4 = await service.queryCount(Aged, {
445
+ const simple4 = await service.countByQuery(Aged, {
445
446
  where: {
446
447
  createdAt: {
447
448
  $gt: TimeUtil.fromNow('-144m'), // -0.1d
@@ -463,35 +464,35 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
463
464
  WithNestedLists.from({ names: [], tags: [] })
464
465
  ]);
465
466
 
466
- let total = await service.queryCount(WithNestedLists, {
467
+ let total = await service.countByQuery(WithNestedLists, {
467
468
  where: {
468
469
  names: { $exists: true }
469
470
  }
470
471
  });
471
472
  assert(total === 3);
472
473
 
473
- total = await service.queryCount(WithNestedLists, {
474
+ total = await service.countByQuery(WithNestedLists, {
474
475
  where: {
475
476
  names: { $exists: false }
476
477
  }
477
478
  });
478
479
  assert(total === 2);
479
480
 
480
- total = await service.queryCount(WithNestedLists, {
481
+ total = await service.countByQuery(WithNestedLists, {
481
482
  where: {
482
483
  tags: { $exists: false }
483
484
  }
484
485
  });
485
486
  assert(total === 3);
486
487
 
487
- total = await service.queryCount(WithNestedLists, {
488
+ total = await service.countByQuery(WithNestedLists, {
488
489
  where: {
489
490
  tags: { $exists: true }
490
491
  }
491
492
  });
492
493
  assert(total === 2);
493
494
 
494
- total = await service.queryCount(WithNestedLists, {
495
+ total = await service.countByQuery(WithNestedLists, {
495
496
  where: {
496
497
  tags: { $exists: false },
497
498
  names: { $exists: false }
@@ -499,7 +500,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
499
500
  });
500
501
  assert(total === 1);
501
502
 
502
- total = await service.queryCount(WithNestedLists, {
503
+ total = await service.countByQuery(WithNestedLists, {
503
504
  where: {
504
505
  tags: { $exists: true },
505
506
  names: { $exists: true }
@@ -520,35 +521,35 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
520
521
  WithNestedNestedLists.from({ sub: {}, tags: [] })
521
522
  ]);
522
523
 
523
- let total = await service.queryCount(WithNestedNestedLists, {
524
+ let total = await service.countByQuery(WithNestedNestedLists, {
524
525
  where: {
525
526
  sub: { names: { $exists: true } }
526
527
  }
527
528
  });
528
529
  assert(total === 3);
529
530
 
530
- total = await service.queryCount(WithNestedNestedLists, {
531
+ total = await service.countByQuery(WithNestedNestedLists, {
531
532
  where: {
532
533
  sub: { names: { $exists: false } }
533
534
  }
534
535
  });
535
536
  assert(total === 2);
536
537
 
537
- total = await service.queryCount(WithNestedNestedLists, {
538
+ total = await service.countByQuery(WithNestedNestedLists, {
538
539
  where: {
539
540
  tags: { $exists: false }
540
541
  }
541
542
  });
542
543
  assert(total === 3);
543
544
 
544
- total = await service.queryCount(WithNestedNestedLists, {
545
+ total = await service.countByQuery(WithNestedNestedLists, {
545
546
  where: {
546
547
  tags: { $exists: true }
547
548
  }
548
549
  });
549
550
  assert(total === 2);
550
551
 
551
- total = await service.queryCount(WithNestedNestedLists, {
552
+ total = await service.countByQuery(WithNestedNestedLists, {
552
553
  where: {
553
554
  tags: { $exists: false },
554
555
  sub: { names: { $exists: false } }
@@ -556,7 +557,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
556
557
  });
557
558
  assert(total === 1);
558
559
 
559
- total = await service.queryCount(WithNestedNestedLists, {
560
+ total = await service.countByQuery(WithNestedNestedLists, {
560
561
  where: {
561
562
  tags: { $exists: true },
562
563
  sub: { names: { $exists: true } }