@travetto/model-query 8.0.0 → 8.0.2
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 +47 -7
- package/__index__.ts +2 -0
- package/package.json +5 -5
- package/src/model/where-clause.ts +2 -0
- package/src/types/aggregate.ts +48 -0
- package/src/util/aggregate.ts +29 -0
- package/support/doc.support.tsx +3 -1
- package/support/test/aggregate.ts +107 -0
- package/support/test/polymorphism.ts +22 -0
package/README.md
CHANGED
|
@@ -127,12 +127,38 @@ export interface ModelQuerySuggestSupport extends ModelQuerySupport {
|
|
|
127
127
|
}
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Aggregate
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
**Code: Query Aggregate**
|
|
134
|
+
```typescript
|
|
135
|
+
export interface ModelQueryAggregateSupport extends ModelQuerySupport {
|
|
136
|
+
/**
|
|
137
|
+
* Run an aggregation on a field
|
|
138
|
+
* @param modelClass The model class to aggregate
|
|
139
|
+
* @param operation The operation to perform ('sum', 'avg', 'min', 'max')
|
|
140
|
+
* @param field The field to aggregate on
|
|
141
|
+
* @param query Additional query filtering
|
|
142
|
+
*/
|
|
143
|
+
aggregateFieldByQuery<
|
|
144
|
+
T extends ModelType,
|
|
145
|
+
Op extends AggregateOperation,
|
|
146
|
+
F extends (Op extends AggregateNumericOperation ? ValidNumericFields<T> : ValidComparableFields<T>)
|
|
147
|
+
>(
|
|
148
|
+
modelClass: Class<T>,
|
|
149
|
+
operation: Op,
|
|
150
|
+
field: F,
|
|
151
|
+
query?: ModelQuery<T>
|
|
152
|
+
): Promise<AggregateResultType<T, F>>;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
130
156
|
## 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'|
|
|
157
|
+
|Service|Query|QueryCrud|QueryFacet|QueryAggregate|
|
|
158
|
+
|-------|-----|---------|----------|--------------|
|
|
159
|
+
|[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|
|
|
160
|
+
|[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'|
|
|
161
|
+
|[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
162
|
|
|
137
163
|
## Querying
|
|
138
164
|
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 +253,15 @@ In addition to the provided contracts, the module also provides common utilities
|
|
|
227
253
|
```typescript
|
|
228
254
|
import { Config } from '@travetto/config';
|
|
229
255
|
import { Injectable } from '@travetto/di';
|
|
230
|
-
import type {
|
|
256
|
+
import type {
|
|
257
|
+
ModelQueryAggregateSupport,
|
|
258
|
+
ModelQueryCrudSupport,
|
|
259
|
+
ModelQueryFacetSupport,
|
|
260
|
+
ModelQuerySuggestSupport
|
|
261
|
+
} from '@travetto/model-query';
|
|
231
262
|
import { Suite } from '@travetto/test';
|
|
232
263
|
|
|
264
|
+
import { ModelQueryAggregateSuite } from '@travetto/model-query/support/test/aggregate.ts';
|
|
233
265
|
import { ModelQueryCrudSuite } from '@travetto/model-query/support/test/crud.ts';
|
|
234
266
|
import { ModelQueryFacetSuite } from '@travetto/model-query/support/test/facet.ts';
|
|
235
267
|
import { ModelQueryPolymorphismSuite } from '@travetto/model-query/support/test/polymorphism.ts';
|
|
@@ -242,7 +274,9 @@ import { QueryModelService } from './query-service.ts';
|
|
|
242
274
|
class CustomModelConfig {}
|
|
243
275
|
|
|
244
276
|
@Injectable()
|
|
245
|
-
class CustomModelService
|
|
277
|
+
class CustomModelService
|
|
278
|
+
extends QueryModelService
|
|
279
|
+
implements ModelQueryAggregateSupport, ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySuggestSupport {}
|
|
246
280
|
|
|
247
281
|
@Suite()
|
|
248
282
|
class CustomQuerySuite extends ModelQuerySuite {
|
|
@@ -250,6 +284,12 @@ class CustomQuerySuite extends ModelQuerySuite {
|
|
|
250
284
|
configClass = CustomModelConfig;
|
|
251
285
|
}
|
|
252
286
|
|
|
287
|
+
@Suite()
|
|
288
|
+
class CustomQueryAggregateSuite extends ModelQueryAggregateSuite {
|
|
289
|
+
serviceClass = CustomModelService;
|
|
290
|
+
configClass = CustomModelConfig;
|
|
291
|
+
}
|
|
292
|
+
|
|
253
293
|
@Suite()
|
|
254
294
|
class CustomQueryCrudSuite extends ModelQueryCrudSuite {
|
|
255
295
|
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.
|
|
3
|
+
"version": "8.0.2",
|
|
4
4
|
"description": "Datastore abstraction for advanced query support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@travetto/di": "^8.0.
|
|
34
|
-
"@travetto/model": "^8.0.
|
|
35
|
-
"@travetto/schema": "^8.0.
|
|
33
|
+
"@travetto/di": "^8.0.1",
|
|
34
|
+
"@travetto/model": "^8.0.2",
|
|
35
|
+
"@travetto/schema": "^8.0.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/test": "^8.0.
|
|
38
|
+
"@travetto/test": "^8.0.1"
|
|
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,48 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
package/support/doc.support.tsx
CHANGED
|
@@ -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
|
+
}
|
|
@@ -7,10 +7,12 @@ 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';
|
|
@@ -109,4 +111,24 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
|
|
|
109
111
|
const docFacet = await svc.facetByQuery(Doctor, 'specialty');
|
|
110
112
|
assert.deepStrictEqual(docFacet, [{ count: 2, key: 'eyes' }]);
|
|
111
113
|
}
|
|
114
|
+
|
|
115
|
+
@Test({ skip: ModelQueryPolymorphismSuite.ifNot(ModelQueryAggregateUtil.isSupported) })
|
|
116
|
+
async testAggregateQuery() {
|
|
117
|
+
const service: ModelQueryAggregateSupport & ModelQuerySupport = castTo(await this.service);
|
|
118
|
+
const [doctorOne, doctorTwo, firefighterOne, firefighterTwo] = [
|
|
119
|
+
Doctor.from({ name: 'bob', specialty: 'eyes' }),
|
|
120
|
+
Doctor.from({ name: 'nob', specialty: 'eyes' }),
|
|
121
|
+
Firefighter.from({ name: 'rob', firehouse: 20 }),
|
|
122
|
+
Firefighter.from({ name: 'fob', firehouse: 30 })
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
await this.saveAll(Worker, [doctorOne, doctorTwo, firefighterOne, firefighterTwo]);
|
|
126
|
+
assert((await this.getSize(Worker)) === 4);
|
|
127
|
+
|
|
128
|
+
const sumFirehouse = await service.aggregateFieldByQuery(Firefighter, 'sum', 'firehouse');
|
|
129
|
+
assert(sumFirehouse === 50);
|
|
130
|
+
|
|
131
|
+
const averageFirehouse = await service.aggregateFieldByQuery(Firefighter, 'avg', 'firehouse');
|
|
132
|
+
assert(averageFirehouse === 25);
|
|
133
|
+
}
|
|
112
134
|
}
|