@travetto/model-query 8.0.0-alpha.9 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -14
- package/__index__.ts +5 -6
- package/package.json +16 -16
- package/src/internal/types.ts +16 -10
- package/src/model/indexes.ts +36 -0
- package/src/model/query.ts +5 -7
- package/src/model/where-clause.ts +31 -29
- package/src/types/crud.ts +4 -4
- package/src/types/facet.ts +5 -5
- package/src/types/query.ts +2 -2
- package/src/types/suggest.ts +15 -5
- package/src/util/crud.ts +2 -2
- package/src/util/facet.ts +2 -2
- package/src/util/query.ts +29 -26
- package/src/util/suggest.ts +28 -11
- package/src/verifier.ts +24 -30
- package/support/doc.support.tsx +6 -7
- package/support/test/crud.ts +90 -52
- package/support/test/facet.ts +23 -15
- package/support/test/model.ts +34 -2
- package/support/test/polymorphism.ts +24 -26
- package/support/test/query.ts +353 -119
- package/support/test/suggest.ts +60 -8
package/src/util/suggest.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModelRegistryIndex, type ModelType } from '@travetto/model';
|
|
2
|
-
import {
|
|
2
|
+
import { type Class, castKey, castTo, hasFunction } from '@travetto/runtime';
|
|
3
3
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
5
|
import type { PageableModelQuery, Query } from '../model/query.ts';
|
|
@@ -10,11 +10,10 @@ import type { ModelQuerySuggestSupport } from '../types/suggest.ts';
|
|
|
10
10
|
* Tools for building suggestion queries
|
|
11
11
|
*/
|
|
12
12
|
export class ModelQuerySuggestUtil {
|
|
13
|
-
|
|
14
13
|
/**
|
|
15
14
|
* Type guard for determining if service supports query suggest operations
|
|
16
15
|
*/
|
|
17
|
-
static isSupported = hasFunction<ModelQuerySuggestSupport>('
|
|
16
|
+
static isSupported = hasFunction<ModelQuerySuggestSupport>('suggestByQuery');
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Build regex for suggesting
|
|
@@ -28,7 +27,15 @@ export class ModelQuerySuggestUtil {
|
|
|
28
27
|
*/
|
|
29
28
|
static getSuggestQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: Query<T>): Query<T> {
|
|
30
29
|
const limit = query?.limit ?? 10;
|
|
31
|
-
const clauses: WhereClauseRaw<ModelType>[] =
|
|
30
|
+
const clauses: WhereClauseRaw<ModelType>[] = [];
|
|
31
|
+
if (prefix) {
|
|
32
|
+
const parts = `${field}`.split('.');
|
|
33
|
+
let o: WhereClauseRaw<ModelType> = { [parts.at(-1)!]: { $regex: this.getSuggestRegex(prefix) } };
|
|
34
|
+
for (let i = parts.length - 2; i >= 0; i -= 1) {
|
|
35
|
+
o = { [parts[i]]: o };
|
|
36
|
+
}
|
|
37
|
+
clauses.push(o);
|
|
38
|
+
}
|
|
32
39
|
const select: Query<T>['select'] = {
|
|
33
40
|
...query?.select
|
|
34
41
|
};
|
|
@@ -36,9 +43,9 @@ export class ModelQuerySuggestUtil {
|
|
|
36
43
|
const polymorphicConfig = SchemaRegistryIndex.getDiscriminatedConfig(cls);
|
|
37
44
|
if (polymorphicConfig) {
|
|
38
45
|
clauses.push({
|
|
39
|
-
[polymorphicConfig.discriminatedField]: polymorphicConfig.discriminatedBase
|
|
40
|
-
{ $in: SchemaRegistryIndex.getDiscriminatedTypes(cls) }
|
|
41
|
-
polymorphicConfig.discriminatedType
|
|
46
|
+
[polymorphicConfig.discriminatedField]: polymorphicConfig.discriminatedBase
|
|
47
|
+
? { $in: SchemaRegistryIndex.getDiscriminatedTypes(cls) }
|
|
48
|
+
: polymorphicConfig.discriminatedType
|
|
42
49
|
});
|
|
43
50
|
if (query?.select) {
|
|
44
51
|
Object.assign(select, { [polymorphicConfig.discriminatedField]: true });
|
|
@@ -75,8 +82,13 @@ export class ModelQuerySuggestUtil {
|
|
|
75
82
|
const pattern = this.getSuggestRegex(prefix);
|
|
76
83
|
|
|
77
84
|
const out: ([string, U] | readonly [string, U])[] = [];
|
|
85
|
+
const parts = `${field}`.split('.');
|
|
86
|
+
|
|
78
87
|
for (const result of results) {
|
|
79
|
-
|
|
88
|
+
let resultValue = result[castKey<T>(parts[0])];
|
|
89
|
+
for (let i = 1; i < parts.length && resultValue !== undefined && resultValue !== null; i += 1) {
|
|
90
|
+
resultValue = resultValue[castKey(parts[i])];
|
|
91
|
+
}
|
|
80
92
|
if (Array.isArray(resultValue)) {
|
|
81
93
|
out.push(...resultValue.filter(item => pattern.test(item)).map((item: string) => [item, transform(item, result)] as const));
|
|
82
94
|
} else if (typeof resultValue === 'string') {
|
|
@@ -85,7 +97,7 @@ export class ModelQuerySuggestUtil {
|
|
|
85
97
|
}
|
|
86
98
|
return out
|
|
87
99
|
.toSorted((a, b) => a[0].localeCompare(b[0]))
|
|
88
|
-
.map(
|
|
100
|
+
.map(a => a[1])
|
|
89
101
|
.filter((result, i, arr) => result !== arr[i - 1])
|
|
90
102
|
.slice(0, limit ?? 10);
|
|
91
103
|
}
|
|
@@ -93,7 +105,12 @@ export class ModelQuerySuggestUtil {
|
|
|
93
105
|
/**
|
|
94
106
|
* Build suggestion query
|
|
95
107
|
*/
|
|
96
|
-
static getSuggestFieldQuery<T extends ModelType>(
|
|
108
|
+
static getSuggestFieldQuery<T extends ModelType>(
|
|
109
|
+
cls: Class<T>,
|
|
110
|
+
field: ValidStringFields<T>,
|
|
111
|
+
prefix?: string,
|
|
112
|
+
query?: PageableModelQuery<T>
|
|
113
|
+
): Query<T> {
|
|
97
114
|
return this.getSuggestQuery<T>(cls, castTo(field), prefix, query);
|
|
98
115
|
}
|
|
99
|
-
}
|
|
116
|
+
}
|
package/src/verifier.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none"} ] */
|
|
2
|
-
import { DataUtil, ValidationResultError, type ValidationError, SchemaRegistryIndex } from '@travetto/schema';
|
|
3
|
-
import { JSONUtil, type Class } from '@travetto/runtime';
|
|
4
2
|
|
|
5
|
-
import
|
|
3
|
+
import { type Class, JSONUtil } from '@travetto/runtime';
|
|
4
|
+
import { DataUtil, SchemaRegistryIndex, type ValidationError, ValidationResultError } from '@travetto/schema';
|
|
6
5
|
|
|
7
6
|
import { TypeUtil } from './internal/types.ts';
|
|
7
|
+
import type { ModelQuery, PageableModelQuery, Query } from './model/query.ts';
|
|
8
8
|
|
|
9
9
|
type SimpleType = keyof typeof TypeUtil.OPERATORS;
|
|
10
10
|
|
|
@@ -28,24 +28,19 @@ const WHERE = 'where';
|
|
|
28
28
|
const SORT = 'sort';
|
|
29
29
|
// const GROUP_BY = 'groupBy';
|
|
30
30
|
|
|
31
|
-
const MULTIPLE_KEYS_ALLOWED = new Set([
|
|
32
|
-
'$maxDistance', '$gt', '$gte',
|
|
33
|
-
'$minDistance', '$lt', '$lte',
|
|
34
|
-
'$near'
|
|
35
|
-
]);
|
|
31
|
+
const MULTIPLE_KEYS_ALLOWED = new Set(['$maxDistance', '$gt', '$gte', '$minDistance', '$lt', '$lte', '$near']);
|
|
36
32
|
|
|
37
33
|
/**
|
|
38
34
|
* Query verification service. Used to verify the query is valid before running.
|
|
39
35
|
*/
|
|
40
36
|
export class QueryVerifier {
|
|
41
|
-
|
|
42
37
|
/**
|
|
43
38
|
* Internal mapping for various clauses
|
|
44
39
|
*/
|
|
45
40
|
static #mapping = [
|
|
46
41
|
[SELECT, 'processSelectClause'] as const,
|
|
47
42
|
[WHERE, 'processWhereClause'] as const,
|
|
48
|
-
[SORT, 'processSortClause'] as const
|
|
43
|
+
[SORT, 'processSortClause'] as const
|
|
49
44
|
];
|
|
50
45
|
|
|
51
46
|
/**
|
|
@@ -59,19 +54,18 @@ export class QueryVerifier {
|
|
|
59
54
|
return;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
if (handler.preMember
|
|
57
|
+
if (handler.preMember?.(state, clause)) {
|
|
63
58
|
return;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
for (const [key, value] of Object.entries(clause)) {
|
|
67
|
-
|
|
68
62
|
// Validate value is correct, and key is valid
|
|
69
63
|
if (value === undefined || value === null) {
|
|
70
64
|
// state.log(`${key} cannot be undefined or null`);
|
|
71
65
|
continue;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
if (handler.preMember
|
|
68
|
+
if (handler.preMember?.(state, value)) {
|
|
75
69
|
continue;
|
|
76
70
|
}
|
|
77
71
|
|
|
@@ -91,7 +85,7 @@ export class QueryVerifier {
|
|
|
91
85
|
// Otherwise recurse
|
|
92
86
|
const subCls = field.type;
|
|
93
87
|
const subValue = value;
|
|
94
|
-
if (handler.onComplexType
|
|
88
|
+
if (handler.onComplexType?.(state, subCls, subValue, field.array ?? false)) {
|
|
95
89
|
continue;
|
|
96
90
|
}
|
|
97
91
|
this.processGenericClause(state.extend(key), subCls, subValue, handler);
|
|
@@ -109,7 +103,13 @@ export class QueryVerifier {
|
|
|
109
103
|
/**
|
|
110
104
|
* Check operator clause
|
|
111
105
|
*/
|
|
112
|
-
static checkOperatorClause(
|
|
106
|
+
static checkOperatorClause(
|
|
107
|
+
state: State,
|
|
108
|
+
declaredType: SimpleType,
|
|
109
|
+
value: unknown,
|
|
110
|
+
allowed: Record<string, Set<string>>,
|
|
111
|
+
isArray: boolean
|
|
112
|
+
): void {
|
|
113
113
|
if (isArray && Array.isArray(value)) {
|
|
114
114
|
// Handle array literal
|
|
115
115
|
for (const item of value) {
|
|
@@ -128,11 +128,7 @@ export class QueryVerifier {
|
|
|
128
128
|
} else {
|
|
129
129
|
const keys = Object.keys(value).toSorted();
|
|
130
130
|
|
|
131
|
-
if (keys.length !== 1 && !(
|
|
132
|
-
keys.length >= 2 &&
|
|
133
|
-
MULTIPLE_KEYS_ALLOWED.has(keys[0]) ||
|
|
134
|
-
MULTIPLE_KEYS_ALLOWED.has(keys[1])
|
|
135
|
-
)) {
|
|
131
|
+
if (keys.length !== 1 && !((keys.length >= 2 && MULTIPLE_KEYS_ALLOWED.has(keys[0])) || MULTIPLE_KEYS_ALLOWED.has(keys[1]))) {
|
|
136
132
|
state.log('One and only one operation may be specified in an operator clause');
|
|
137
133
|
return;
|
|
138
134
|
}
|
|
@@ -172,7 +168,7 @@ export class QueryVerifier {
|
|
|
172
168
|
* Process where clause
|
|
173
169
|
*/
|
|
174
170
|
static processWhereClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
175
|
-
|
|
171
|
+
this.processGenericClause(st, cls, passed, {
|
|
176
172
|
preMember: (state: State, value: Record<string, unknown>) => {
|
|
177
173
|
const keys = Object.keys(value);
|
|
178
174
|
const firstKey = keys[0];
|
|
@@ -221,7 +217,7 @@ export class QueryVerifier {
|
|
|
221
217
|
* Handle sort clause
|
|
222
218
|
*/
|
|
223
219
|
static processSortClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
224
|
-
|
|
220
|
+
this.processGenericClause(st, cls, passed, {
|
|
225
221
|
onSimpleType: (state, type, value) => {
|
|
226
222
|
if (value === 1 || value === -1 || typeof value === 'boolean') {
|
|
227
223
|
return;
|
|
@@ -235,7 +231,7 @@ export class QueryVerifier {
|
|
|
235
231
|
* Handle select clause
|
|
236
232
|
*/
|
|
237
233
|
static processSelectClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
238
|
-
|
|
234
|
+
this.processGenericClause(st, cls, passed, {
|
|
239
235
|
onSimpleType: (state, type, value) => {
|
|
240
236
|
const actual = TypeUtil.getActualType(value);
|
|
241
237
|
if (actual === 'number' || actual === 'boolean') {
|
|
@@ -244,7 +240,7 @@ export class QueryVerifier {
|
|
|
244
240
|
}
|
|
245
241
|
state.log('Only true, false 0, and 1 are allowed for including/excluding fields');
|
|
246
242
|
} else {
|
|
247
|
-
|
|
243
|
+
/* if (actual === 'string') {
|
|
248
244
|
if (!/[A-Za-z_$0-9]/.test(value)) {
|
|
249
245
|
state.log(`Only A-Z, a-z, 0-9, '$' and '_' are allowed in aliases for selecting fields`);
|
|
250
246
|
return;
|
|
@@ -258,7 +254,8 @@ export class QueryVerifier {
|
|
|
258
254
|
// or { alias: string, calc?: string }
|
|
259
255
|
// console.log('Yay');
|
|
260
256
|
}
|
|
261
|
-
*/
|
|
257
|
+
*/
|
|
258
|
+
}
|
|
262
259
|
state.log('Only true, false, 0, and 1 are allowed for selecting fields');
|
|
263
260
|
}
|
|
264
261
|
});
|
|
@@ -293,10 +290,7 @@ export class QueryVerifier {
|
|
|
293
290
|
continue;
|
|
294
291
|
}
|
|
295
292
|
|
|
296
|
-
if (!(key in query)
|
|
297
|
-
|| query[key] === undefined
|
|
298
|
-
|| query[key] === null
|
|
299
|
-
) {
|
|
293
|
+
if (!(key in query) || query[key] === undefined || query[key] === null) {
|
|
300
294
|
continue;
|
|
301
295
|
}
|
|
302
296
|
|
|
@@ -316,4 +310,4 @@ export class QueryVerifier {
|
|
|
316
310
|
throw new ValidationResultError(errors);
|
|
317
311
|
}
|
|
318
312
|
}
|
|
319
|
-
}
|
|
313
|
+
}
|
package/support/doc.support.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** @jsxImportSource @travetto/doc/support */
|
|
2
|
-
import {
|
|
3
|
-
import { Runtime, toConcrete } from '@travetto/runtime';
|
|
2
|
+
import { DocFileUtil, type DocJSXElement, type DocJSXElementByFn, d } from '@travetto/doc';
|
|
3
|
+
import { castKey, Runtime, toConcrete } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import type { ModelQueryCrudSupport } from '../src/types/crud.ts';
|
|
6
|
+
import type { ModelQueryFacetSupport } from '../src/types/facet.ts';
|
|
6
7
|
import type { ModelQuerySupport } from '../src/types/query.ts';
|
|
7
|
-
import type { ModelQueryFacetSupport, } from '../src/types/facet.ts';
|
|
8
8
|
import type { ModelQuerySuggestSupport } from '../src/types/suggest.ts';
|
|
9
9
|
|
|
10
10
|
const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'> =>
|
|
@@ -14,7 +14,7 @@ export const Links = {
|
|
|
14
14
|
QueryCrud: toLink('Query Crud', toConcrete<ModelQueryCrudSupport>()),
|
|
15
15
|
QueryFacet: toLink('Facet', toConcrete<ModelQueryFacetSupport>()),
|
|
16
16
|
QuerySuggest: toLink('Suggest', toConcrete<ModelQuerySuggestSupport>()),
|
|
17
|
-
Query: toLink('Query', toConcrete<ModelQuerySupport>())
|
|
17
|
+
Query: toLink('Query', toConcrete<ModelQuerySupport>())
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export const ModelQueryTypes = (fn: Function): DocJSXElement[] => {
|
|
@@ -24,10 +24,9 @@ export const ModelQueryTypes = (fn: Function): DocJSXElement[] => {
|
|
|
24
24
|
for (const [, key] of content.matchAll(/Model(Query(Suggest|Facet|Crud)?)Support/g)) {
|
|
25
25
|
if (!seen.has(key) && key in Links) {
|
|
26
26
|
seen.add(key);
|
|
27
|
-
|
|
28
|
-
const link = Links[key as keyof typeof Links];
|
|
27
|
+
const link = Links[castKey(key)];
|
|
29
28
|
found.push(link);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
return found.map(type => <li>{type}</li>);
|
|
33
|
-
};
|
|
32
|
+
};
|
package/support/test/crud.ts
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
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
5
|
import { Suite, Test } from '@travetto/test';
|
|
4
|
-
import { type ModelCrudSupport, NotFoundError } from '@travetto/model';
|
|
5
6
|
|
|
6
7
|
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { QueryIndex } from '../../__index__.ts';
|
|
9
10
|
import type { ModelQueryCrudSupport } from '../../src/types/crud.ts';
|
|
11
|
+
import { Address, BigIntModel, Person, Todo } from './model.ts';
|
|
12
|
+
|
|
13
|
+
@Model()
|
|
14
|
+
@QueryIndex({
|
|
15
|
+
name: 'uniqueUser2',
|
|
16
|
+
fields: [{ name: true }],
|
|
17
|
+
unique: true
|
|
18
|
+
})
|
|
19
|
+
class UniqueUser2 {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
@Suite()
|
|
12
25
|
export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudSupport & ModelCrudSupport> {
|
|
26
|
+
supportsUniqueIndexes = true;
|
|
27
|
+
|
|
28
|
+
@Test({ skip: self => !castTo<ModelQueryCrudSuite>(self).supportsUniqueIndexes })
|
|
29
|
+
async testUnique() {
|
|
30
|
+
const svc = await this.service;
|
|
31
|
+
await svc.create(UniqueUser2, UniqueUser2.from({ name: 'bob' }));
|
|
32
|
+
await assert.rejects(() => svc.create(UniqueUser2, UniqueUser2.from({ name: 'bob' })), UniqueError);
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
@Test()
|
|
14
36
|
async testUpdateOneWithQuery() {
|
|
15
37
|
const svc = await this.service;
|
|
@@ -19,31 +41,44 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
19
41
|
assert(todo1.id);
|
|
20
42
|
assert(todo1.version === 0);
|
|
21
43
|
|
|
22
|
-
const todo1v2 = await svc.updateByQuery(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
const todo1v2 = await svc.updateByQuery(
|
|
45
|
+
Todo,
|
|
46
|
+
Todo.from({
|
|
47
|
+
id: todo1.id,
|
|
48
|
+
text: `${todo1.text}!!`,
|
|
49
|
+
version: todo1.version + 1
|
|
50
|
+
}),
|
|
51
|
+
{ where: { id: todo1.id, version: todo1.version } }
|
|
52
|
+
);
|
|
27
53
|
|
|
28
54
|
assert(todo1v2.id === todo1.id);
|
|
29
55
|
assert(todo1v2.version > todo1.version);
|
|
30
56
|
|
|
31
57
|
await assert.rejects(
|
|
32
|
-
() =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
() =>
|
|
59
|
+
svc.updateByQuery(
|
|
60
|
+
Todo,
|
|
61
|
+
Todo.from({
|
|
62
|
+
id: todo1.id,
|
|
63
|
+
text: `${todo1.text}!!`,
|
|
64
|
+
version: todo1.version + 1
|
|
65
|
+
}),
|
|
66
|
+
{ where: { id: todo1.id, version: todo1.version } }
|
|
67
|
+
),
|
|
37
68
|
NotFoundError
|
|
38
69
|
);
|
|
39
70
|
|
|
40
71
|
const todo2 = await svc.create(Todo, Todo.from({ text: 'bob2' }));
|
|
41
72
|
|
|
42
|
-
const result = await svc.updateByQuery(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
const result = await svc.updateByQuery(
|
|
74
|
+
Todo,
|
|
75
|
+
Todo.from({
|
|
76
|
+
id: todo2.id,
|
|
77
|
+
text: `${todo1.text}!!`,
|
|
78
|
+
version: todo1.version + 1
|
|
79
|
+
}),
|
|
80
|
+
{ where: { id: todo2.id, text: 'bob2' } }
|
|
81
|
+
);
|
|
47
82
|
|
|
48
83
|
assert(result.id === todo2.id);
|
|
49
84
|
}
|
|
@@ -91,15 +126,20 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
91
126
|
async testDeleteByQuery() {
|
|
92
127
|
const svc = await this.service;
|
|
93
128
|
|
|
94
|
-
const count = await this.saveAll(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
const count = await this.saveAll(
|
|
130
|
+
Person,
|
|
131
|
+
[1, 2, 3, 4, 5].map(d =>
|
|
132
|
+
Person.from({
|
|
133
|
+
age: d,
|
|
134
|
+
gender: 'm',
|
|
135
|
+
name: `Test ${d}`,
|
|
136
|
+
address: Address.from({
|
|
137
|
+
street1: 'street1',
|
|
138
|
+
street2: 'street2'
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
);
|
|
103
143
|
|
|
104
144
|
assert(count === 5);
|
|
105
145
|
|
|
@@ -107,33 +147,37 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
107
147
|
|
|
108
148
|
assert(c === 2);
|
|
109
149
|
|
|
110
|
-
assert(await svc.queryCount(Person, {}) === 3);
|
|
150
|
+
assert((await svc.queryCount(Person, {})) === 3);
|
|
111
151
|
|
|
112
152
|
const c2 = await svc.deleteByQuery(Person, { where: { age: { $lte: 3 } } });
|
|
113
153
|
|
|
114
154
|
assert(c2 === 3);
|
|
115
155
|
|
|
116
|
-
assert(await svc.queryCount(Person, {}) === 0);
|
|
117
|
-
|
|
156
|
+
assert((await svc.queryCount(Person, {})) === 0);
|
|
118
157
|
}
|
|
119
158
|
|
|
120
159
|
@Test()
|
|
121
160
|
async testUpdateByQuery() {
|
|
122
161
|
const svc = await this.service;
|
|
123
162
|
|
|
124
|
-
const count = await this.saveAll(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
const count = await this.saveAll(
|
|
164
|
+
Person,
|
|
165
|
+
[1, 2, 3, 4, 5].map(d =>
|
|
166
|
+
Person.from({
|
|
167
|
+
age: d,
|
|
168
|
+
gender: 'm',
|
|
169
|
+
name: `Test ${d}`,
|
|
170
|
+
address: Address.from({
|
|
171
|
+
street1: 'street1',
|
|
172
|
+
street2: 'street2'
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
);
|
|
133
177
|
|
|
134
178
|
assert(count === 5);
|
|
135
179
|
|
|
136
|
-
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 5);
|
|
180
|
+
assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 5);
|
|
137
181
|
|
|
138
182
|
const c = await svc.updatePartialByQuery(Person, { where: { age: { $gt: 3 } } }, { gender: 'f' });
|
|
139
183
|
|
|
@@ -141,15 +185,14 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
141
185
|
|
|
142
186
|
assert(c === 2);
|
|
143
187
|
|
|
144
|
-
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 3);
|
|
188
|
+
assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 3);
|
|
145
189
|
|
|
146
190
|
const c2 = await svc.updatePartialByQuery(Person, { where: { gender: 'm' } }, { gender: 'f' });
|
|
147
191
|
|
|
148
192
|
assert(c2 === 3);
|
|
149
193
|
|
|
150
|
-
assert(await svc.queryCount(Person, { where: { gender: 'f' } }) === 5);
|
|
151
|
-
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 0);
|
|
152
|
-
|
|
194
|
+
assert((await svc.queryCount(Person, { where: { gender: 'f' } })) === 5);
|
|
195
|
+
assert((await svc.queryCount(Person, { where: { gender: 'm' } })) === 0);
|
|
153
196
|
}
|
|
154
197
|
|
|
155
198
|
@Test()
|
|
@@ -162,7 +205,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
162
205
|
BigIntModel.from({ largeNumber: 200n, optionalBigInt: 2000n }),
|
|
163
206
|
BigIntModel.from({ largeNumber: 300n }),
|
|
164
207
|
BigIntModel.from({ largeNumber: 9007199254740991n, optionalBigInt: 1234567890123456789n }),
|
|
165
|
-
BigIntModel.from({ largeNumber: 18014398509481982n })
|
|
208
|
+
BigIntModel.from({ largeNumber: 18014398509481982n })
|
|
166
209
|
]);
|
|
167
210
|
|
|
168
211
|
assert(count === 5);
|
|
@@ -205,17 +248,13 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
205
248
|
const count = await this.saveAll(BigIntModel, [
|
|
206
249
|
BigIntModel.from({ largeNumber: 100n, optionalBigInt: 1000n }),
|
|
207
250
|
BigIntModel.from({ largeNumber: 200n, optionalBigInt: 2000n }),
|
|
208
|
-
BigIntModel.from({ largeNumber: 300n })
|
|
251
|
+
BigIntModel.from({ largeNumber: 300n })
|
|
209
252
|
]);
|
|
210
253
|
|
|
211
254
|
assert(count === 3);
|
|
212
255
|
|
|
213
256
|
// Update records with bigint condition
|
|
214
|
-
const updated = await svc.updatePartialByQuery(
|
|
215
|
-
BigIntModel,
|
|
216
|
-
{ where: { largeNumber: { $lte: 200n } } },
|
|
217
|
-
{ optionalBigInt: 5000n }
|
|
218
|
-
);
|
|
257
|
+
const updated = await svc.updatePartialByQuery(BigIntModel, { where: { largeNumber: { $lte: 200n } } }, { optionalBigInt: 5000n });
|
|
219
258
|
|
|
220
259
|
assert(updated === 2);
|
|
221
260
|
|
|
@@ -235,7 +274,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
235
274
|
BigIntModel.from({ largeNumber: 200n }),
|
|
236
275
|
BigIntModel.from({ largeNumber: 300n }),
|
|
237
276
|
BigIntModel.from({ largeNumber: 400n }),
|
|
238
|
-
BigIntModel.from({ largeNumber: 500n })
|
|
277
|
+
BigIntModel.from({ largeNumber: 500n })
|
|
239
278
|
]);
|
|
240
279
|
|
|
241
280
|
assert(count === 5);
|
|
@@ -253,4 +292,3 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
253
292
|
assert(all.every(x => x.largeNumber <= 300n));
|
|
254
293
|
}
|
|
255
294
|
}
|
|
256
|
-
|
package/support/test/facet.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { Suite, Test } from '@travetto/test';
|
|
|
5
5
|
|
|
6
6
|
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
7
7
|
|
|
8
|
-
import { Person } from './model.ts';
|
|
9
8
|
import type { ModelQueryFacetSupport } from '../../src/types/facet.ts';
|
|
9
|
+
import { Person } from './model.ts';
|
|
10
10
|
|
|
11
11
|
const pick = <T>(arr: T[] | readonly T[]): T => arr[Math.trunc(Math.random() * arr.length)]!;
|
|
12
12
|
|
|
@@ -17,31 +17,39 @@ const AGES = new Array(100).fill(0).map((x, i) => i + 10);
|
|
|
17
17
|
|
|
18
18
|
@Suite()
|
|
19
19
|
export abstract class ModelQueryFacetSuite extends BaseModelSuite<ModelQueryFacetSupport & ModelCrudSupport> {
|
|
20
|
-
|
|
21
20
|
@Test('verify aggregations')
|
|
22
21
|
async testFacet() {
|
|
23
|
-
const people = ' '
|
|
22
|
+
const people = ' '
|
|
23
|
+
.repeat(50)
|
|
24
24
|
.split('')
|
|
25
|
-
.map(() =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
.map(() =>
|
|
26
|
+
Person.from({
|
|
27
|
+
age: pick(AGES),
|
|
28
|
+
gender: pick(GENDERS),
|
|
29
|
+
name: `${pick(FNAME)} ${pick(LNAME)}`,
|
|
30
|
+
address: {
|
|
31
|
+
street1: `${pick(AGES)} ${pick(LNAME)} Road`
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
);
|
|
33
35
|
|
|
34
36
|
const svc = await this.service;
|
|
35
37
|
const saved = await this.saveAll(Person, people);
|
|
36
38
|
|
|
37
39
|
assert(saved === 50);
|
|
38
40
|
|
|
39
|
-
const results = await svc.
|
|
41
|
+
const results = await svc.facetByQuery(Person, 'gender');
|
|
40
42
|
|
|
41
43
|
assert(results.length === 2);
|
|
42
44
|
assert(results[0].count >= results[1].count);
|
|
43
45
|
|
|
44
|
-
const genders = people.reduce(
|
|
46
|
+
const genders = people.reduce(
|
|
47
|
+
(acc, p) => {
|
|
48
|
+
acc[p.gender] += 1;
|
|
49
|
+
return acc;
|
|
50
|
+
},
|
|
51
|
+
{ m: 0, f: 0 }
|
|
52
|
+
);
|
|
45
53
|
|
|
46
54
|
assert(results.find(x => x.key === 'm')!.count === genders.m);
|
|
47
55
|
assert(results.find(x => x.key === 'f')!.count === genders.f);
|
|
@@ -51,7 +59,7 @@ export abstract class ModelQueryFacetSuite extends BaseModelSuite<ModelQueryFace
|
|
|
51
59
|
names[el.name!] = (names[el.name!] || 0) + 1;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
const nameFacet = await svc.
|
|
62
|
+
const nameFacet = await svc.facetByQuery(Person, 'name');
|
|
55
63
|
assert(Object.keys(names).length === nameFacet.length);
|
|
56
64
|
}
|
|
57
|
-
}
|
|
65
|
+
}
|
package/support/test/model.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Precision, Schema, Text, type Point } from '@travetto/schema';
|
|
2
1
|
import { Model, type ModelType } from '@travetto/model';
|
|
2
|
+
import { type Point, Precision, Schema, Text } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
@Schema()
|
|
5
5
|
export class Address {
|
|
@@ -98,4 +98,36 @@ export class BigIntModel {
|
|
|
98
98
|
id: string;
|
|
99
99
|
largeNumber: bigint;
|
|
100
100
|
optionalBigInt?: bigint;
|
|
101
|
-
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Schema()
|
|
104
|
+
export class Child {
|
|
105
|
+
name: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Schema()
|
|
109
|
+
export class Family {
|
|
110
|
+
children?: Child[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Model()
|
|
114
|
+
export class PersonFamily implements ModelType {
|
|
115
|
+
id: string;
|
|
116
|
+
family?: Family;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Schema()
|
|
120
|
+
export class RecipeIngredient {
|
|
121
|
+
name: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Schema()
|
|
125
|
+
export class RecipeSection {
|
|
126
|
+
ingredients: RecipeIngredient[] = [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Model()
|
|
130
|
+
export class Recipe implements ModelType {
|
|
131
|
+
id: string;
|
|
132
|
+
sections: RecipeSection[] = [];
|
|
133
|
+
}
|