@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 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
- All fields are optional, but the `id` and `type` are important as those field types are unable to be changed. 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 and cannot 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.").
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",
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.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.3"
32
+ "@travetto/schema": "^3.3.4"
33
33
  },
34
34
  "peerDependencies": {
35
- "@travetto/cli": "^3.3.4",
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
- if (result.postLoad) {
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
- if (item.prePersist) {
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
- if (item.prePersist) {
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
  }
@@ -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
  }
@@ -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
  /**
@@ -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 };
@@ -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,
@@ -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', User4.from({ child: { name: 'bob' } })));
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
- type: string;
19
+ @SubTypeField()
20
+ _type: string;
20
21
  @Text()
21
22
  name: string;
22
23
  age?: number;
23
- updatedDate?: Date;
24
24
 
25
- prePersist() {
26
- this.updatedDate = new Date();
27
- }
25
+ @PersistValue(() => new Date())
26
+ updatedDate?: Date;
28
27
  }
29
28
 
30
29
  @Model()