@travetto/model 8.0.0-alpha.8 → 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 +91 -102
- package/__index__.ts +7 -11
- package/package.json +18 -18
- package/src/error/exists.ts +1 -1
- package/src/error/invalid-index.ts +5 -3
- package/src/error/invalid-sub-type.ts +1 -1
- package/src/error/not-found.ts +1 -1
- package/src/error/unique.ts +13 -0
- package/src/registry/decorator.ts +38 -32
- package/src/registry/registry-adapter.ts +11 -8
- package/src/registry/registry-index.ts +12 -33
- package/src/registry/types.ts +24 -39
- package/src/types/basic.ts +3 -3
- package/src/types/blob.ts +6 -6
- package/src/types/bulk.ts +5 -9
- package/src/types/crud.ts +9 -6
- package/src/types/expiry.ts +2 -2
- package/src/types/model.ts +11 -2
- package/src/types/storage.ts +2 -3
- package/src/util/blob.ts +2 -2
- package/src/util/bulk.ts +6 -3
- package/src/util/crud.ts +105 -22
- package/src/util/expiry.ts +4 -5
- package/src/util/storage.ts +3 -4
- package/support/base-command.ts +3 -5
- package/support/bin/candidate.ts +8 -11
- package/support/bin/export.ts +2 -2
- package/support/bin/install.ts +2 -2
- package/support/cli.model_export.ts +9 -5
- package/support/cli.model_install.ts +9 -5
- package/support/doc.support.tsx +16 -20
- package/support/test/base.ts +14 -12
- package/support/test/basic.ts +44 -3
- package/support/test/blob.ts +8 -9
- package/support/test/bulk.ts +16 -5
- package/support/test/crud.ts +134 -82
- package/support/test/expiry.ts +39 -21
- package/support/test/polymorphism.ts +43 -143
- package/support/test/suite.ts +15 -12
- package/src/types/indexed.ts +0 -43
- package/src/util/indexed.ts +0 -144
- package/support/test/indexed.ts +0 -190
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { Model, type ModelCrudSupport, NotFoundError, PersistValue, SubTypeNotSupportedError } from '@travetto/model';
|
|
5
5
|
import { castTo } from '@travetto/runtime';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
type ModelIndexedSupport, Index, type ModelCrudSupport, Model,
|
|
9
|
-
NotFoundError, SubTypeNotSupportedError, PersistValue
|
|
10
|
-
} from '@travetto/model';
|
|
6
|
+
import { DiscriminatorField, Schema, Text, TypeMismatchError } from '@travetto/schema';
|
|
7
|
+
import { Suite, Test } from '@travetto/test';
|
|
11
8
|
|
|
12
|
-
import { ModelIndexedUtil } from '../../src/util/indexed.ts';
|
|
13
9
|
import { ExistsError } from '../../src/error/exists.ts';
|
|
14
|
-
|
|
15
10
|
import { BaseModelSuite } from './base.ts';
|
|
16
11
|
|
|
17
12
|
@Schema()
|
|
@@ -43,45 +38,8 @@ export class Engineer extends Worker {
|
|
|
43
38
|
major: string;
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
@Model()
|
|
47
|
-
@Index({
|
|
48
|
-
name: 'worker-name',
|
|
49
|
-
type: 'sorted',
|
|
50
|
-
fields: [{ name: 1 }, { age: 1 }]
|
|
51
|
-
})
|
|
52
|
-
@Discriminated('type')
|
|
53
|
-
export class IndexedWorker {
|
|
54
|
-
id: string;
|
|
55
|
-
type: string;
|
|
56
|
-
name: string;
|
|
57
|
-
age?: number;
|
|
58
|
-
}
|
|
59
|
-
@Model()
|
|
60
|
-
export class IndexedDoctor extends IndexedWorker {
|
|
61
|
-
specialty: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@Model()
|
|
65
|
-
export class IndexedFirefighter extends IndexedWorker {
|
|
66
|
-
firehouse: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@Model()
|
|
70
|
-
export class IndexedEngineer extends IndexedWorker {
|
|
71
|
-
major: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
75
|
-
const out: T[] = [];
|
|
76
|
-
for await (const el of iterable) {
|
|
77
|
-
out.push(el);
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
41
|
@Suite()
|
|
83
42
|
export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
84
|
-
|
|
85
43
|
@Test('Polymorphic create and find')
|
|
86
44
|
async polymorphicCreateAndFind() {
|
|
87
45
|
const service = await this.service;
|
|
@@ -95,9 +53,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
95
53
|
assert(doc instanceof Doctor);
|
|
96
54
|
assert(doc.updatedDate !== undefined);
|
|
97
55
|
|
|
98
|
-
await assert.rejects(
|
|
99
|
-
() => service.get(Engineer, doc.id),
|
|
100
|
-
NotFoundError);
|
|
56
|
+
await assert.rejects(() => service.get(Engineer, doc.id), NotFoundError);
|
|
101
57
|
|
|
102
58
|
assert(doc instanceof Doctor);
|
|
103
59
|
assert(fire instanceof Firefighter);
|
|
@@ -108,7 +64,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
108
64
|
const fire2 = await service.get(Worker, fire.id);
|
|
109
65
|
assert(fire2 instanceof Firefighter);
|
|
110
66
|
|
|
111
|
-
const all = await
|
|
67
|
+
const all = await this.toArray(service.list(Worker));
|
|
112
68
|
assert(all.length === 3);
|
|
113
69
|
|
|
114
70
|
const doc3 = all.find(x => x instanceof Doctor);
|
|
@@ -126,18 +82,21 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
126
82
|
assert(eng3.major === 'oranges');
|
|
127
83
|
assert(eng3.name === 'cob');
|
|
128
84
|
|
|
129
|
-
const engineers = await
|
|
85
|
+
const engineers = await this.toArray(service.list(Engineer));
|
|
130
86
|
assert(engineers.length === 1);
|
|
131
87
|
|
|
132
|
-
await service.create(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
88
|
+
await service.create(
|
|
89
|
+
Engineer,
|
|
90
|
+
Engineer.from({
|
|
91
|
+
major: 'foodService',
|
|
92
|
+
name: 'bob2'
|
|
93
|
+
})
|
|
94
|
+
);
|
|
136
95
|
|
|
137
|
-
const all2 = await
|
|
96
|
+
const all2 = await this.toArray(service.list(Worker));
|
|
138
97
|
assert(all2.length === 4);
|
|
139
98
|
|
|
140
|
-
const engineers2 = await
|
|
99
|
+
const engineers2 = await this.toArray(service.list(Engineer));
|
|
141
100
|
assert(engineers2.length === 2);
|
|
142
101
|
}
|
|
143
102
|
|
|
@@ -152,16 +111,21 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
152
111
|
|
|
153
112
|
await this.saveAll(Worker, [doc, fire, eng]);
|
|
154
113
|
|
|
155
|
-
assert(await service.get(Worker, doc.id) instanceof Doctor);
|
|
156
|
-
assert(await service.get(Worker, fire.id) instanceof Firefighter);
|
|
114
|
+
assert((await service.get(Worker, doc.id)) instanceof Doctor);
|
|
115
|
+
assert((await service.get(Worker, fire.id)) instanceof Firefighter);
|
|
157
116
|
|
|
158
117
|
const update = new Date();
|
|
159
118
|
|
|
160
119
|
await assert.rejects(
|
|
161
120
|
() =>
|
|
162
|
-
service.upsert(
|
|
163
|
-
|
|
164
|
-
|
|
121
|
+
service.upsert(
|
|
122
|
+
Doctor,
|
|
123
|
+
Doctor.from({
|
|
124
|
+
id: fire.id,
|
|
125
|
+
name: 'gob',
|
|
126
|
+
specialty: 'eyes'
|
|
127
|
+
})
|
|
128
|
+
),
|
|
165
129
|
e => e instanceof SubTypeNotSupportedError || e instanceof ExistsError
|
|
166
130
|
);
|
|
167
131
|
|
|
@@ -172,18 +136,28 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
172
136
|
await timers.setTimeout(15);
|
|
173
137
|
|
|
174
138
|
try {
|
|
175
|
-
const result = await service.upsert(
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
const result = await service.upsert(
|
|
140
|
+
Doctor,
|
|
141
|
+
Doctor.from({
|
|
142
|
+
id: doc.id,
|
|
143
|
+
name: 'gob',
|
|
144
|
+
specialty: 'eyes'
|
|
145
|
+
})
|
|
146
|
+
);
|
|
178
147
|
|
|
179
148
|
assert(result.updatedDate!.getTime() > update.getTime());
|
|
180
149
|
} catch (err) {
|
|
181
150
|
assert(err instanceof SubTypeNotSupportedError);
|
|
182
151
|
}
|
|
183
152
|
|
|
184
|
-
const resAlt = await service.upsert(
|
|
185
|
-
|
|
186
|
-
|
|
153
|
+
const resAlt = await service.upsert(
|
|
154
|
+
Worker,
|
|
155
|
+
Doctor.from({
|
|
156
|
+
id: doc.id,
|
|
157
|
+
name: 'gob',
|
|
158
|
+
specialty: 'eyes'
|
|
159
|
+
})
|
|
160
|
+
);
|
|
187
161
|
|
|
188
162
|
assert(resAlt.updatedDate!.getTime() > update.getTime());
|
|
189
163
|
|
|
@@ -196,10 +170,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
196
170
|
// Delete by base class
|
|
197
171
|
await service.delete(Worker, fire.id);
|
|
198
172
|
|
|
199
|
-
await assert.rejects(
|
|
200
|
-
() => service.delete(Worker, fire.id),
|
|
201
|
-
NotFoundError
|
|
202
|
-
);
|
|
173
|
+
await assert.rejects(() => service.delete(Worker, fire.id), NotFoundError);
|
|
203
174
|
|
|
204
175
|
// Delete by any subtype when id is missing
|
|
205
176
|
await assert.rejects(
|
|
@@ -207,75 +178,4 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
207
178
|
e => e instanceof SubTypeNotSupportedError || e instanceof NotFoundError
|
|
208
179
|
);
|
|
209
180
|
}
|
|
210
|
-
|
|
211
|
-
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
212
|
-
async polymorphicIndexGet() {
|
|
213
|
-
const service: ModelIndexedSupport = castTo(await this.service);
|
|
214
|
-
const now = 30;
|
|
215
|
-
const [doc, fire, eng] = [
|
|
216
|
-
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
217
|
-
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
218
|
-
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
219
|
-
];
|
|
220
|
-
|
|
221
|
-
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
222
|
-
|
|
223
|
-
const result = await service.getByIndex(IndexedWorker, 'worker-name', {
|
|
224
|
-
age: now,
|
|
225
|
-
name: 'rob'
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
assert(result instanceof IndexedFirefighter);
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const res2 = await service.getByIndex(IndexedFirefighter, 'worker-name', {
|
|
232
|
-
age: now,
|
|
233
|
-
name: 'rob'
|
|
234
|
-
});
|
|
235
|
-
assert(res2 instanceof IndexedFirefighter); // If service allows for get by subtype
|
|
236
|
-
} catch (err) {
|
|
237
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError); // If it does not
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
242
|
-
async polymorphicIndexDelete() {
|
|
243
|
-
const service: ModelIndexedSupport = castTo(await this.service);
|
|
244
|
-
const now = 30;
|
|
245
|
-
const [doc, fire, eng] = [
|
|
246
|
-
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
247
|
-
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
248
|
-
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
252
|
-
|
|
253
|
-
assert(await this.getSize(IndexedWorker) === 3);
|
|
254
|
-
|
|
255
|
-
await service.deleteByIndex(IndexedWorker, 'worker-name', {
|
|
256
|
-
age: now,
|
|
257
|
-
name: 'bob'
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
assert(await this.getSize(IndexedWorker) === 2);
|
|
261
|
-
assert(await this.getSize(IndexedDoctor) === 0);
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
await service.deleteByIndex(IndexedFirefighter, 'worker-name', {
|
|
265
|
-
age: now,
|
|
266
|
-
name: 'rob'
|
|
267
|
-
});
|
|
268
|
-
} catch (err) {
|
|
269
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
await service.deleteByIndex(IndexedEngineer, 'worker-name', {
|
|
274
|
-
age: now,
|
|
275
|
-
name: 'bob'
|
|
276
|
-
});
|
|
277
|
-
} catch (err) {
|
|
278
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
181
|
+
}
|
package/support/test/suite.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { type Class } from '@travetto/runtime';
|
|
2
1
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
3
2
|
import { Registry } from '@travetto/registry';
|
|
4
|
-
import {
|
|
3
|
+
import type { Class } from '@travetto/runtime';
|
|
5
4
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
5
|
+
import { type SuitePhaseHandler, SuiteRegistryIndex, TestFixtures } from '@travetto/test';
|
|
6
6
|
|
|
7
|
+
import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
7
8
|
import { ModelBlobUtil } from '../../src/util/blob.ts';
|
|
8
9
|
import { ModelStorageUtil } from '../../src/util/storage.ts';
|
|
9
|
-
import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
10
10
|
|
|
11
|
-
type ConfigType = { autoCreate?: boolean
|
|
11
|
+
type ConfigType = { autoCreate?: boolean; namespace?: string };
|
|
12
12
|
|
|
13
|
-
class ModelSuiteHandler<T extends { configClass: Class<ConfigType
|
|
13
|
+
class ModelSuiteHandler<T extends { configClass: Class<ConfigType>; serviceClass: Class }> implements SuitePhaseHandler<T> {
|
|
14
14
|
qualifier?: symbol;
|
|
15
15
|
target: Class<T>;
|
|
16
16
|
constructor(target: Class<T>, qualifier?: symbol) {
|
|
@@ -35,9 +35,11 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
|
|
|
35
35
|
if (ModelStorageUtil.isSupported(service)) {
|
|
36
36
|
await service.createStorage();
|
|
37
37
|
if (service.upsertModel) {
|
|
38
|
-
await Promise.all(
|
|
39
|
-
.
|
|
40
|
-
|
|
38
|
+
await Promise.all(
|
|
39
|
+
ModelRegistryIndex.getClasses()
|
|
40
|
+
.filter(cls => cls === SchemaRegistryIndex.getBaseClass(cls))
|
|
41
|
+
.map(modelCls => service.upsertModel!(modelCls))
|
|
42
|
+
);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -45,8 +47,7 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
|
|
|
45
47
|
async afterEach(instance: T) {
|
|
46
48
|
const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
|
|
47
49
|
if (ModelStorageUtil.isSupported(service)) {
|
|
48
|
-
const models = ModelRegistryIndex.getClasses()
|
|
49
|
-
.filter(model => model === SchemaRegistryIndex.getBaseClass(model));
|
|
50
|
+
const models = ModelRegistryIndex.getClasses().filter(model => model === SchemaRegistryIndex.getBaseClass(model));
|
|
50
51
|
|
|
51
52
|
if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
|
|
52
53
|
await service.truncateBlob();
|
|
@@ -83,7 +84,9 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
|
|
|
83
84
|
* @example opt-in
|
|
84
85
|
* @kind decorator
|
|
85
86
|
*/
|
|
86
|
-
export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
87
|
+
export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean; namespace?: string }>; serviceClass: Class }>(
|
|
88
|
+
qualifier?: symbol
|
|
89
|
+
) {
|
|
87
90
|
const fixtures = new TestFixtures(['@travetto/model']);
|
|
88
91
|
return (target: Class<T>): void => {
|
|
89
92
|
target.prototype.fixtures = fixtures;
|
|
@@ -91,4 +94,4 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
|
91
94
|
phaseHandlers: [new ModelSuiteHandler(target, qualifier)]
|
|
92
95
|
});
|
|
93
96
|
};
|
|
94
|
-
}
|
|
97
|
+
}
|
package/src/types/indexed.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Class, DeepPartial } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
4
|
-
import type { ModelBasicSupport } from './basic.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Support for simple indexed activity
|
|
8
|
-
*
|
|
9
|
-
* @concrete
|
|
10
|
-
*/
|
|
11
|
-
export interface ModelIndexedSupport extends ModelBasicSupport {
|
|
12
|
-
/**
|
|
13
|
-
* Get entity by index as defined by fields of idx and the body fields
|
|
14
|
-
* @param cls The type to search by
|
|
15
|
-
* @param idx The index name to search against
|
|
16
|
-
* @param body The payload of fields needed to search
|
|
17
|
-
*/
|
|
18
|
-
getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Delete entity by index as defined by fields of idx and the body fields
|
|
22
|
-
* @param cls The type to search by
|
|
23
|
-
* @param idx The index name to search against
|
|
24
|
-
* @param body The payload of fields needed to search
|
|
25
|
-
*/
|
|
26
|
-
deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* List entity by ranged index as defined by fields of idx and the body fields
|
|
30
|
-
* @param cls The type to search by
|
|
31
|
-
* @param idx The index name to search against
|
|
32
|
-
* @param body The payload of fields needed to search
|
|
33
|
-
*/
|
|
34
|
-
listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Upsert by index, allowing the index to act as a primary key
|
|
38
|
-
* @param cls The type to create for
|
|
39
|
-
* @param idx The index name to use
|
|
40
|
-
* @param body The document to potentially store
|
|
41
|
-
*/
|
|
42
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
|
|
43
|
-
}
|
package/src/util/indexed.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { castTo, type Class, type DeepPartial, hasFunction, TypedObject } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import { IndexNotSupported } from '../error/invalid-index.ts';
|
|
4
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
5
|
-
import type { IndexConfig } from '../registry/types.ts';
|
|
6
|
-
import type { ModelCrudSupport } from '../types/crud.ts';
|
|
7
|
-
import type { ModelIndexedSupport } from '../types/indexed.ts';
|
|
8
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
9
|
-
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
10
|
-
|
|
11
|
-
type ComputeConfig = {
|
|
12
|
-
includeSortInFields?: boolean;
|
|
13
|
-
emptyValue?: unknown;
|
|
14
|
-
emptySortValue?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
|
|
18
|
-
type IndexSortPart = { path: string[], dir: number, value: number | Date };
|
|
19
|
-
|
|
20
|
-
const DEFAULT_SEP = '\u8203';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Utils for working with indexed model services
|
|
24
|
-
*/
|
|
25
|
-
export class ModelIndexedUtil {
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Type guard for determining if service supports indexed operation
|
|
29
|
-
*/
|
|
30
|
-
static isSupported = hasFunction<ModelIndexedSupport>('getByIndex');
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Compute flattened field to value mappings
|
|
34
|
-
* @param cls Class to get info for
|
|
35
|
-
* @param idx Index config
|
|
36
|
-
* @param item Item to read values from
|
|
37
|
-
*/
|
|
38
|
-
static computeIndexParts<T extends ModelType>(
|
|
39
|
-
cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
|
|
40
|
-
): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
|
|
41
|
-
const config = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
42
|
-
const sortField = config.type === 'sorted' ? config.fields.at(-1) : undefined;
|
|
43
|
-
|
|
44
|
-
const fields: IndexFieldPart[] = [];
|
|
45
|
-
let sortDirection: number = 0;
|
|
46
|
-
let sorted: IndexSortPart | undefined;
|
|
47
|
-
|
|
48
|
-
for (const field of config.fields) {
|
|
49
|
-
let fieldRef: Record<string, unknown> = field;
|
|
50
|
-
let itemRef: Record<string, unknown> = item;
|
|
51
|
-
const parts = [];
|
|
52
|
-
|
|
53
|
-
while (itemRef !== undefined && itemRef !== null) {
|
|
54
|
-
const key = TypedObject.keys(fieldRef)[0];
|
|
55
|
-
itemRef = castTo(itemRef[key]);
|
|
56
|
-
parts.push(key);
|
|
57
|
-
if (typeof fieldRef[key] === 'boolean' || typeof fieldRef[key] === 'number') {
|
|
58
|
-
if (config.type === 'sorted') {
|
|
59
|
-
sortDirection = fieldRef[key] === true ? 1 : fieldRef[key] === false ? 0 : fieldRef[key];
|
|
60
|
-
}
|
|
61
|
-
break; // At the bottom
|
|
62
|
-
} else {
|
|
63
|
-
fieldRef = castTo(fieldRef[key]);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (field === sortField) {
|
|
67
|
-
sorted = { path: parts, dir: sortDirection, value: castTo(itemRef) };
|
|
68
|
-
}
|
|
69
|
-
if (itemRef === undefined || itemRef === null) {
|
|
70
|
-
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
71
|
-
if (empty === undefined || empty === Error) {
|
|
72
|
-
throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
76
|
-
fields.push({ path: parts, value: castTo(itemRef) });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { fields, sorted };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Project item via index
|
|
86
|
-
* @param cls Type to get index for
|
|
87
|
-
* @param idx Index config
|
|
88
|
-
*/
|
|
89
|
-
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, config?: ComputeConfig): Record<string, unknown> {
|
|
90
|
-
const response: Record<string, unknown> = {};
|
|
91
|
-
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, config).fields) {
|
|
92
|
-
let sub: Record<string, unknown> = response;
|
|
93
|
-
const all = path.slice(0);
|
|
94
|
-
const last = all.pop()!;
|
|
95
|
-
for (const part of all) {
|
|
96
|
-
sub = castTo(sub[part] ??= {});
|
|
97
|
-
}
|
|
98
|
-
sub[last] = value;
|
|
99
|
-
}
|
|
100
|
-
return response;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Compute index key as a single value
|
|
105
|
-
* @param cls Class to get index for
|
|
106
|
-
* @param idx Index config
|
|
107
|
-
* @param item item to process
|
|
108
|
-
*/
|
|
109
|
-
static computeIndexKey<T extends ModelType>(
|
|
110
|
-
cls: Class<T>,
|
|
111
|
-
idx: IndexConfig<T> | string,
|
|
112
|
-
item: DeepPartial<T> = {},
|
|
113
|
-
config?: ComputeConfig & { separator?: string }
|
|
114
|
-
): { type: string, key: string, sort?: number | Date } {
|
|
115
|
-
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(config ?? {}), includeSortInFields: false });
|
|
116
|
-
const key = fields.map(({ value }) => value).map(value => `${value}`).join(config?.separator ?? DEFAULT_SEP);
|
|
117
|
-
const indexConfig = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
118
|
-
return !sorted ? { type: indexConfig.type, key } : { type: indexConfig.type, key, sort: sorted.value };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Naive upsert by index
|
|
123
|
-
* @param service
|
|
124
|
-
* @param cls
|
|
125
|
-
* @param idx
|
|
126
|
-
* @param body
|
|
127
|
-
*/
|
|
128
|
-
static async naiveUpsert<T extends ModelType>(
|
|
129
|
-
service: ModelIndexedSupport & ModelCrudSupport,
|
|
130
|
-
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
131
|
-
): Promise<T> {
|
|
132
|
-
try {
|
|
133
|
-
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
134
|
-
body.id = id;
|
|
135
|
-
return await service.update(cls, castTo(body));
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (error instanceof NotFoundError) {
|
|
138
|
-
return await service.create(cls, body);
|
|
139
|
-
} else {
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|