@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 +54 -13
- package/__index__.ts +2 -0
- package/package.json +4 -4
- package/src/model/where-clause.ts +2 -0
- package/src/types/aggregate.ts +55 -0
- package/src/types/query.ts +0 -6
- package/src/util/aggregate.ts +29 -0
- package/support/doc.support.tsx +3 -1
- package/support/test/aggregate.ts +107 -0
- package/support/test/crud.ts +17 -11
- package/support/test/polymorphism.ts +30 -6
- package/support/test/query.ts +21 -20
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 {
|
|
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
|
|
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.
|
|
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.
|
|
35
|
-
"@travetto/schema": "^8.0.
|
|
34
|
+
"@travetto/model": "^8.0.3",
|
|
35
|
+
"@travetto/schema": "^8.0.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/test": "^8.0.
|
|
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
|
+
}
|
package/src/types/query.ts
CHANGED
|
@@ -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
|
+
}
|
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
|
+
}
|
package/support/test/crud.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
195
|
-
assert((await
|
|
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
|
|
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
|
|
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<
|
|
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.
|
|
38
|
-
assert((await svc.
|
|
39
|
-
assert((await svc.
|
|
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.
|
|
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
|
}
|
package/support/test/query.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
560
|
+
total = await service.countByQuery(WithNestedNestedLists, {
|
|
560
561
|
where: {
|
|
561
562
|
tags: { $exists: true },
|
|
562
563
|
sub: { names: { $exists: true } }
|