@travetto/model 8.0.0-alpha.2 → 8.0.0-alpha.21
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 +40 -56
- package/__index__.ts +0 -2
- package/package.json +7 -7
- package/src/error/invalid-index.ts +2 -2
- package/src/registry/decorator.ts +15 -15
- package/src/registry/registry-adapter.ts +6 -3
- package/src/registry/registry-index.ts +5 -27
- package/src/registry/types.ts +23 -38
- package/src/types/basic.ts +1 -1
- package/src/types/blob.ts +4 -3
- package/src/types/bulk.ts +1 -1
- package/src/types/crud.ts +8 -3
- package/src/types/expiry.ts +1 -1
- package/src/types/model.ts +10 -1
- package/src/types/storage.ts +1 -1
- package/src/util/crud.ts +81 -8
- package/support/cli.model_export.ts +4 -1
- package/support/cli.model_install.ts +4 -1
- package/support/doc.support.tsx +0 -2
- package/support/test/base.ts +5 -5
- package/support/test/basic.ts +40 -1
- package/support/test/blob.ts +1 -1
- package/support/test/crud.ts +39 -5
- package/support/test/polymorphism.ts +6 -114
- package/src/types/indexed.ts +0 -43
- package/src/util/indexed.ts +0 -144
- package/support/test/indexed.ts +0 -190
package/src/util/crud.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { castTo, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
|
|
1
|
+
import { castTo, castKey, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
|
|
2
2
|
import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
@@ -34,6 +34,21 @@ export class ModelCrudUtil {
|
|
|
34
34
|
return { create, valid };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
static async filterOutNotFound<T extends ModelType>(actions: Promise<T>[] | undefined): Promise<T[]> {
|
|
38
|
+
if (!actions) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return (await Promise.allSettled(actions)).map(p => {
|
|
42
|
+
if (p.status === 'fulfilled') {
|
|
43
|
+
return p.value;
|
|
44
|
+
} else if (p.reason instanceof NotFoundError) {
|
|
45
|
+
return undefined!;
|
|
46
|
+
} else {
|
|
47
|
+
throw p.reason;
|
|
48
|
+
}
|
|
49
|
+
}).filter(item => !!item);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* Load model
|
|
39
54
|
* @param cls Class to load model for
|
|
@@ -96,6 +111,70 @@ export class ModelCrudUtil {
|
|
|
96
111
|
return castTo(item);
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Recursively deletes or masks transient fields from an object based on its Model config.
|
|
116
|
+
* If transient fields are present, returns a copy with those fields set to undefined.
|
|
117
|
+
*/
|
|
118
|
+
static cleanTransientFields<T>(cls: Class<T>, item: T): T {
|
|
119
|
+
if (!item || typeof item !== 'object') {
|
|
120
|
+
return item;
|
|
121
|
+
}
|
|
122
|
+
if (SchemaRegistryIndex.has(cls)) {
|
|
123
|
+
const schema = SchemaRegistryIndex.get(cls).get();
|
|
124
|
+
const transientFields = ModelRegistryIndex.has(cls) ? ModelRegistryIndex.getConfig(cls).transientFields : undefined;
|
|
125
|
+
|
|
126
|
+
const isCleanTarget = (key: string): boolean => {
|
|
127
|
+
return transientFields?.includes(key) ?? false;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const hasTargets = Object.keys(schema.fields).some(key => isCleanTarget(key));
|
|
131
|
+
|
|
132
|
+
let res = item;
|
|
133
|
+
if (hasTargets) {
|
|
134
|
+
res = { ...item };
|
|
135
|
+
for (const key of Object.keys(schema.fields)) {
|
|
136
|
+
if (isCleanTarget(key)) {
|
|
137
|
+
res[castKey<T>(key)] = undefined!;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
143
|
+
const fieldKey = castKey<typeof res>(key);
|
|
144
|
+
if (!isCleanTarget(key) && res[fieldKey] !== undefined && res[fieldKey] !== null) {
|
|
145
|
+
if (field.array && Array.isArray(res[fieldKey])) {
|
|
146
|
+
const arr = res[fieldKey] as any[];
|
|
147
|
+
let changed = false;
|
|
148
|
+
const newArr = arr.map(subItem => {
|
|
149
|
+
const cleaned = this.cleanTransientFields(field.type, subItem);
|
|
150
|
+
if (cleaned !== subItem) {
|
|
151
|
+
changed = true;
|
|
152
|
+
}
|
|
153
|
+
return cleaned;
|
|
154
|
+
});
|
|
155
|
+
if (changed) {
|
|
156
|
+
if (res === item) {
|
|
157
|
+
res = { ...item };
|
|
158
|
+
}
|
|
159
|
+
res[fieldKey] = newArr as any;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
const val = res[fieldKey];
|
|
163
|
+
const cleaned = this.cleanTransientFields(field.type, val);
|
|
164
|
+
if (cleaned !== val) {
|
|
165
|
+
if (res === item) {
|
|
166
|
+
res = { ...item };
|
|
167
|
+
}
|
|
168
|
+
res[fieldKey] = cleaned;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return res;
|
|
174
|
+
}
|
|
175
|
+
return item;
|
|
176
|
+
}
|
|
177
|
+
|
|
99
178
|
/**
|
|
100
179
|
* Ensure subtype is not supported
|
|
101
180
|
*/
|
|
@@ -117,10 +196,7 @@ export class ModelCrudUtil {
|
|
|
117
196
|
item = await handler(item) ?? item;
|
|
118
197
|
}
|
|
119
198
|
}
|
|
120
|
-
|
|
121
|
-
item = await item.prePersist() ?? item;
|
|
122
|
-
}
|
|
123
|
-
return item;
|
|
199
|
+
return this.cleanTransientFields(cls, item);
|
|
124
200
|
}
|
|
125
201
|
|
|
126
202
|
/**
|
|
@@ -131,9 +207,6 @@ export class ModelCrudUtil {
|
|
|
131
207
|
for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
|
|
132
208
|
item = await handler(item) ?? item;
|
|
133
209
|
}
|
|
134
|
-
if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
|
|
135
|
-
item = await item.postLoad() ?? item;
|
|
136
|
-
}
|
|
137
210
|
return item;
|
|
138
211
|
}
|
|
139
212
|
|
|
@@ -6,7 +6,10 @@ import { ModelExportUtil } from './bin/export.ts';
|
|
|
6
6
|
import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Export model definitions for a selected provider and model set.
|
|
10
|
+
*
|
|
11
|
+
* The command resolves candidate models and delegates to provider-specific
|
|
12
|
+
* export logic to produce schema/install artifacts.
|
|
10
13
|
*/
|
|
11
14
|
@CliCommand()
|
|
12
15
|
export class ModelExportCommand extends BaseModelCommand {
|
|
@@ -6,7 +6,10 @@ import { ModelInstallUtil } from './bin/install.ts';
|
|
|
6
6
|
import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Install or update model definitions for a selected provider.
|
|
10
|
+
*
|
|
11
|
+
* The command resolves candidate models and applies provider install/upsert
|
|
12
|
+
* operations so backing stores are prepared for runtime usage.
|
|
10
13
|
*/
|
|
11
14
|
@CliCommand()
|
|
12
15
|
export class ModelInstallCommand extends BaseModelCommand {
|
package/support/doc.support.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import type { ModelBlobSupport } from '../src/types/blob.ts';
|
|
|
8
8
|
import type { ModelBulkSupport } from '../src/types/bulk.ts';
|
|
9
9
|
import type { ModelCrudSupport } from '../src/types/crud.ts';
|
|
10
10
|
import type { ModelExpirySupport } from '../src/types/expiry.ts';
|
|
11
|
-
import type { ModelIndexedSupport } from '../src/types/indexed.ts';
|
|
12
11
|
|
|
13
12
|
const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'> =>
|
|
14
13
|
d.codeLink(title, Runtime.getSourceFile(target), new RegExp(`\\binterface\\s+${target.name}`));
|
|
@@ -17,7 +16,6 @@ export const Links = {
|
|
|
17
16
|
Basic: toLink('Basic', toConcrete<ModelBasicSupport>()),
|
|
18
17
|
Crud: toLink('CRUD', toConcrete<ModelCrudSupport>()),
|
|
19
18
|
Expiry: toLink('Expiry', toConcrete<ModelExpirySupport>()),
|
|
20
|
-
Indexed: toLink('Indexed', toConcrete<ModelIndexedSupport>()),
|
|
21
19
|
Bulk: toLink('Bulk', toConcrete<ModelBulkSupport>()),
|
|
22
20
|
Blob: toLink('Blob', toConcrete<ModelBlobSupport>()),
|
|
23
21
|
};
|
package/support/test/base.ts
CHANGED
|
@@ -22,8 +22,8 @@ export abstract class BaseModelSuite<T> {
|
|
|
22
22
|
const svc = (await this.service);
|
|
23
23
|
if (ModelCrudUtil.isSupported(svc)) {
|
|
24
24
|
let i = 0;
|
|
25
|
-
for await (const
|
|
26
|
-
i +=
|
|
25
|
+
for await (const batch of svc.list(cls)) {
|
|
26
|
+
i += batch.length;
|
|
27
27
|
}
|
|
28
28
|
return i;
|
|
29
29
|
} else {
|
|
@@ -52,11 +52,11 @@ export abstract class BaseModelSuite<T> {
|
|
|
52
52
|
return DependencyRegistryIndex.getInstance(this.serviceClass);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
56
|
-
const out: U[] = [];
|
|
55
|
+
async toArray<U>(src: AsyncIterable<U | U[]> | AsyncGenerator<U | U[]>): Promise<U[]> {
|
|
56
|
+
const out: (U | U[])[] = [];
|
|
57
57
|
for await (const el of src) {
|
|
58
58
|
out.push(el);
|
|
59
59
|
}
|
|
60
|
-
return out;
|
|
60
|
+
return castTo(out.flat());
|
|
61
61
|
}
|
|
62
62
|
}
|
package/support/test/basic.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Suite, Test } from '@travetto/test';
|
|
4
|
-
import { type ModelCrudSupport, Model, NotFoundError } from '@travetto/model';
|
|
4
|
+
import { type ModelCrudSupport, Model, NotFoundError, ModelCrudUtil, TransientField } from '@travetto/model';
|
|
5
5
|
|
|
6
6
|
import { BaseModelSuite } from './base.ts';
|
|
7
7
|
|
|
8
|
+
@Model('computed_person')
|
|
9
|
+
class ComputedPerson {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
@TransientField()
|
|
13
|
+
get nameUpper(): string {
|
|
14
|
+
return this.name.toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
@TransientField()
|
|
17
|
+
ignoredField?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
@Model('basic_person')
|
|
9
21
|
class Person {
|
|
10
22
|
id: string;
|
|
@@ -59,4 +71,31 @@ export abstract class ModelBasicSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
59
71
|
assert(single !== undefined);
|
|
60
72
|
assert(single.age === 25);
|
|
61
73
|
}
|
|
74
|
+
|
|
75
|
+
@Test('Should not persist computed properties')
|
|
76
|
+
async testComputed() {
|
|
77
|
+
const service = await this.service;
|
|
78
|
+
const id = service.idSource.create();
|
|
79
|
+
await service.create(ComputedPerson, ComputedPerson.from({
|
|
80
|
+
id,
|
|
81
|
+
name: 'Bob',
|
|
82
|
+
ignoredField: 'secret'
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const retrieved = await service.get(ComputedPerson, id);
|
|
86
|
+
assert(retrieved.nameUpper === 'BOB');
|
|
87
|
+
|
|
88
|
+
// Verify it wasn't saved in the database holistically:
|
|
89
|
+
// When we fetch the document, the database driver retrieves a raw object and maps it.
|
|
90
|
+
// If the database stored 'nameUpper', trying to map it would set it on the retrieved instance.
|
|
91
|
+
// Since nameUpper is a getter-only property on the instance, we can verify that the persistence
|
|
92
|
+
// preparation (prePersist) recursively stripped the getter property from the stored object.
|
|
93
|
+
const instance = ComputedPerson.from({ id, name: 'Bob', ignoredField: 'secret' });
|
|
94
|
+
assert(Object.hasOwn(instance, 'nameUpper'));
|
|
95
|
+
assert(instance.ignoredField === 'secret');
|
|
96
|
+
|
|
97
|
+
const prepared = await ModelCrudUtil.prePersist(ComputedPerson, instance, 'all');
|
|
98
|
+
assert(prepared.nameUpper === undefined);
|
|
99
|
+
assert(prepared.ignoredField === undefined);
|
|
100
|
+
}
|
|
62
101
|
}
|
package/support/test/blob.ts
CHANGED
|
@@ -92,7 +92,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
92
92
|
const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
|
|
93
93
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
94
94
|
|
|
95
|
-
const og = await this.fixture.
|
|
95
|
+
const og = await this.fixture.readUTF8('/text.txt');
|
|
96
96
|
|
|
97
97
|
assert(subContent === og.substring(10, 21));
|
|
98
98
|
|
package/support/test/crud.ts
CHANGED
|
@@ -3,7 +3,7 @@ import timers from 'node:timers/promises';
|
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
5
|
import { Schema, Text, Precision, Required, } from '@travetto/schema';
|
|
6
|
-
import { type ModelCrudSupport, Model, NotFoundError, PersistValue } from '@travetto/model';
|
|
6
|
+
import { type ModelCrudSupport, Model, NotFoundError, PersistValue, PrePersist } from '@travetto/model';
|
|
7
7
|
|
|
8
8
|
import { BaseModelSuite } from './base.ts';
|
|
9
9
|
|
|
@@ -42,14 +42,13 @@ class SimpleList {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
@Model()
|
|
45
|
+
@PrePersist((item) => {
|
|
46
|
+
item.name = `${item.name}-suffix`;
|
|
47
|
+
})
|
|
45
48
|
class User2 {
|
|
46
49
|
id: string;
|
|
47
50
|
address?: Address;
|
|
48
51
|
name: string;
|
|
49
|
-
|
|
50
|
-
prePersist() {
|
|
51
|
-
this.name = `${this.name}-suffix`;
|
|
52
|
-
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
@Model()
|
|
@@ -77,6 +76,8 @@ class BigIntModel {
|
|
|
77
76
|
@Suite()
|
|
78
77
|
export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
79
78
|
|
|
79
|
+
indexLimitSkew = 0;
|
|
80
|
+
|
|
80
81
|
@Test('save it')
|
|
81
82
|
async save() {
|
|
82
83
|
const service = await this.service;
|
|
@@ -268,6 +269,39 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
268
269
|
assert(found[2].age === people[2].age);
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
@Test('verify list abort signal')
|
|
273
|
+
async listAbortSignal() {
|
|
274
|
+
const service = await this.service;
|
|
275
|
+
|
|
276
|
+
await Promise.all(
|
|
277
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => service.upsert(Person, Person.from({
|
|
278
|
+
id: service.idSource.create(),
|
|
279
|
+
name: 'Bob',
|
|
280
|
+
age: 20 + x,
|
|
281
|
+
gender: 'm',
|
|
282
|
+
address: {
|
|
283
|
+
street1: 'a',
|
|
284
|
+
...(x === 1 ? { street2: 'b' } : {})
|
|
285
|
+
}
|
|
286
|
+
})))
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const controller = new AbortController();
|
|
290
|
+
const found: Person[] = [];
|
|
291
|
+
|
|
292
|
+
for await (const items of service.list(Person, { abort: controller.signal, batchSizeHint: 1 })) {
|
|
293
|
+
found.push(...items);
|
|
294
|
+
controller.abort();
|
|
295
|
+
await timers.setTimeout(10);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.indexLimitSkew) {
|
|
299
|
+
assert(found.length > 0 && found.length < this.indexLimitSkew);
|
|
300
|
+
} else {
|
|
301
|
+
assert(found.length === 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
271
305
|
@Test('save it')
|
|
272
306
|
async verifyRaw() {
|
|
273
307
|
const service = await this.service;
|
|
@@ -3,13 +3,12 @@ import timers from 'node:timers/promises';
|
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
5
|
import { castTo } from '@travetto/runtime';
|
|
6
|
-
import { Schema, DiscriminatorField, Text, TypeMismatchError
|
|
6
|
+
import { Schema, DiscriminatorField, Text, TypeMismatchError } from '@travetto/schema';
|
|
7
7
|
import {
|
|
8
|
-
type
|
|
8
|
+
type ModelCrudSupport, Model,
|
|
9
9
|
NotFoundError, SubTypeNotSupportedError, PersistValue
|
|
10
10
|
} from '@travetto/model';
|
|
11
11
|
|
|
12
|
-
import { ModelIndexedUtil } from '../../src/util/indexed.ts';
|
|
13
12
|
import { ExistsError } from '../../src/error/exists.ts';
|
|
14
13
|
|
|
15
14
|
import { BaseModelSuite } from './base.ts';
|
|
@@ -43,42 +42,6 @@ export class Engineer extends Worker {
|
|
|
43
42
|
major: string;
|
|
44
43
|
}
|
|
45
44
|
|
|
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
45
|
@Suite()
|
|
83
46
|
export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
84
47
|
|
|
@@ -108,7 +71,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
108
71
|
const fire2 = await service.get(Worker, fire.id);
|
|
109
72
|
assert(fire2 instanceof Firefighter);
|
|
110
73
|
|
|
111
|
-
const all = await
|
|
74
|
+
const all = await this.toArray(service.list(Worker));
|
|
112
75
|
assert(all.length === 3);
|
|
113
76
|
|
|
114
77
|
const doc3 = all.find(x => x instanceof Doctor);
|
|
@@ -126,7 +89,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
126
89
|
assert(eng3.major === 'oranges');
|
|
127
90
|
assert(eng3.name === 'cob');
|
|
128
91
|
|
|
129
|
-
const engineers = await
|
|
92
|
+
const engineers = await this.toArray(service.list(Engineer));
|
|
130
93
|
assert(engineers.length === 1);
|
|
131
94
|
|
|
132
95
|
await service.create(Engineer, Engineer.from({
|
|
@@ -134,10 +97,10 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
134
97
|
name: 'bob2'
|
|
135
98
|
}));
|
|
136
99
|
|
|
137
|
-
const all2 = await
|
|
100
|
+
const all2 = await this.toArray(service.list(Worker));
|
|
138
101
|
assert(all2.length === 4);
|
|
139
102
|
|
|
140
|
-
const engineers2 = await
|
|
103
|
+
const engineers2 = await this.toArray(service.list(Engineer));
|
|
141
104
|
assert(engineers2.length === 2);
|
|
142
105
|
}
|
|
143
106
|
|
|
@@ -207,75 +170,4 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
207
170
|
e => e instanceof SubTypeNotSupportedError || e instanceof NotFoundError
|
|
208
171
|
);
|
|
209
172
|
}
|
|
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
173
|
}
|
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
|
-
}
|