@travetto/model 5.0.0-rc.7 → 5.0.0-rc.9
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 +1 -1
- package/package.json +7 -7
- package/src/internal/service/common.ts +16 -22
- package/src/internal/service/crud.ts +15 -21
- package/src/internal/service/expiry.ts +2 -5
- package/src/internal/service/indexed.ts +13 -24
- package/src/internal/service/storage.ts +1 -4
- package/src/provider/file.ts +2 -4
- package/src/provider/memory.ts +3 -5
- package/src/registry/decorator.ts +7 -12
- package/src/registry/model.ts +5 -6
- package/src/service/stream.ts +2 -2
- package/support/bin/candidate.ts +2 -3
- package/support/test/base.ts +3 -3
- package/support/test/crud.ts +3 -3
- package/support/test/polymorphism.ts +7 -6
- package/support/test/stream.ts +14 -11
package/README.md
CHANGED
|
@@ -235,7 +235,7 @@ In addition to the provided contracts, the module also provides common utilities
|
|
|
235
235
|
```typescript
|
|
236
236
|
import { Readable } from 'node:stream';
|
|
237
237
|
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
238
|
-
import { Class, TimeSpan, DeepPartial } from '@travetto/runtime';
|
|
238
|
+
import { Class, TimeSpan, DeepPartial, castTo } from '@travetto/runtime';
|
|
239
239
|
import { Injectable } from '@travetto/di';
|
|
240
240
|
import { Config } from '@travetto/config';
|
|
241
241
|
import { ModelCrudSupport } from '../service/crud';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.9",
|
|
4
4
|
"description": "Datastore abstraction for core operations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/model"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^5.0.0-rc.
|
|
30
|
-
"@travetto/di": "^5.0.0-rc.
|
|
31
|
-
"@travetto/registry": "^5.0.0-rc.
|
|
32
|
-
"@travetto/schema": "^5.0.0-rc.
|
|
29
|
+
"@travetto/config": "^5.0.0-rc.9",
|
|
30
|
+
"@travetto/di": "^5.0.0-rc.9",
|
|
31
|
+
"@travetto/registry": "^5.0.0-rc.9",
|
|
32
|
+
"@travetto/schema": "^5.0.0-rc.9"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^5.0.0-rc.
|
|
36
|
-
"@travetto/test": "^5.0.0-rc.
|
|
35
|
+
"@travetto/cli": "^5.0.0-rc.9",
|
|
36
|
+
"@travetto/test": "^5.0.0-rc.9"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ClassInstance } from '@travetto/runtime';
|
|
1
2
|
import type { ModelBulkSupport } from '../../service/bulk';
|
|
2
3
|
import { ModelCrudSupport } from '../../service/crud';
|
|
3
4
|
import type { ModelExpirySupport } from '../../service/expiry';
|
|
@@ -17,62 +18,55 @@ export class ModelIndexedSupportTarget { }
|
|
|
17
18
|
* Type guard for determining if service supports basic operations
|
|
18
19
|
* @param o
|
|
19
20
|
*/
|
|
20
|
-
export function isBasicSupported(o:
|
|
21
|
-
|
|
22
|
-
return !!o && !!(o as Record<string, unknown>)['create'];
|
|
21
|
+
export function isBasicSupported(o: ClassInstance): o is ModelBulkSupport {
|
|
22
|
+
return !!o && 'create' in o;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Type guard for determining if service supports crud operations
|
|
27
27
|
* @param o
|
|
28
28
|
*/
|
|
29
|
-
export function isCrudSupported(o:
|
|
30
|
-
|
|
31
|
-
return !!o && !!(o as Record<string, unknown>)['upsert'];
|
|
29
|
+
export function isCrudSupported(o: ClassInstance): o is ModelCrudSupport {
|
|
30
|
+
return !!o && 'upsert' in o;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
|
-
* Type guard for determining if model
|
|
34
|
+
* Type guard for determining if model supports expiry
|
|
36
35
|
* @param o
|
|
37
36
|
*/
|
|
38
|
-
export function isExpirySupported(o:
|
|
39
|
-
|
|
40
|
-
return !!o && !!(o as Record<string, unknown>)['deleteExpired'];
|
|
37
|
+
export function isExpirySupported(o: ClassInstance): o is ModelExpirySupport {
|
|
38
|
+
return !!o && 'deleteExpired' in o;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
42
|
* Type guard for determining if service supports storage operation
|
|
45
43
|
* @param o
|
|
46
44
|
*/
|
|
47
|
-
export function isStorageSupported(o:
|
|
48
|
-
|
|
49
|
-
return !!o && !!(o as Record<string, unknown>)['createStorage'];
|
|
45
|
+
export function isStorageSupported(o: ClassInstance): o is ModelStorageSupport {
|
|
46
|
+
return !!o && 'createStorage' in o;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* Type guard for determining if service supports streaming operation
|
|
54
51
|
* @param o
|
|
55
52
|
*/
|
|
56
|
-
export function isStreamSupported(o:
|
|
57
|
-
|
|
58
|
-
return !!o && !!(o as Record<string, unknown>)['getStream'];
|
|
53
|
+
export function isStreamSupported(o: ClassInstance): o is ModelStreamSupport {
|
|
54
|
+
return !!o && 'getStream' in o;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
/**
|
|
62
58
|
* Type guard for determining if service supports streaming operation
|
|
63
59
|
* @param o
|
|
64
60
|
*/
|
|
65
|
-
export function isBulkSupported(o:
|
|
66
|
-
|
|
67
|
-
return !!o && !!(o as Record<string, unknown>)['processBulk'];
|
|
61
|
+
export function isBulkSupported(o: ClassInstance): o is ModelBulkSupport {
|
|
62
|
+
return !!o && 'processBulk' in o;
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
/**
|
|
71
66
|
* Type guard for determining if service supports indexed operation
|
|
72
67
|
* @param o
|
|
73
68
|
*/
|
|
74
|
-
export function isIndexedSupported(o:
|
|
75
|
-
|
|
76
|
-
return !!o && !!(o as Record<string, unknown>)['getByIndex'];
|
|
69
|
+
export function isIndexedSupported(o: ClassInstance): o is ModelIndexedSupport {
|
|
70
|
+
return !!o && 'getByIndex' in o;
|
|
77
71
|
}
|
|
78
72
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import { Class, Util } from '@travetto/runtime';
|
|
3
|
+
import { castTo, Class, asFull, Util, asConstructable } from '@travetto/runtime';
|
|
4
4
|
import { DataUtil, SchemaRegistry, SchemaValidator, ValidationError, ValidationResultError } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import { ModelRegistry } from '../../registry/model';
|
|
@@ -46,14 +46,16 @@ export class ModelCrudUtil {
|
|
|
46
46
|
* @param input Input as string or plain object
|
|
47
47
|
*/
|
|
48
48
|
static async load<T extends ModelType>(cls: Class<T>, input: Buffer | string | object, onTypeMismatch: 'notfound' | 'exists' = 'notfound'): Promise<T> {
|
|
49
|
+
let resolvedInput: object;
|
|
49
50
|
if (typeof input === 'string') {
|
|
50
|
-
|
|
51
|
+
resolvedInput = JSON.parse(input);
|
|
51
52
|
} else if (input instanceof Buffer) {
|
|
52
|
-
|
|
53
|
+
resolvedInput = JSON.parse(input.toString('utf8'));
|
|
54
|
+
} else {
|
|
55
|
+
resolvedInput = input;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
const result = ModelRegistry.getBaseModel(cls).from(input as object) as T;
|
|
58
|
+
const result = ModelRegistry.getBaseModel(cls).from(resolvedInput);
|
|
57
59
|
|
|
58
60
|
if (!(result instanceof cls || result.constructor.Ⲑid === cls.Ⲑid)) {
|
|
59
61
|
if (onTypeMismatch === 'notfound') {
|
|
@@ -78,12 +80,10 @@ export class ModelCrudUtil {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
if (DataUtil.isPlainObject(item)) {
|
|
81
|
-
|
|
82
|
-
item = cls.from(item as object);
|
|
83
|
+
item = cls.from(castTo(item));
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
const config = ModelRegistry.get(item.constructor as Class<T>);
|
|
86
|
+
const config = ModelRegistry.get(asConstructable(item).constructor);
|
|
87
87
|
if (config.subType) { // Sub-typing, assign type
|
|
88
88
|
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
89
89
|
}
|
|
@@ -106,8 +106,7 @@ export class ModelCrudUtil {
|
|
|
106
106
|
if (errors.length) {
|
|
107
107
|
throw new ValidationResultError(errors);
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
return item as T;
|
|
109
|
+
return castTo(item);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
/**
|
|
@@ -119,12 +118,10 @@ export class ModelCrudUtil {
|
|
|
119
118
|
*/
|
|
120
119
|
static async naivePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view: undefined | string, getExisting: () => Promise<T>): Promise<T> {
|
|
121
120
|
if (DataUtil.isPlainObject(item)) {
|
|
122
|
-
|
|
123
|
-
item = cls.from(item as object);
|
|
121
|
+
item = cls.from(castTo(item));
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
|
|
127
|
-
const config = ModelRegistry.get(item.constructor as Class<T>);
|
|
124
|
+
const config = ModelRegistry.get(asConstructable(item).constructor);
|
|
128
125
|
if (config.subType) { // Sub-typing, assign type
|
|
129
126
|
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
130
127
|
}
|
|
@@ -139,8 +136,7 @@ export class ModelCrudUtil {
|
|
|
139
136
|
|
|
140
137
|
item = await this.prePersist(cls, item, 'partial');
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
return item as T;
|
|
139
|
+
return asFull(item);
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
/**
|
|
@@ -159,8 +155,7 @@ export class ModelCrudUtil {
|
|
|
159
155
|
const config = ModelRegistry.get(cls);
|
|
160
156
|
for (const state of (config.prePersist ?? [])) {
|
|
161
157
|
if (state.scope === scope || scope === 'all' || state.scope === 'all') {
|
|
162
|
-
|
|
163
|
-
const handler = state.handler as unknown as DataHandler<T>;
|
|
158
|
+
const handler: DataHandler<T> = castTo(state.handler);
|
|
164
159
|
item = await handler(item) ?? item;
|
|
165
160
|
}
|
|
166
161
|
}
|
|
@@ -175,8 +170,7 @@ export class ModelCrudUtil {
|
|
|
175
170
|
*/
|
|
176
171
|
static async postLoad<T>(cls: Class<T>, item: T): Promise<T> {
|
|
177
172
|
const config = ModelRegistry.get(cls);
|
|
178
|
-
|
|
179
|
-
for (const handler of (config.postLoad ?? []) as unknown as DataHandler<T>[]) {
|
|
173
|
+
for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
|
|
180
174
|
item = await handler(item) ?? item;
|
|
181
175
|
}
|
|
182
176
|
if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShutdownManager, Class, TimeSpan, TimeUtil, Util } from '@travetto/runtime';
|
|
1
|
+
import { ShutdownManager, Class, TimeSpan, TimeUtil, Util, castTo } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { ModelRegistry } from '../../registry/model';
|
|
4
4
|
import { ModelExpirySupport } from '../../service/expiry';
|
|
@@ -15,10 +15,7 @@ export class ModelExpiryUtil {
|
|
|
15
15
|
*/
|
|
16
16
|
static getExpiryState<T extends ModelType>(cls: Class<T>, item: T): { expiresAt?: Date, expired?: boolean } {
|
|
17
17
|
const expKey = ModelRegistry.getExpiry(cls);
|
|
18
|
-
|
|
19
|
-
const keyAsT = expKey as keyof T;
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
|
-
const expiresAt = item[keyAsT] ? item[keyAsT] as unknown as Date : undefined;
|
|
18
|
+
const expiresAt: Date = castTo(item[expKey]);
|
|
22
19
|
|
|
23
20
|
return {
|
|
24
21
|
expiresAt,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, DeepPartial } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class, DeepPartial, TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { IndexNotSupported } from '../../error/invalid-index';
|
|
4
4
|
import { NotFoundError } from '../../error/not-found';
|
|
@@ -44,38 +44,30 @@ export class ModelIndexedUtil {
|
|
|
44
44
|
const parts = [];
|
|
45
45
|
|
|
46
46
|
while (o !== undefined && o !== null) {
|
|
47
|
-
const k =
|
|
48
|
-
|
|
49
|
-
o = (o[k] as Record<string, unknown>);
|
|
47
|
+
const k = TypedObject.keys(f)[0];
|
|
48
|
+
o = castTo(o[k]);
|
|
50
49
|
parts.push(k);
|
|
51
|
-
|
|
52
|
-
const fk = k as (keyof typeof f);
|
|
53
|
-
if (typeof f[fk] === 'boolean' || typeof f[fk] === 'number') {
|
|
50
|
+
if (typeof f[k] === 'boolean' || typeof f[k] === 'number') {
|
|
54
51
|
if (cfg.type === 'sorted') {
|
|
55
|
-
|
|
56
|
-
sortDir = f[fk] === true ? 1 : f[fk] as number;
|
|
52
|
+
sortDir = f[k] === true ? 1 : f[k] === false ? 0 : f[k];
|
|
57
53
|
}
|
|
58
54
|
break; // At the bottom
|
|
59
55
|
} else {
|
|
60
|
-
|
|
61
|
-
f = f[fk] as Record<string, unknown>;
|
|
56
|
+
f = castTo(f[k]);
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
if (field === sortField) {
|
|
65
|
-
|
|
66
|
-
sorted = { path: parts, dir: sortDir, value: o as unknown as number | Date };
|
|
60
|
+
sorted = { path: parts, dir: sortDir, value: castTo(o) };
|
|
67
61
|
}
|
|
68
62
|
if (o === undefined || o === null) {
|
|
69
63
|
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
70
64
|
if (empty === undefined || empty === Error) {
|
|
71
65
|
throw new IndexNotSupported(cls, cfg, `Missing field value for ${parts.join('.')}`);
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
o = empty as Record<string, unknown>;
|
|
67
|
+
o = castTo(empty!);
|
|
75
68
|
} else {
|
|
76
69
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
77
|
-
|
|
78
|
-
fields.push({ path: parts, value: o as unknown as string | boolean | Date | number });
|
|
70
|
+
fields.push({ path: parts, value: castTo(o) });
|
|
79
71
|
}
|
|
80
72
|
}
|
|
81
73
|
}
|
|
@@ -92,12 +84,11 @@ export class ModelIndexedUtil {
|
|
|
92
84
|
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, cfg?: ComputeConfig): Record<string, unknown> {
|
|
93
85
|
const res: Record<string, unknown> = {};
|
|
94
86
|
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, cfg).fields) {
|
|
95
|
-
let sub = res;
|
|
87
|
+
let sub: Record<string, unknown> = res;
|
|
96
88
|
const all = path.slice(0);
|
|
97
89
|
const last = all.pop()!;
|
|
98
90
|
for (const k of all) {
|
|
99
|
-
|
|
100
|
-
sub = (sub[k] ??= {}) as typeof res;
|
|
91
|
+
sub = castTo(sub[k] ??= {});
|
|
101
92
|
}
|
|
102
93
|
sub[last] = value;
|
|
103
94
|
}
|
|
@@ -134,11 +125,9 @@ export class ModelIndexedUtil {
|
|
|
134
125
|
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
135
126
|
): Promise<T> {
|
|
136
127
|
try {
|
|
137
|
-
|
|
138
|
-
const { id } = await service.getByIndex(cls, idx, body as DeepPartial<T>);
|
|
128
|
+
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
139
129
|
body.id = id;
|
|
140
|
-
|
|
141
|
-
return await service.update(cls, body as T);
|
|
130
|
+
return await service.update(cls, castTo(body));
|
|
142
131
|
} catch (err) {
|
|
143
132
|
if (err instanceof NotFoundError) {
|
|
144
133
|
return await service.create(cls, body);
|
|
@@ -12,14 +12,11 @@ export class ModelStorageUtil {
|
|
|
12
12
|
/**
|
|
13
13
|
* Register change listener on startup
|
|
14
14
|
*/
|
|
15
|
-
static async registerModelChangeListener(storage: ModelStorageSupport
|
|
15
|
+
static async registerModelChangeListener(storage: ModelStorageSupport): Promise<void> {
|
|
16
16
|
if (!Runtime.dynamic || !(storage?.config?.autoCreate ?? !Runtime.production)) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
|
-
target = target ?? storage.constructor as Class<ModelStorageSupport>;
|
|
22
|
-
|
|
23
20
|
const checkType = (cls: Class, enforceBase = true): boolean => {
|
|
24
21
|
if (enforceBase && ModelRegistry.getBaseModel(cls) !== cls) {
|
|
25
22
|
return false;
|
package/src/provider/file.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Readable } from 'node:stream';
|
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
import { Class, TimeSpan, Runtime } from '@travetto/runtime';
|
|
8
|
+
import { Class, TimeSpan, Runtime, asFull } from '@travetto/runtime';
|
|
9
9
|
import { Injectable } from '@travetto/di';
|
|
10
10
|
import { Config } from '@travetto/config';
|
|
11
11
|
import { Required } from '@travetto/schema';
|
|
@@ -153,9 +153,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
153
153
|
item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
154
154
|
const file = await this.#resolveName(cls, '.json', item.id);
|
|
155
155
|
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
156
|
-
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
158
|
-
return item as T;
|
|
156
|
+
return asFull<T>(item);
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
package/src/provider/memory.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
3
3
|
|
|
4
|
-
import { Class, TimeSpan, DeepPartial } from '@travetto/runtime';
|
|
4
|
+
import { Class, TimeSpan, DeepPartial, castTo } from '@travetto/runtime';
|
|
5
5
|
import { Injectable } from '@travetto/di';
|
|
6
6
|
import { Config } from '@travetto/config';
|
|
7
7
|
|
|
@@ -87,8 +87,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
87
87
|
const item = await this.get(cls, id);
|
|
88
88
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
89
89
|
const idxName = indexName(cls, idx);
|
|
90
|
-
|
|
91
|
-
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
90
|
+
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
|
|
92
91
|
this.#indices[idx.type].get(idxName)?.get(key)?.delete(id);
|
|
93
92
|
}
|
|
94
93
|
} catch (err) {
|
|
@@ -101,8 +100,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
|
|
|
101
100
|
async #writeIndices<T extends ModelType>(cls: Class<T>, item: T): Promise<void> {
|
|
102
101
|
for (const idx of ModelRegistry.getIndices(cls, ['sorted', 'unsorted'])) {
|
|
103
102
|
const idxName = indexName(cls, idx);
|
|
104
|
-
|
|
105
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item as DeepPartial<T>);
|
|
103
|
+
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, castTo(item));
|
|
106
104
|
let index = this.#indices[idx.type].get(idxName)?.get(key);
|
|
107
105
|
|
|
108
106
|
if (!index) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/runtime';
|
|
1
|
+
import { asConstructable, castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistry } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
@@ -25,7 +25,7 @@ export function Model(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
|
25
25
|
* Defines an index on a model
|
|
26
26
|
*/
|
|
27
27
|
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
28
|
-
return function (target: Class<T>) {
|
|
28
|
+
return function (target: Class<T>): void {
|
|
29
29
|
ModelRegistry.getOrCreatePending(target).indices!.push(...indices);
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -36,8 +36,7 @@ export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
|
36
36
|
*/
|
|
37
37
|
export function ExpiresAt() {
|
|
38
38
|
return <K extends string, T extends Partial<Record<K, Date>>>(tgt: T, prop: K): void => {
|
|
39
|
-
|
|
40
|
-
ModelRegistry.register(tgt.constructor as Class<T>, { expiresAt: prop });
|
|
39
|
+
ModelRegistry.register(asConstructable(tgt).constructor, { expiresAt: prop });
|
|
41
40
|
};
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -49,8 +48,7 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
49
48
|
ModelRegistry.registerDataHandlers(tgt, {
|
|
50
49
|
prePersist: [{
|
|
51
50
|
scope,
|
|
52
|
-
|
|
53
|
-
handler: handler as DataHandler
|
|
51
|
+
handler: castTo(handler)
|
|
54
52
|
}]
|
|
55
53
|
});
|
|
56
54
|
};
|
|
@@ -61,13 +59,11 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
61
59
|
*/
|
|
62
60
|
export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
63
61
|
return function <K extends string, C extends Partial<Record<K, T>>>(tgt: C, prop: K): void {
|
|
64
|
-
|
|
65
|
-
ModelRegistry.registerDataHandlers(tgt.constructor as Class<C>, {
|
|
62
|
+
ModelRegistry.registerDataHandlers(asConstructable(tgt).constructor, {
|
|
66
63
|
prePersist: [{
|
|
67
64
|
scope,
|
|
68
65
|
handler: (inst): void => {
|
|
69
|
-
|
|
70
|
-
const cInst = (inst as unknown as Record<K, T>);
|
|
66
|
+
const cInst: Record<K, T> = castTo(inst);
|
|
71
67
|
cInst[prop] = handler(cInst[prop]);
|
|
72
68
|
}
|
|
73
69
|
}]
|
|
@@ -81,7 +77,6 @@ export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PreP
|
|
|
81
77
|
*/
|
|
82
78
|
export function PostLoad<T>(handler: DataHandler<T>) {
|
|
83
79
|
return function (tgt: Class<T>): void {
|
|
84
|
-
|
|
85
|
-
ModelRegistry.registerDataHandlers(tgt, { postLoad: [handler as DataHandler] });
|
|
80
|
+
ModelRegistry.registerDataHandlers(tgt, { postLoad: [castTo(handler)] });
|
|
86
81
|
};
|
|
87
82
|
}
|
package/src/registry/model.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SchemaRegistry } from '@travetto/schema';
|
|
2
2
|
import { MetadataRegistry } from '@travetto/registry';
|
|
3
3
|
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
-
import { AppError, Class, describeFunction } from '@travetto/runtime';
|
|
4
|
+
import { AppError, castTo, Class, describeFunction, asFull } from '@travetto/runtime';
|
|
5
5
|
import { AllViewⲐ } from '@travetto/schema/src/internal/types';
|
|
6
6
|
|
|
7
7
|
import { IndexConfig, IndexType, ModelOptions } from './types';
|
|
@@ -71,8 +71,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
onInstallFinalize(cls: Class): ModelOptions<ModelType> {
|
|
74
|
-
|
|
75
|
-
const config = this.pending.get(cls.Ⲑid)! as ModelOptions<ModelType>;
|
|
74
|
+
const config = asFull(this.pending.get(cls.Ⲑid)!);
|
|
76
75
|
|
|
77
76
|
const schema = SchemaRegistry.get(cls);
|
|
78
77
|
const view = schema.views[AllViewⲐ].schema;
|
|
@@ -103,7 +102,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
103
102
|
/**
|
|
104
103
|
* Find base class for a given model
|
|
105
104
|
*/
|
|
106
|
-
getBaseModel(cls: Class): Class<
|
|
105
|
+
getBaseModel<T extends ModelType>(cls: Class<T>): Class<T> {
|
|
107
106
|
if (!this.baseModels.has(cls)) {
|
|
108
107
|
let conf = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
109
108
|
let parent = cls;
|
|
@@ -208,12 +207,12 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
208
207
|
* Get expiry field
|
|
209
208
|
* @param cls
|
|
210
209
|
*/
|
|
211
|
-
getExpiry(cls: Class):
|
|
210
|
+
getExpiry<T extends ModelType>(cls: Class<T>): keyof T {
|
|
212
211
|
const expiry = this.get(cls).expiresAt;
|
|
213
212
|
if (!expiry) {
|
|
214
213
|
throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`, 'general');
|
|
215
214
|
}
|
|
216
|
-
return expiry;
|
|
215
|
+
return castTo(expiry);
|
|
217
216
|
}
|
|
218
217
|
}
|
|
219
218
|
|
package/src/service/stream.ts
CHANGED
|
@@ -12,11 +12,11 @@ export interface StreamMeta {
|
|
|
12
12
|
/**
|
|
13
13
|
* Hash of the file contents. Different files with the same name, will have the same hash
|
|
14
14
|
*/
|
|
15
|
-
hash
|
|
15
|
+
hash?: string;
|
|
16
16
|
/**
|
|
17
17
|
* The original base filename of the file
|
|
18
18
|
*/
|
|
19
|
-
filename
|
|
19
|
+
filename?: string;
|
|
20
20
|
/**
|
|
21
21
|
* Filenames title, optional for elements like images, audio, videos
|
|
22
22
|
*/
|
package/support/bin/candidate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { ModelRegistry } from '@travetto/model/src/registry/model';
|
|
3
3
|
import { InjectableConfig, DependencyRegistry } from '@travetto/di';
|
|
4
4
|
import { ModelStorageSupportTarget } from '@travetto/model/src/internal/service/common';
|
|
@@ -40,8 +40,7 @@ export class ModelCandidateUtil {
|
|
|
40
40
|
* Get all providers that are viable candidates
|
|
41
41
|
*/
|
|
42
42
|
static async getProviders(op?: keyof ModelStorageSupport): Promise<InjectableConfig[]> {
|
|
43
|
-
|
|
44
|
-
const types = DependencyRegistry.getCandidateTypes<ModelStorageSupport>(ModelStorageSupportTarget as unknown as Class<ModelStorageSupport>);
|
|
43
|
+
const types = DependencyRegistry.getCandidateTypes<ModelStorageSupport>(castTo(ModelStorageSupportTarget));
|
|
45
44
|
return types.filter(x => !op || x.class.prototype?.[op]);
|
|
46
45
|
}
|
|
47
46
|
|
package/support/test/base.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DependencyRegistry } from '@travetto/di';
|
|
2
|
-
import { AppError, Class } from '@travetto/runtime';
|
|
2
|
+
import { AppError, castTo, Class, classConstruct } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { isBulkSupported, isCrudSupported } from '../../src/internal/service/common';
|
|
5
5
|
import { ModelType } from '../../src/types/model';
|
|
@@ -11,7 +11,7 @@ type ServiceClass = { serviceClass: { new(): unknown } };
|
|
|
11
11
|
export abstract class BaseModelSuite<T> {
|
|
12
12
|
|
|
13
13
|
static ifNot(pred: (svc: unknown) => boolean): (x: unknown) => Promise<boolean> {
|
|
14
|
-
return async (x: unknown) => !pred(
|
|
14
|
+
return async (x: unknown) => !pred(classConstruct(castTo<ServiceClass>(x).serviceClass));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
serviceClass: Class<T>;
|
|
@@ -36,7 +36,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
36
36
|
const res = await svc.processBulk(cls, items.map(x => ({ insert: x })));
|
|
37
37
|
return res.counts.insert;
|
|
38
38
|
} else if (isCrudSupported(svc)) {
|
|
39
|
-
const out
|
|
39
|
+
const out: Promise<M>[] = [];
|
|
40
40
|
for (const el of items) {
|
|
41
41
|
out.push(svc.create(cls, el));
|
|
42
42
|
}
|
package/support/test/crud.ts
CHANGED
|
@@ -49,7 +49,7 @@ class User2 {
|
|
|
49
49
|
name: string;
|
|
50
50
|
|
|
51
51
|
prePersist() {
|
|
52
|
-
this.name = `${this.name}-
|
|
52
|
+
this.name = `${this.name}-suffix`;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -193,7 +193,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
193
193
|
}));
|
|
194
194
|
|
|
195
195
|
assert(o.address === undefined);
|
|
196
|
-
assert(o.name === 'bob-
|
|
196
|
+
assert(o.name === 'bob-suffix');
|
|
197
197
|
|
|
198
198
|
await service.updatePartial(User2, User2.from({
|
|
199
199
|
id: o.id,
|
|
@@ -217,7 +217,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
217
217
|
assert(res.createdDate instanceof Date);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
@Test('verify
|
|
220
|
+
@Test('verify pre-persist on create/update')
|
|
221
221
|
async testPrePersist() {
|
|
222
222
|
const service = await this.service;
|
|
223
223
|
const res = await service.create(Dated, Dated.from({}));
|
|
@@ -12,6 +12,7 @@ import { isIndexedSupported } from '../../src/internal/service/common';
|
|
|
12
12
|
import { ExistsError } from '../../src/error/exists';
|
|
13
13
|
|
|
14
14
|
import { BaseModelSuite } from './base';
|
|
15
|
+
import { castTo } from '@travetto/runtime';
|
|
15
16
|
|
|
16
17
|
@Model({ baseType: true })
|
|
17
18
|
export class Worker {
|
|
@@ -70,7 +71,7 @@ export class IndexedEngineer extends IndexedWorker {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
73
|
-
const out
|
|
74
|
+
const out: T[] = [];
|
|
74
75
|
for await (const el of iterable) {
|
|
75
76
|
out.push(el);
|
|
76
77
|
}
|
|
@@ -116,12 +117,12 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
116
117
|
|
|
117
118
|
const fire3 = all.find(x => x instanceof Firefighter);
|
|
118
119
|
assert(fire3 instanceof Firefighter);
|
|
119
|
-
assert(
|
|
120
|
+
assert(fire3.firehouse === 20);
|
|
120
121
|
assert(fire3.name === 'rob');
|
|
121
122
|
|
|
122
123
|
const eng3 = all.find(x => x instanceof Engineer);
|
|
123
124
|
assert(eng3 instanceof Engineer);
|
|
124
|
-
assert(
|
|
125
|
+
assert(eng3.major === 'oranges');
|
|
125
126
|
assert(eng3.name === 'cob');
|
|
126
127
|
|
|
127
128
|
const engineers = await collect(service.list(Engineer));
|
|
@@ -161,7 +162,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
161
162
|
);
|
|
162
163
|
|
|
163
164
|
await assert.rejects(
|
|
164
|
-
() => service.update(Engineer, Doctor.from({ ...doc })
|
|
165
|
+
() => service.update(Engineer, castTo(Doctor.from({ ...doc }))),
|
|
165
166
|
(e: Error) => (e instanceof NotFoundError || e instanceof SubTypeNotSupportedError || e instanceof TypeMismatchError) ? undefined : e);
|
|
166
167
|
|
|
167
168
|
await timers.setTimeout(15);
|
|
@@ -205,7 +206,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
205
206
|
|
|
206
207
|
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
|
|
207
208
|
async polymorphicIndexGet() {
|
|
208
|
-
const service = (await this.service)
|
|
209
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
209
210
|
const now = 30;
|
|
210
211
|
const [doc, fire, eng] = [
|
|
211
212
|
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
@@ -235,7 +236,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
235
236
|
|
|
236
237
|
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
|
|
237
238
|
async polymorphicIndexDelete() {
|
|
238
|
-
const service = (await this.service)
|
|
239
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
239
240
|
const now = 30;
|
|
240
241
|
const [doc, fire, eng] = [
|
|
241
242
|
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
package/support/test/stream.ts
CHANGED
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { Readable } from 'node:stream';
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
|
-
import {
|
|
6
|
+
import { text as toText } from 'node:stream/consumers';
|
|
7
7
|
|
|
8
8
|
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
9
9
|
|
|
@@ -17,9 +17,9 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
|
|
|
17
17
|
fixture = new TestFixtures(['@travetto/model']);
|
|
18
18
|
|
|
19
19
|
async getHash(stream: Readable): Promise<string> {
|
|
20
|
-
const
|
|
21
|
-
await pipeline(stream,
|
|
22
|
-
return
|
|
20
|
+
const hash = crypto.createHash('sha1').setEncoding('hex');
|
|
21
|
+
await pipeline(stream, hash);
|
|
22
|
+
return hash.read().toString();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
async getStream(resource: string): Promise<readonly [{ size: number, contentType: string, hash: string, filename: string }, Readable]> {
|
|
@@ -77,30 +77,33 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
|
|
|
77
77
|
await service.upsertStream(meta.hash, stream, meta);
|
|
78
78
|
|
|
79
79
|
const retrieved = await service.getStream(meta.hash);
|
|
80
|
-
const content =
|
|
80
|
+
const content = await toText(retrieved);
|
|
81
81
|
assert(content.startsWith('abc'));
|
|
82
82
|
assert(content.endsWith('xyz'));
|
|
83
83
|
|
|
84
84
|
const partial = await service.getStream(meta.hash, { start: 10, end: 20 });
|
|
85
|
-
const subContent =
|
|
85
|
+
const subContent = await toText(partial);
|
|
86
86
|
const range = await enforceRange({ start: 10, end: 20 }, meta.size);
|
|
87
87
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
const og = await this.fixture.read('/text.txt');
|
|
90
|
+
|
|
91
|
+
assert(subContent === og.substring(10, 21));
|
|
89
92
|
|
|
90
93
|
const partialUnbounded = await service.getStream(meta.hash, { start: 10 });
|
|
91
|
-
const subContent2 =
|
|
94
|
+
const subContent2 = await toText(partialUnbounded);
|
|
92
95
|
const range2 = await enforceRange({ start: 10 }, meta.size);
|
|
93
96
|
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
94
97
|
assert(subContent2.startsWith('klm'));
|
|
95
98
|
assert(subContent2.endsWith('xyz'));
|
|
96
99
|
|
|
97
100
|
const partialSingle = await service.getStream(meta.hash, { start: 10, end: 10 });
|
|
98
|
-
const subContent3 =
|
|
101
|
+
const subContent3 = await toText(partialSingle);
|
|
99
102
|
assert(subContent3.length === 1);
|
|
100
103
|
assert(subContent3 === 'k');
|
|
101
104
|
|
|
102
|
-
const
|
|
103
|
-
const subContent4 =
|
|
105
|
+
const partialOverBounded = await service.getStream(meta.hash, { start: 20, end: 40 });
|
|
106
|
+
const subContent4 = await toText(partialOverBounded);
|
|
104
107
|
assert(subContent4.length === 6);
|
|
105
108
|
assert(subContent4.endsWith('xyz'));
|
|
106
109
|
|