@travetto/model 3.3.3 → 3.3.4
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 -9
- package/package.json +4 -4
- package/src/internal/service/crud.ts +34 -10
- package/src/registry/decorator.ts +40 -1
- package/src/registry/model.ts +20 -1
- package/src/registry/types.ts +10 -0
- package/src/types/model.ts +0 -8
- package/support/test/crud.ts +5 -0
- package/support/test/indexed.ts +2 -6
- package/support/test/polymorphism.ts +6 -7
package/README.md
CHANGED
|
@@ -216,18 +216,10 @@ export interface ModelType {
|
|
|
216
216
|
* If not provided, will be computed on create
|
|
217
217
|
*/
|
|
218
218
|
id: string;
|
|
219
|
-
/**
|
|
220
|
-
* Run before saving
|
|
221
|
-
*/
|
|
222
|
-
prePersist?(): void | Promise<void>;
|
|
223
|
-
/**
|
|
224
|
-
* Run after loading
|
|
225
|
-
*/
|
|
226
|
-
postLoad?(): void | Promise<void>;
|
|
227
219
|
}
|
|
228
220
|
```
|
|
229
221
|
|
|
230
|
-
|
|
222
|
+
The `id` is the only required field for a model, as this is a hard requirement on naming and type. This may make using existing data models impossible if types other than strings are required. Additionally, the `type` field, is intended to record the base model type, but can be remapped. This is important to support polymorphism, not only in [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."), but also in [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.").
|
|
231
223
|
|
|
232
224
|
## Implementations
|
|
233
225
|
|Service|Basic|CRUD|Indexed|Expiry|Stream|Bulk|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
4
4
|
"description": "Datastore abstraction for core operations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"directory": "module/model"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^3.3.
|
|
29
|
+
"@travetto/config": "^3.3.4",
|
|
30
30
|
"@travetto/di": "^3.3.3",
|
|
31
31
|
"@travetto/registry": "^3.3.3",
|
|
32
|
-
"@travetto/schema": "^3.3.
|
|
32
|
+
"@travetto/schema": "^3.3.4"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^3.3.
|
|
35
|
+
"@travetto/cli": "^3.3.5",
|
|
36
36
|
"@travetto/test": "^3.3.4"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
@@ -8,6 +8,7 @@ import { ModelIdSource, ModelType, OptionalId } from '../../types/model';
|
|
|
8
8
|
import { NotFoundError } from '../../error/not-found';
|
|
9
9
|
import { ExistsError } from '../../error/exists';
|
|
10
10
|
import { SubTypeNotSupportedError } from '../../error/invalid-sub-type';
|
|
11
|
+
import { DataHandler } from '../../registry/types';
|
|
11
12
|
|
|
12
13
|
export type ModelCrudProvider = {
|
|
13
14
|
idSource: ModelIdSource;
|
|
@@ -62,10 +63,7 @@ export class ModelCrudUtil {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
await result.postLoad();
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
66
|
+
return this.postLoad(cls, result);
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
/**
|
|
@@ -90,9 +88,7 @@ export class ModelCrudUtil {
|
|
|
90
88
|
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
await item.prePersist();
|
|
95
|
-
}
|
|
91
|
+
item = await this.prePersist(cls, item);
|
|
96
92
|
|
|
97
93
|
let errors: ValidationError[] = [];
|
|
98
94
|
try {
|
|
@@ -141,9 +137,7 @@ export class ModelCrudUtil {
|
|
|
141
137
|
|
|
142
138
|
item = Object.assign(existing, item);
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
await item.prePersist();
|
|
146
|
-
}
|
|
140
|
+
item = await this.prePersist(cls, item);
|
|
147
141
|
|
|
148
142
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
149
143
|
return item as T;
|
|
@@ -157,4 +151,34 @@ export class ModelCrudUtil {
|
|
|
157
151
|
throw new SubTypeNotSupportedError(cls);
|
|
158
152
|
}
|
|
159
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pre persist behavior
|
|
157
|
+
*/
|
|
158
|
+
static async prePersist<T>(cls: Class<T>, item: T): Promise<T> {
|
|
159
|
+
const config = ModelRegistry.get(cls);
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
161
|
+
for (const handler of (config.prePersist ?? []) as unknown as DataHandler<T>[]) {
|
|
162
|
+
item = await handler(item) ?? item;
|
|
163
|
+
}
|
|
164
|
+
if (typeof item === 'object' && item && 'prePersist' in item && typeof item['prePersist'] === 'function') {
|
|
165
|
+
item = await item.prePersist() ?? item;
|
|
166
|
+
}
|
|
167
|
+
return item;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Post load behavior
|
|
172
|
+
*/
|
|
173
|
+
static async postLoad<T>(cls: Class<T>, item: T): Promise<T> {
|
|
174
|
+
const config = ModelRegistry.get(cls);
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
176
|
+
for (const handler of (config.postLoad ?? []) as unknown as DataHandler<T>[]) {
|
|
177
|
+
item = await handler(item) ?? item;
|
|
178
|
+
}
|
|
179
|
+
if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
|
|
180
|
+
item = await item.postLoad() ?? item;
|
|
181
|
+
}
|
|
182
|
+
return item;
|
|
183
|
+
}
|
|
160
184
|
}
|
|
@@ -3,7 +3,7 @@ import { SchemaRegistry } from '@travetto/schema';
|
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
5
5
|
import { ModelRegistry } from './model';
|
|
6
|
-
import { IndexConfig, ModelOptions } from './types';
|
|
6
|
+
import { DataHandler, IndexConfig, ModelOptions } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Model decorator, extends `@Schema`
|
|
@@ -40,4 +40,43 @@ export function ExpiresAt() {
|
|
|
40
40
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
41
41
|
ModelRegistry.register(tgt.constructor as Class<T>, { expiresAt: prop });
|
|
42
42
|
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Model class decorator for pre-persist behavior
|
|
47
|
+
*/
|
|
48
|
+
export function PrePersist<T>(handler: DataHandler<T>) {
|
|
49
|
+
return function (tgt: Class<T>): void {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
51
|
+
ModelRegistry.registerDataHandlers(tgt, { prePersist: [handler as DataHandler] });
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Model field decorator for pre-persist value setting
|
|
57
|
+
*/
|
|
58
|
+
export function PersistValue<T>(handler: (curr: T | undefined) => T) {
|
|
59
|
+
return function <K extends string, C extends Partial<Record<K, T>>>(tgt: C, prop: K): void {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
61
|
+
ModelRegistry.registerDataHandlers(tgt.constructor as Class<C>, {
|
|
62
|
+
prePersist: [
|
|
63
|
+
(inst): void => {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
65
|
+
const cInst = (inst as unknown as Record<K, T>);
|
|
66
|
+
cInst[prop] = handler(cInst[prop]);
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Model class decorator for post-load behavior
|
|
76
|
+
*/
|
|
77
|
+
export function PostLoad<T>(handler: DataHandler<T>) {
|
|
78
|
+
return function (tgt: Class<T>): void {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
80
|
+
ModelRegistry.registerDataHandlers(tgt, { postLoad: [handler as DataHandler] });
|
|
81
|
+
};
|
|
43
82
|
}
|
package/src/registry/model.ts
CHANGED
|
@@ -52,7 +52,16 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
createPending(cls: Class): Partial<ModelOptions<ModelType>> {
|
|
55
|
-
return { class: cls, indices: [], autoCreate: true, baseType: RootIndex.getFunctionMetadata(cls)?.abstract };
|
|
55
|
+
return { class: cls, indices: [], autoCreate: true, baseType: RootIndex.getFunctionMetadata(cls)?.abstract, postLoad: [], prePersist: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerDataHandlers(cls: Class, pConfig?: Partial<ModelOptions<ModelType>>): void {
|
|
59
|
+
const cfg = this.getOrCreatePending(cls);
|
|
60
|
+
this.register(cls, {
|
|
61
|
+
...cfg,
|
|
62
|
+
prePersist: [...cfg.prePersist ?? [], ...pConfig?.prePersist ?? []],
|
|
63
|
+
postLoad: [...cfg.postLoad ?? [], ...pConfig?.postLoad ?? []],
|
|
64
|
+
});
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
onInstallFinalize(cls: Class): ModelOptions<ModelType> {
|
|
@@ -66,6 +75,16 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
66
75
|
if (schema.subTypeField in view && this.getBaseModel(cls) !== cls) {
|
|
67
76
|
config.subType = !!schema.subTypeName; // Copy from schema
|
|
68
77
|
delete view[schema.subTypeField].required; // Allow type to be optional
|
|
78
|
+
let parent = this.getParentClass(cls);
|
|
79
|
+
let from = cls;
|
|
80
|
+
// Merge inherited prepersist/postload
|
|
81
|
+
while (parent && from !== parent) {
|
|
82
|
+
const pCfg = this.get(parent);
|
|
83
|
+
config.prePersist = [...pCfg.prePersist ?? [], ...config.prePersist ?? []];
|
|
84
|
+
config.postLoad = [...pCfg.postLoad ?? [], ...config.postLoad ?? []];
|
|
85
|
+
from = parent;
|
|
86
|
+
parent = this.getParentClass(from);
|
|
87
|
+
}
|
|
69
88
|
}
|
|
70
89
|
return config;
|
|
71
90
|
}
|
package/src/registry/types.ts
CHANGED
|
@@ -21,6 +21,8 @@ type IndexClauseRaw<T> = {
|
|
|
21
21
|
T[P] extends object ? IndexClauseRaw<RetainFields<T[P]>> : 1 | -1 | true;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
27
|
* Model options
|
|
26
28
|
*/
|
|
@@ -57,6 +59,14 @@ export class ModelOptions<T extends ModelType = ModelType> {
|
|
|
57
59
|
* Auto create in development mode
|
|
58
60
|
*/
|
|
59
61
|
autoCreate: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Pre-persist handlers
|
|
64
|
+
*/
|
|
65
|
+
prePersist?: DataHandler<unknown>[];
|
|
66
|
+
/**
|
|
67
|
+
* Post-load handlers
|
|
68
|
+
*/
|
|
69
|
+
postLoad?: DataHandler<unknown>[];
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/**
|
package/src/types/model.ts
CHANGED
|
@@ -13,14 +13,6 @@ export interface ModelType {
|
|
|
13
13
|
* If not provided, will be computed on create
|
|
14
14
|
*/
|
|
15
15
|
id: string;
|
|
16
|
-
/**
|
|
17
|
-
* Run before saving
|
|
18
|
-
*/
|
|
19
|
-
prePersist?(): void | Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Run after loading
|
|
22
|
-
*/
|
|
23
|
-
postLoad?(): void | Promise<void>;
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
export type OptionalId<T extends { id: string }> = Omit<T, 'id'> & { id?: string };
|
package/support/test/crud.ts
CHANGED
|
@@ -46,6 +46,10 @@ class User2 {
|
|
|
46
46
|
id: string;
|
|
47
47
|
address?: Address;
|
|
48
48
|
name: string;
|
|
49
|
+
|
|
50
|
+
prePersist() {
|
|
51
|
+
this.name = `${this.name}-suff`;
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
@Model()
|
|
@@ -175,6 +179,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
175
179
|
}));
|
|
176
180
|
|
|
177
181
|
assert(o.address === undefined);
|
|
182
|
+
assert(o.name === 'bob-suff');
|
|
178
183
|
|
|
179
184
|
await service.updatePartial(User2, User2.from({
|
|
180
185
|
id: o.id,
|
package/support/test/indexed.ts
CHANGED
|
@@ -48,13 +48,9 @@ class Child {
|
|
|
48
48
|
@Index({ type: 'sorted', name: 'nameCreated', fields: [{ child: { name: 1 } }, { createdDate: 1 }] })
|
|
49
49
|
class User4 {
|
|
50
50
|
id: string;
|
|
51
|
-
createdDate?: Date;
|
|
51
|
+
createdDate?: Date = new Date();
|
|
52
52
|
color: string;
|
|
53
53
|
child: Child;
|
|
54
|
-
|
|
55
|
-
prePersist?() {
|
|
56
|
-
this.createdDate ??= new Date();
|
|
57
|
-
}
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
@Suite()
|
|
@@ -157,7 +153,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
|
|
|
157
153
|
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.timeFromNow('2d'), color: 'red' }));
|
|
158
154
|
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.timeFromNow('-1d'), color: 'green' }));
|
|
159
155
|
|
|
160
|
-
const arr = await this.toArray(service.listByIndex(User4, 'nameCreated',
|
|
156
|
+
const arr = await this.toArray(service.listByIndex(User4, 'nameCreated', { child: { name: 'bob' } }));
|
|
161
157
|
|
|
162
158
|
assert(arr[0].color === 'green' && arr[0].child.name === 'bob' && arr[0].child.age === 50);
|
|
163
159
|
assert(arr[1].color === 'red' && arr[1].child.name === 'bob' && arr[1].child.age === 30);
|
|
@@ -2,10 +2,10 @@ import assert from 'assert';
|
|
|
2
2
|
import timers from 'timers/promises';
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
|
-
import { Text, TypeMismatchError } from '@travetto/schema';
|
|
5
|
+
import { SubTypeField, Text, TypeMismatchError } from '@travetto/schema';
|
|
6
6
|
import {
|
|
7
7
|
ModelIndexedSupport, Index, ModelCrudSupport, Model,
|
|
8
|
-
NotFoundError, SubTypeNotSupportedError
|
|
8
|
+
NotFoundError, SubTypeNotSupportedError, PersistValue
|
|
9
9
|
} from '@travetto/model';
|
|
10
10
|
|
|
11
11
|
import { isIndexedSupported } from '../../src/internal/service/common';
|
|
@@ -16,15 +16,14 @@ import { BaseModelSuite } from './base';
|
|
|
16
16
|
@Model({ baseType: true })
|
|
17
17
|
export class Worker {
|
|
18
18
|
id: string;
|
|
19
|
-
|
|
19
|
+
@SubTypeField()
|
|
20
|
+
_type: string;
|
|
20
21
|
@Text()
|
|
21
22
|
name: string;
|
|
22
23
|
age?: number;
|
|
23
|
-
updatedDate?: Date;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
25
|
+
@PersistValue(() => new Date())
|
|
26
|
+
updatedDate?: Date;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
@Model()
|