@travetto/model-query 7.0.0-rc.1 → 7.0.0-rc.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 +5 -5
- package/package.json +5 -5
- package/src/internal/types.ts +16 -16
- package/src/util/crud.ts +3 -3
- package/src/util/query.ts +24 -24
- package/src/util/suggest.ts +7 -7
- package/src/verifier.ts +39 -39
- package/support/doc.support.tsx +1 -1
package/README.md
CHANGED
|
@@ -235,31 +235,31 @@ class CustomModelService extends QueryModelService implements ModelQueryCrudSupp
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
@Suite()
|
|
238
|
-
|
|
238
|
+
class CustomQuerySuite extends ModelQuerySuite {
|
|
239
239
|
serviceClass = CustomModelService;
|
|
240
240
|
configClass = CustomModelConfig;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
@Suite()
|
|
244
|
-
|
|
244
|
+
class CustomQueryCrudSuite extends ModelQueryCrudSuite {
|
|
245
245
|
serviceClass = CustomModelService;
|
|
246
246
|
configClass = CustomModelConfig;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
@Suite()
|
|
250
|
-
|
|
250
|
+
class CustomQueryFacetSuite extends ModelQueryFacetSuite {
|
|
251
251
|
serviceClass = CustomModelService;
|
|
252
252
|
configClass = CustomModelConfig;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
@Suite()
|
|
256
|
-
|
|
256
|
+
class CustomQueryPolymorphismSuite extends ModelQueryPolymorphismSuite {
|
|
257
257
|
serviceClass = CustomModelService;
|
|
258
258
|
configClass = CustomModelConfig;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
@Suite()
|
|
262
|
-
|
|
262
|
+
class CustomQuerySuggestSuite extends ModelQuerySuggestSuite {
|
|
263
263
|
serviceClass = CustomModelService;
|
|
264
264
|
configClass = CustomModelConfig;
|
|
265
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-query",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "Datastore abstraction for advanced query support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"directory": "module/model-query"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/di": "^7.0.0-rc.
|
|
30
|
-
"@travetto/model": "^7.0.0-rc.
|
|
31
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
29
|
+
"@travetto/di": "^7.0.0-rc.3",
|
|
30
|
+
"@travetto/model": "^7.0.0-rc.3",
|
|
31
|
+
"@travetto/schema": "^7.0.0-rc.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/test": "^7.0.0-rc.
|
|
34
|
+
"@travetto/test": "^7.0.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/test": {
|
package/src/internal/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SchemaFieldConfig, Point } from '@travetto/schema';
|
|
2
2
|
import { Class, toConcrete } from '@travetto/runtime';
|
|
3
3
|
|
|
4
|
-
const st = (
|
|
5
|
-
new Set((Array.isArray(
|
|
4
|
+
const st = (value: string | string[], isArray: boolean = false): Set<string> =>
|
|
5
|
+
new Set((Array.isArray(value) ? value : [value]).map(item => isArray ? `${item}[]` : item));
|
|
6
6
|
|
|
7
7
|
const basic = (types: Set<string>): Record<string, Set<string>> => ({ $ne: types, $eq: types, $exists: st('boolean') });
|
|
8
8
|
const scalar = (types: Set<string>): Record<string, Set<string>> => ({ $in: types, $nin: types });
|
|
@@ -16,7 +16,7 @@ const geo = (type: string): Record<string, Set<string>> => ({
|
|
|
16
16
|
$geoIntersects: st(type, true)
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const PointConcrete = toConcrete<Point>();
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Basic type support
|
|
@@ -36,17 +36,17 @@ export class TypeUtil {
|
|
|
36
36
|
/**
|
|
37
37
|
* Get declared type of a given field, only for primitive types
|
|
38
38
|
*/
|
|
39
|
-
static getDeclaredType(
|
|
40
|
-
const type = 'type' in
|
|
39
|
+
static getDeclaredType(field: SchemaFieldConfig | Class): keyof typeof TypeUtil.OPERATORS | undefined {
|
|
40
|
+
const type = 'type' in field ? field.type : field;
|
|
41
41
|
switch (type) {
|
|
42
42
|
case String: return 'string';
|
|
43
43
|
case Number: return 'number';
|
|
44
44
|
case Boolean: return 'boolean';
|
|
45
45
|
case Date: return 'Date';
|
|
46
|
-
case
|
|
46
|
+
case PointConcrete: return 'Point';
|
|
47
47
|
default: {
|
|
48
|
-
if ('type' in
|
|
49
|
-
return this.getDeclaredType(
|
|
48
|
+
if ('type' in field && field.array) {
|
|
49
|
+
return this.getDeclaredType(field.type);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -55,22 +55,22 @@ export class TypeUtil {
|
|
|
55
55
|
/**
|
|
56
56
|
* Get the actual type of a given field, only for primitive types
|
|
57
57
|
*/
|
|
58
|
-
static getActualType(
|
|
59
|
-
const type = typeof
|
|
58
|
+
static getActualType(value: unknown): string {
|
|
59
|
+
const type = typeof value;
|
|
60
60
|
if (['string', 'number', 'boolean'].includes(type)) {
|
|
61
61
|
return type;
|
|
62
|
-
} else if (
|
|
62
|
+
} else if (value instanceof RegExp) {
|
|
63
63
|
return 'RegExp';
|
|
64
|
-
} else if (
|
|
64
|
+
} else if (value instanceof Date) {
|
|
65
65
|
return 'Date';
|
|
66
|
-
} else if (Array.isArray(
|
|
67
|
-
const typeString: string = `${this.getActualType(
|
|
68
|
-
if (
|
|
66
|
+
} else if (Array.isArray(value)) {
|
|
67
|
+
const typeString: string = `${this.getActualType(value[0])}[]`;
|
|
68
|
+
if (value.length === 2 && typeString === 'number[]') {
|
|
69
69
|
return 'Point';
|
|
70
70
|
} else {
|
|
71
71
|
return typeString;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
throw new Error(`Unknown type for ${
|
|
74
|
+
throw new Error(`Unknown type for ${value}`);
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/util/crud.ts
CHANGED
|
@@ -12,15 +12,15 @@ export class ModelQueryCrudUtil {
|
|
|
12
12
|
/**
|
|
13
13
|
* Delete all expired
|
|
14
14
|
*/
|
|
15
|
-
static async deleteExpired<T extends ModelType>(
|
|
15
|
+
static async deleteExpired<T extends ModelType>(service: ModelQueryCrudSupport & ModelCrudSupport, cls: Class<T>): Promise<number> {
|
|
16
16
|
const expiry = await ModelRegistryIndex.getExpiryFieldName(cls);
|
|
17
|
-
const
|
|
17
|
+
const count = await service.deleteByQuery<ModelType>(cls, {
|
|
18
18
|
where: {
|
|
19
19
|
[expiry]: {
|
|
20
20
|
$lt: new Date()
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
-
return
|
|
24
|
+
return count ?? 0;
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/util/query.ts
CHANGED
|
@@ -17,14 +17,14 @@ export class ModelQueryUtil {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolve comparator
|
|
20
|
-
* @param
|
|
20
|
+
* @param value
|
|
21
21
|
* @returns
|
|
22
22
|
*/
|
|
23
|
-
static resolveComparator(
|
|
24
|
-
if (typeof
|
|
25
|
-
return TimeUtil.fromNow(
|
|
23
|
+
static resolveComparator(value: unknown): unknown {
|
|
24
|
+
if (typeof value === 'string' && TimeUtil.isTimeSpan(value)) {
|
|
25
|
+
return TimeUtil.fromNow(value);
|
|
26
26
|
} else {
|
|
27
|
-
return
|
|
27
|
+
return value;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -41,9 +41,9 @@ export class ModelQueryUtil {
|
|
|
41
41
|
if (requestedId) {
|
|
42
42
|
throw new NotFoundError(cls, requestedId);
|
|
43
43
|
} else {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
throw
|
|
44
|
+
const error = new NotFoundError(cls, 'unknown');
|
|
45
|
+
error.message = 'No results found for query';
|
|
46
|
+
throw error;
|
|
47
47
|
}
|
|
48
48
|
} else {
|
|
49
49
|
throw new AppError(`Invalid number of results: ${result.length}`, { category: 'data' });
|
|
@@ -53,8 +53,8 @@ export class ModelQueryUtil {
|
|
|
53
53
|
/**
|
|
54
54
|
* Get a where clause with type
|
|
55
55
|
*/
|
|
56
|
-
static getWhereClause<T extends ModelType>(cls: Class<T>,
|
|
57
|
-
const clauses: WhereClauseRaw<T>[] = (
|
|
56
|
+
static getWhereClause<T extends ModelType>(cls: Class<T>, where: WhereClause<T> | undefined, checkExpiry = true): WhereClause<T> {
|
|
57
|
+
const clauses: WhereClauseRaw<T>[] = (where ? [where] : []);
|
|
58
58
|
|
|
59
59
|
const polymorphicConfig = SchemaRegistryIndex.getDiscriminatedConfig(cls);
|
|
60
60
|
if (polymorphicConfig) {
|
|
@@ -66,28 +66,28 @@ export class ModelQueryUtil {
|
|
|
66
66
|
));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
if (checkExpiry &&
|
|
69
|
+
const indexConfig = ModelRegistryIndex.getConfig(cls);
|
|
70
|
+
if (checkExpiry && indexConfig.expiresAt) {
|
|
71
71
|
clauses.push(castTo({
|
|
72
72
|
$or: [
|
|
73
|
-
{ [
|
|
74
|
-
{ [
|
|
75
|
-
{ [
|
|
73
|
+
{ [indexConfig.expiresAt]: { $exists: false } },
|
|
74
|
+
{ [indexConfig.expiresAt]: undefined },
|
|
75
|
+
{ [indexConfig.expiresAt]: { $gte: new Date() } },
|
|
76
76
|
]
|
|
77
77
|
}));
|
|
78
78
|
}
|
|
79
79
|
if (clauses.length > 1) {
|
|
80
|
-
|
|
80
|
+
where = { $and: clauses };
|
|
81
81
|
} else {
|
|
82
|
-
|
|
82
|
+
where = clauses[0];
|
|
83
83
|
}
|
|
84
|
-
return
|
|
84
|
+
return where!;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
static has$And = (
|
|
88
|
-
!!
|
|
89
|
-
static has$Or = (
|
|
90
|
-
!!
|
|
91
|
-
static has$Not = (
|
|
92
|
-
!!
|
|
87
|
+
static has$And = (value: unknown): value is ({ $and: WhereClause<unknown>[] }) =>
|
|
88
|
+
!!value && typeof value === 'object' && '$and' in value;
|
|
89
|
+
static has$Or = (value: unknown): value is ({ $or: WhereClause<unknown>[] }) =>
|
|
90
|
+
!!value && typeof value === 'object' && '$or' in value;
|
|
91
|
+
static has$Not = (value: unknown): value is ({ $not: WhereClause<unknown> }) =>
|
|
92
|
+
!!value && typeof value === 'object' && '$not' in value;
|
|
93
93
|
}
|
package/src/util/suggest.ts
CHANGED
|
@@ -75,18 +75,18 @@ export class ModelQuerySuggestUtil {
|
|
|
75
75
|
const pattern = this.getSuggestRegex(prefix);
|
|
76
76
|
|
|
77
77
|
const out: ([string, U] | readonly [string, U])[] = [];
|
|
78
|
-
for (const
|
|
79
|
-
const
|
|
80
|
-
if (Array.isArray(
|
|
81
|
-
out.push(...
|
|
82
|
-
} else if (typeof
|
|
83
|
-
out.push([
|
|
78
|
+
for (const result of results) {
|
|
79
|
+
const resultValue = result[field];
|
|
80
|
+
if (Array.isArray(resultValue)) {
|
|
81
|
+
out.push(...resultValue.filter(item => pattern.test(item)).map((item: string) => [item, transform(item, result)] as const));
|
|
82
|
+
} else if (typeof resultValue === 'string') {
|
|
83
|
+
out.push([resultValue, transform(resultValue, result)]);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
return out
|
|
87
87
|
.toSorted((a, b) => a[0].localeCompare(b[0]))
|
|
88
88
|
.map((a) => a[1])
|
|
89
|
-
.filter((
|
|
89
|
+
.filter((result, i, arr) => result !== arr[i - 1])
|
|
90
90
|
.slice(0, limit ?? 10);
|
|
91
91
|
}
|
|
92
92
|
|
package/src/verifier.ts
CHANGED
|
@@ -12,7 +12,7 @@ interface State {
|
|
|
12
12
|
path: string;
|
|
13
13
|
collect(element: string, message: string): void;
|
|
14
14
|
extend(path: string): State;
|
|
15
|
-
log(
|
|
15
|
+
log(error: string): void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface ProcessingHandler {
|
|
@@ -51,19 +51,19 @@ export class QueryVerifier {
|
|
|
51
51
|
/**
|
|
52
52
|
* Handle generic clauses
|
|
53
53
|
*/
|
|
54
|
-
static processGenericClause<T>(state: State, cls: Class<T>,
|
|
54
|
+
static processGenericClause<T>(state: State, cls: Class<T>, clause: object, handler: ProcessingHandler): void {
|
|
55
55
|
const view = SchemaRegistryIndex.getConfig(cls).fields;
|
|
56
56
|
|
|
57
|
-
if (
|
|
57
|
+
if (clause === undefined || clause === null) {
|
|
58
58
|
state.log('Value cannot be undefined or null');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (handler.preMember && handler.preMember(state,
|
|
62
|
+
if (handler.preMember && handler.preMember(state, clause)) {
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
for (const [key, value] of Object.entries(
|
|
66
|
+
for (const [key, value] of Object.entries(clause)) {
|
|
67
67
|
|
|
68
68
|
// Validate value is correct, and key is valid
|
|
69
69
|
if (value === undefined || value === null) {
|
|
@@ -82,19 +82,19 @@ export class QueryVerifier {
|
|
|
82
82
|
|
|
83
83
|
// Find field
|
|
84
84
|
const field = view[key];
|
|
85
|
-
const
|
|
85
|
+
const type = TypeUtil.getDeclaredType(field);
|
|
86
86
|
|
|
87
87
|
// If a simple operation
|
|
88
|
-
if (
|
|
89
|
-
handler.onSimpleType(state.extend(key),
|
|
88
|
+
if (type) {
|
|
89
|
+
handler.onSimpleType(state.extend(key), type, value, field.array ?? false);
|
|
90
90
|
} else {
|
|
91
91
|
// Otherwise recurse
|
|
92
92
|
const subCls = field.type;
|
|
93
|
-
const
|
|
94
|
-
if (handler.onComplexType && handler.onComplexType(state, subCls,
|
|
93
|
+
const subValue = value;
|
|
94
|
+
if (handler.onComplexType && handler.onComplexType(state, subCls, subValue, field.array ?? false)) {
|
|
95
95
|
continue;
|
|
96
96
|
}
|
|
97
|
-
this.processGenericClause(state.extend(key), subCls,
|
|
97
|
+
this.processGenericClause(state.extend(key), subCls, subValue, handler);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -113,8 +113,8 @@ export class QueryVerifier {
|
|
|
113
113
|
if (isArray) {
|
|
114
114
|
if (Array.isArray(value)) {
|
|
115
115
|
// Handle array literal
|
|
116
|
-
for (const
|
|
117
|
-
this.checkOperatorClause(state, declaredType,
|
|
116
|
+
for (const item of value) {
|
|
117
|
+
this.checkOperatorClause(state, declaredType, item, allowed, false);
|
|
118
118
|
}
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
@@ -141,30 +141,30 @@ export class QueryVerifier {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Should only be one?
|
|
144
|
-
for (const [
|
|
145
|
-
if (
|
|
146
|
-
if (!Array.isArray(
|
|
147
|
-
state.log(`${
|
|
144
|
+
for (const [key, keyValue] of Object.entries(value)) {
|
|
145
|
+
if (key === '$all' || key === '$elemMatch' || key === '$in' || key === '$nin') {
|
|
146
|
+
if (!Array.isArray(keyValue)) {
|
|
147
|
+
state.log(`${key} operator requires comparison to be an array, not ${typeof keyValue}`);
|
|
148
148
|
return;
|
|
149
|
-
} else if (
|
|
150
|
-
state.log(`${
|
|
149
|
+
} else if (keyValue.length === 0) {
|
|
150
|
+
state.log(`${key} operator requires comparison to be a non-empty array`);
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
for (const
|
|
155
|
-
const
|
|
156
|
-
if (!this.typesMatch(declaredType,
|
|
157
|
-
state.log(`${
|
|
154
|
+
for (const item of keyValue) {
|
|
155
|
+
const itemType = TypeUtil.getActualType(item);
|
|
156
|
+
if (!this.typesMatch(declaredType, itemType)) {
|
|
157
|
+
state.log(`${key} operator requires all values to be ${declaredType}, but ${itemType} was found`);
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
} else if (!(
|
|
162
|
-
state.log(`Operation ${
|
|
161
|
+
} else if (!(key in allowed)) {
|
|
162
|
+
state.log(`Operation ${key}, not allowed for field of type ${declaredType}`);
|
|
163
163
|
} else {
|
|
164
|
-
const actualSubType = TypeUtil.getActualType(
|
|
164
|
+
const actualSubType = TypeUtil.getActualType(keyValue)!;
|
|
165
165
|
|
|
166
|
-
if (!allowed[
|
|
167
|
-
state.log(`Passed in value ${actualSubType} mismatches with expected type(s) ${Array.from(allowed[
|
|
166
|
+
if (!allowed[key].has(actualSubType)) {
|
|
167
|
+
state.log(`Passed in value ${actualSubType} mismatches with expected type(s) ${Array.from(allowed[key])}`);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -190,8 +190,8 @@ export class QueryVerifier {
|
|
|
190
190
|
state.log(`${firstKey} requires the value to be an array`);
|
|
191
191
|
} else {
|
|
192
192
|
// Iterate
|
|
193
|
-
for (const
|
|
194
|
-
this.processWhereClause(state, cls,
|
|
193
|
+
for (const item of sub) {
|
|
194
|
+
this.processWhereClause(state, cls, item);
|
|
195
195
|
}
|
|
196
196
|
return true;
|
|
197
197
|
}
|
|
@@ -208,7 +208,7 @@ export class QueryVerifier {
|
|
|
208
208
|
onSimpleType: (state: State, type: SimpleType, value: unknown, isArray: boolean) => {
|
|
209
209
|
this.checkOperatorClause(state, type, value, TypeUtil.OPERATORS[type], isArray);
|
|
210
210
|
},
|
|
211
|
-
onComplexType: (state: State, subCls: Class<T>,
|
|
211
|
+
onComplexType: (state: State, subCls: Class<T>, subValue: T, isArray: boolean): boolean => false
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -281,8 +281,8 @@ export class QueryVerifier {
|
|
|
281
281
|
collect(path: string, message: string): void {
|
|
282
282
|
errors.push({ message: `${path}: ${message}`, path, kind: 'model' });
|
|
283
283
|
},
|
|
284
|
-
log(
|
|
285
|
-
this.collect(this.path,
|
|
284
|
+
log(error: string): void {
|
|
285
|
+
this.collect(this.path, error);
|
|
286
286
|
},
|
|
287
287
|
extend<S extends { path: string }>(this: S, sub: string): S {
|
|
288
288
|
return { ...this, path: !this.path ? sub : `${this.path}.${sub}` };
|
|
@@ -302,15 +302,15 @@ export class QueryVerifier {
|
|
|
302
302
|
continue;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
const
|
|
305
|
+
const value = query[key];
|
|
306
306
|
const subState = state.extend(key);
|
|
307
307
|
|
|
308
|
-
if (Array.isArray(
|
|
309
|
-
for (const
|
|
310
|
-
this[fn](subState, cls,
|
|
308
|
+
if (Array.isArray(value)) {
|
|
309
|
+
for (const item of value) {
|
|
310
|
+
this[fn](subState, cls, item);
|
|
311
311
|
}
|
|
312
|
-
} else if (typeof
|
|
313
|
-
this[fn](subState, cls,
|
|
312
|
+
} else if (typeof value !== 'string') {
|
|
313
|
+
this[fn](subState, cls, value);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
package/support/doc.support.tsx
CHANGED