@travetto/model 8.0.0-alpha.20 → 8.0.0-alpha.22
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/package.json +7 -7
- package/src/registry/decorator.ts +13 -0
- package/src/registry/registry-adapter.ts +4 -1
- package/src/registry/types.ts +4 -0
- package/src/util/crud.ts +66 -2
- package/support/test/basic.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Datastore abstraction for core operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
"directory": "module/model"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/di": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/registry": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.21",
|
|
31
|
+
"@travetto/di": "^8.0.0-alpha.19",
|
|
32
|
+
"@travetto/registry": "^8.0.0-alpha.19",
|
|
33
|
+
"@travetto/schema": "^8.0.0-alpha.21"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
37
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.27",
|
|
37
|
+
"@travetto/test": "^8.0.0-alpha.20"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@travetto/cli": {
|
|
@@ -89,6 +89,19 @@ export function Transient<T>() {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Prevent a field from being persisted
|
|
94
|
+
* @augments `@travetto/schema:Field`
|
|
95
|
+
* @kind decorator
|
|
96
|
+
*/
|
|
97
|
+
export function TransientField() {
|
|
98
|
+
return function <K extends string, C extends Partial<Record<K, unknown>>>(instance: C, property: K): void {
|
|
99
|
+
ModelRegistryIndex.getForRegister(getClass(instance)).register({
|
|
100
|
+
transientFields: [property]
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
/**
|
|
93
106
|
* Model class decorator for post-load behavior
|
|
94
107
|
* @augments `@travetto/schema:Schema`
|
|
@@ -10,6 +10,7 @@ function combineClasses(target: ModelConfig, sources: Partial<ModelConfig>[]): M
|
|
|
10
10
|
indices: { ...(target.indices || {}), ...(source.indices || {}) },
|
|
11
11
|
postLoad: [...(target.postLoad || []), ...(source.postLoad || [])],
|
|
12
12
|
prePersist: [...(target.prePersist || []), ...(source.prePersist || [])],
|
|
13
|
+
transientFields: [...(target.transientFields || []), ...(source.transientFields || [])],
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
if (target.store) {
|
|
@@ -33,7 +34,8 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
33
34
|
autoCreate: 'development',
|
|
34
35
|
store: this.#cls.name,
|
|
35
36
|
postLoad: [],
|
|
36
|
-
prePersist: []
|
|
37
|
+
prePersist: [],
|
|
38
|
+
transientFields: []
|
|
37
39
|
};
|
|
38
40
|
combineClasses(config, data);
|
|
39
41
|
return config;
|
|
@@ -50,6 +52,7 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
50
52
|
|
|
51
53
|
config.postLoad = [...parent.postLoad ?? [], ...config.postLoad ?? []];
|
|
52
54
|
config.prePersist = [...parent.prePersist ?? [], ...config.prePersist ?? []];
|
|
55
|
+
config.transientFields = [...parent.transientFields ?? [], ...config.transientFields ?? []];
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
package/src/registry/types.ts
CHANGED
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';
|
|
@@ -111,6 +111,70 @@ export class ModelCrudUtil {
|
|
|
111
111
|
return castTo(item);
|
|
112
112
|
}
|
|
113
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
|
+
|
|
114
178
|
/**
|
|
115
179
|
* Ensure subtype is not supported
|
|
116
180
|
*/
|
|
@@ -132,7 +196,7 @@ export class ModelCrudUtil {
|
|
|
132
196
|
item = await handler(item) ?? item;
|
|
133
197
|
}
|
|
134
198
|
}
|
|
135
|
-
return item;
|
|
199
|
+
return this.cleanTransientFields(cls, item);
|
|
136
200
|
}
|
|
137
201
|
|
|
138
202
|
/**
|
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
|
}
|