@travetto/model 2.0.2 → 2.1.1
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 +4 -4
- package/package.json +6 -6
- package/src/internal/service/crud.ts +14 -4
- package/src/provider/file.ts +18 -22
- package/src/registry/decorator.ts +0 -29
- package/src/registry/model.ts +10 -3
- package/src/registry/types.ts +2 -2
- package/test-support/polymorphism.ts +2 -3
- package/test-support/stream.ts +5 -4
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ export interface ModelBulkSupport extends ModelCrudSupport {
|
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
## Declaration
|
|
199
|
-
Models are declared via the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
199
|
+
Models are declared via the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L12) decorator, which allows the system to know that this is a class that is compatible with the module. The only requirement for a model is the [ModelType](https://github.com/travetto/travetto/tree/main/module/model/src/types/model.ts#L4)
|
|
200
200
|
|
|
201
201
|
**Code: ModelType**
|
|
202
202
|
```typescript
|
|
@@ -236,7 +236,7 @@ All fields are optional, but the `id` and `type` are important as those field ty
|
|
|
236
236
|
|[S3 Model Support](https://github.com/travetto/travetto/tree/main/module/model-s3#readme "S3 backing for the travetto model module.")|X|X| |X|X| |
|
|
237
237
|
|[SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.")|X|X|X|X| |X|
|
|
238
238
|
|[MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L50)|X|X|X|X|X|X|
|
|
239
|
-
|[FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#
|
|
239
|
+
|[FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#L47)|X|X| |X|X|X|
|
|
240
240
|
|
|
241
241
|
## Custom Model Service
|
|
242
242
|
In addition to the provided contracts, the module also provides common utilities and shared test suites. The common utilities are useful for
|
|
@@ -375,7 +375,7 @@ export class MemoryPolymorphicSuite extends ModelPolymorphismSuite {
|
|
|
375
375
|
|
|
376
376
|
## CLI - model:export
|
|
377
377
|
|
|
378
|
-
The module provides the ability to generate an export of the model structure from all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
378
|
+
The module provides the ability to generate an export of the model structure from all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L12)s within the application. This is useful for being able to generate the appropriate files to manually create the data schemas in production.
|
|
379
379
|
|
|
380
380
|
**Terminal: Running model export**
|
|
381
381
|
```bash
|
|
@@ -390,7 +390,7 @@ Options:
|
|
|
390
390
|
|
|
391
391
|
## CLI - model:install
|
|
392
392
|
|
|
393
|
-
The module provides the ability to install all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
393
|
+
The module provides the ability to install all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L12)s within the application given the current configuration being targetted. This is useful for being able to prepare the datastore manually.
|
|
394
394
|
|
|
395
395
|
**Terminal: Running model install**
|
|
396
396
|
```bash
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
3
|
"displayName": "Data Modeling Support",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.1",
|
|
5
5
|
"description": "Datastore abstraction for core operations.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"datastore",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"directory": "module/model"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/di": "^2.
|
|
32
|
-
"@travetto/config": "^2.
|
|
33
|
-
"@travetto/registry": "^2.
|
|
34
|
-
"@travetto/schema": "^2.
|
|
31
|
+
"@travetto/di": "^2.1.1",
|
|
32
|
+
"@travetto/config": "^2.1.1",
|
|
33
|
+
"@travetto/registry": "^2.1.1",
|
|
34
|
+
"@travetto/schema": "^2.1.1"
|
|
35
35
|
},
|
|
36
36
|
"optionalPeerDependencies": {
|
|
37
|
-
"@travetto/cli": "^2.
|
|
37
|
+
"@travetto/cli": "^2.1.1"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
|
|
3
3
|
import { Class, Util } from '@travetto/base';
|
|
4
|
-
import { SchemaValidator } from '@travetto/schema';
|
|
4
|
+
import { SchemaRegistry, SchemaValidator } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import { ModelRegistry } from '../../registry/model';
|
|
7
7
|
import { ModelType, OptionalId } from '../../types/model';
|
|
8
8
|
import { NotFoundError } from '../../error/not-found';
|
|
9
9
|
import { ExistsError } from '../../error/exists';
|
|
10
|
+
import { SubTypeNotSupportedError } from '../../error/invalid-sub-type';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Crud utilities
|
|
@@ -39,7 +40,7 @@ export class ModelCrudUtil {
|
|
|
39
40
|
|
|
40
41
|
const result = ModelRegistry.getBaseModel(cls).from(input as object) as T;
|
|
41
42
|
|
|
42
|
-
if (!(result instanceof cls)) {
|
|
43
|
+
if (!(result instanceof cls || result.constructor.ᚕid === cls.ᚕid)) {
|
|
43
44
|
if (onTypeMismatch === 'notfound') {
|
|
44
45
|
throw new NotFoundError(cls, result.id);
|
|
45
46
|
} else {
|
|
@@ -70,7 +71,7 @@ export class ModelCrudUtil {
|
|
|
70
71
|
|
|
71
72
|
const config = ModelRegistry.get(item.constructor as Class<T>);
|
|
72
73
|
if (config.subType) { // Subtyping, assign type
|
|
73
|
-
|
|
74
|
+
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
await SchemaValidator.validate(cls, item);
|
|
@@ -96,7 +97,7 @@ export class ModelCrudUtil {
|
|
|
96
97
|
|
|
97
98
|
const config = ModelRegistry.get(item.constructor as Class<T>);
|
|
98
99
|
if (config.subType) { // Subtyping, assign type
|
|
99
|
-
|
|
100
|
+
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
if (view) {
|
|
@@ -113,4 +114,13 @@ export class ModelCrudUtil {
|
|
|
113
114
|
|
|
114
115
|
return item as T;
|
|
115
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Ensure subtype is not supported
|
|
120
|
+
*/
|
|
121
|
+
static ensureNotSubType(cls: Class) {
|
|
122
|
+
if (ModelRegistry.get(cls).subType) {
|
|
123
|
+
throw new SubTypeNotSupportedError(cls);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
116
126
|
}
|
package/src/provider/file.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import { createReadStream } from 'fs';
|
|
2
3
|
import * as os from 'os';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
|
|
@@ -18,7 +19,6 @@ import { ModelCrudUtil } from '../internal/service/crud';
|
|
|
18
19
|
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
19
20
|
import { NotFoundError } from '../error/not-found';
|
|
20
21
|
import { ExistsError } from '../error/exists';
|
|
21
|
-
import { SubTypeNotSupportedError } from '../error/invalid-sub-type';
|
|
22
22
|
import { StreamModel, STREAMS } from '../internal/service/stream';
|
|
23
23
|
|
|
24
24
|
type Suffix = '.bin' | '.meta' | '.json' | '.expires';
|
|
@@ -48,8 +48,8 @@ export class FileModelConfig {
|
|
|
48
48
|
export class FileModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport {
|
|
49
49
|
|
|
50
50
|
private static async * scanFolder(folder: string, suffix: string) {
|
|
51
|
-
for (const sub of await fs.
|
|
52
|
-
for (const file of await fs.
|
|
51
|
+
for (const sub of await fs.readdir(folder)) {
|
|
52
|
+
for (const file of await fs.readdir(PathUtil.resolveUnix(folder, sub))) {
|
|
53
53
|
if (file.endsWith(suffix)) {
|
|
54
54
|
yield [file.replace(suffix, ''), PathUtil.resolveUnix(folder, sub, file)] as [id: string, file: string];
|
|
55
55
|
}
|
|
@@ -80,7 +80,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
80
80
|
dir = path.dirname(resolved);
|
|
81
81
|
}
|
|
82
82
|
if (!await FsUtil.exists(dir)) {
|
|
83
|
-
await fs.
|
|
83
|
+
await fs.mkdir(dir, { recursive: true });
|
|
84
84
|
}
|
|
85
85
|
return resolved;
|
|
86
86
|
}
|
|
@@ -115,7 +115,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
115
115
|
const file = await this.#resolveName(cls, '.json', id);
|
|
116
116
|
|
|
117
117
|
if (await FsUtil.exists(file)) {
|
|
118
|
-
const content = await StreamUtil.streamToBuffer(
|
|
118
|
+
const content = await StreamUtil.streamToBuffer(createReadStream(file));
|
|
119
119
|
return this.checkExpiry(cls, await ModelCrudUtil.load(cls, content));
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -130,7 +130,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
130
130
|
const file = await this.#resolveName(cls, '.json', item.id);
|
|
131
131
|
|
|
132
132
|
if (await FsUtil.exists(file)) {
|
|
133
|
-
throw new ExistsError(cls, item.id);
|
|
133
|
+
throw new ExistsError(cls, item.id!);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
return await this.upsert(cls, item);
|
|
@@ -142,32 +142,28 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
145
|
-
|
|
146
|
-
throw new SubTypeNotSupportedError(cls);
|
|
147
|
-
}
|
|
145
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
148
146
|
const prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
149
147
|
|
|
150
148
|
const file = await this.#resolveName(cls, '.json', item.id);
|
|
151
|
-
await fs.
|
|
149
|
+
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
152
150
|
|
|
153
151
|
return prepped;
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
157
|
-
|
|
158
|
-
throw new SubTypeNotSupportedError(cls);
|
|
159
|
-
}
|
|
155
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
160
156
|
const id = item.id;
|
|
161
157
|
item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id));
|
|
162
158
|
const file = await this.#resolveName(cls, '.json', item.id);
|
|
163
|
-
await fs.
|
|
159
|
+
await fs.writeFile(file, JSON.stringify(item), { encoding: 'utf8' });
|
|
164
160
|
|
|
165
161
|
return item as T;
|
|
166
162
|
}
|
|
167
163
|
|
|
168
164
|
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
169
165
|
const file = await this.#find(cls, '.json', id);
|
|
170
|
-
await fs.
|
|
166
|
+
await fs.unlink(file);
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
async * list<T extends ModelType>(cls: Class<T>) {
|
|
@@ -187,18 +183,18 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
187
183
|
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
188
184
|
await Promise.all([
|
|
189
185
|
StreamUtil.writeToFile(input, file),
|
|
190
|
-
fs.
|
|
186
|
+
fs.writeFile(file.replace(BIN, META), JSON.stringify(meta), 'utf8')
|
|
191
187
|
]);
|
|
192
188
|
}
|
|
193
189
|
|
|
194
190
|
async getStream(location: string) {
|
|
195
191
|
const file = await this.#find(STREAMS, BIN, location);
|
|
196
|
-
return
|
|
192
|
+
return createReadStream(file);
|
|
197
193
|
}
|
|
198
194
|
|
|
199
195
|
async describeStream(location: string) {
|
|
200
196
|
const file = await this.#find(STREAMS, META, location);
|
|
201
|
-
const content = await StreamUtil.streamToBuffer(
|
|
197
|
+
const content = await StreamUtil.streamToBuffer(createReadStream(file));
|
|
202
198
|
const text = JSON.parse(content.toString('utf8'));
|
|
203
199
|
return text as StreamMeta;
|
|
204
200
|
}
|
|
@@ -207,8 +203,8 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
207
203
|
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
208
204
|
if (await FsUtil.exists(file)) {
|
|
209
205
|
await Promise.all([
|
|
210
|
-
fs.
|
|
211
|
-
fs.
|
|
206
|
+
fs.unlink(file),
|
|
207
|
+
fs.unlink(file.replace('.bin', META))
|
|
212
208
|
]);
|
|
213
209
|
} else {
|
|
214
210
|
throw new NotFoundError('Stream', location);
|
|
@@ -229,7 +225,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
229
225
|
// Storage mgmt
|
|
230
226
|
async createStorage() {
|
|
231
227
|
const dir = PathUtil.resolveUnix(this.config.folder, this.config.namespace);
|
|
232
|
-
await fs.
|
|
228
|
+
await fs.mkdir(dir, { recursive: true });
|
|
233
229
|
}
|
|
234
230
|
|
|
235
231
|
async deleteStorage() {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Class } from '@travetto/base';
|
|
2
|
-
import { SchemaRegistry } from '@travetto/schema';
|
|
3
2
|
|
|
4
3
|
import { ModelType } from '../types/model';
|
|
5
4
|
import { ModelRegistry } from './model';
|
|
@@ -15,39 +14,11 @@ export function Model(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
|
15
14
|
if (typeof conf === 'string') {
|
|
16
15
|
conf = { store: conf };
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
// Force registry first, and update with extra information after computing
|
|
20
|
-
ModelRegistry.register(target, conf);
|
|
21
|
-
|
|
22
|
-
const baseModel = ModelRegistry.getBaseModel(target);
|
|
23
|
-
if (baseModel !== target) { // Subtyping if base isn't self
|
|
24
|
-
if (conf.subType) {
|
|
25
|
-
SchemaRegistry.registerSubTypes(target, conf.subType);
|
|
26
|
-
} else {
|
|
27
|
-
conf.subType = SchemaRegistry.getSubTypeName(target);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
17
|
ModelRegistry.register(target, conf);
|
|
31
18
|
return target;
|
|
32
19
|
};
|
|
33
20
|
}
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Base Model decorator, extends `@Schema`
|
|
38
|
-
*
|
|
39
|
-
* @augments `@trv:schema/Schema`
|
|
40
|
-
*/
|
|
41
|
-
export function BaseModel(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
42
|
-
return function <T extends ModelType & { type: string }, U extends Class<T>>(target: U): U {
|
|
43
|
-
ModelRegistry.register(target, {
|
|
44
|
-
baseType: true,
|
|
45
|
-
...(typeof conf === 'string' ? { store: conf } : conf)
|
|
46
|
-
});
|
|
47
|
-
return target;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
22
|
/**
|
|
52
23
|
* Defines an index on a model
|
|
53
24
|
*/
|
package/src/registry/model.ts
CHANGED
|
@@ -51,12 +51,20 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
createPending(cls: Class): Partial<ModelOptions<ModelType>> {
|
|
54
|
-
return { class: cls, indices: [], autoCreate: true };
|
|
54
|
+
return { class: cls, indices: [], autoCreate: true, baseType: cls.ᚕabstract };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
onInstallFinalize(cls: Class) {
|
|
58
58
|
const config = this.pending.get(cls.ᚕid)! as ModelOptions<ModelType>;
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
const schema = SchemaRegistry.get(cls);
|
|
61
|
+
const view = schema.views[AllViewⲐ].schema;
|
|
62
|
+
delete view.id.required; // Allow ids to be optional
|
|
63
|
+
|
|
64
|
+
if ('type' in view && this.getBaseModel(cls) !== cls) {
|
|
65
|
+
config.subType = schema.subType; // Copy from schema
|
|
66
|
+
delete view.type.required; // Allow type to be optional
|
|
67
|
+
}
|
|
60
68
|
return config;
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -128,7 +136,6 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
128
136
|
return this.getStore(this.getBaseModel(cls));
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
|
|
132
139
|
const name = config.store ?? cls.name.toLowerCase();
|
|
133
140
|
|
|
134
141
|
const candidates = this.getInitialNameMapping().get(name) || [];
|
package/src/registry/types.ts
CHANGED
|
@@ -10,9 +10,8 @@ import {
|
|
|
10
10
|
} from '..';
|
|
11
11
|
import { isIndexedSupported } from '../src/internal/service/common';
|
|
12
12
|
import { ExistsError } from '../src/error/exists';
|
|
13
|
-
import { BaseModel } from '../src/registry/decorator';
|
|
14
13
|
|
|
15
|
-
@
|
|
14
|
+
@Model({ baseType: true })
|
|
16
15
|
export class Worker {
|
|
17
16
|
id: string;
|
|
18
17
|
type: string;
|
|
@@ -41,7 +40,7 @@ export class Engineer extends Worker {
|
|
|
41
40
|
major: string;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
@
|
|
43
|
+
@Model({ baseType: true })
|
|
45
44
|
@Index({
|
|
46
45
|
name: 'worker-name',
|
|
47
46
|
type: 'sorted',
|
package/test-support/stream.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as assert from 'assert';
|
|
2
|
-
import * as fs from 'fs';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { createReadStream } from 'fs';
|
|
3
4
|
import * as crypto from 'crypto';
|
|
4
5
|
|
|
5
6
|
import { PathUtil } from '@travetto/boot';
|
|
@@ -25,12 +26,12 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
|
|
|
25
26
|
|
|
26
27
|
async getStream(resource: string) {
|
|
27
28
|
const file = await ResourceManager.findAbsolute(resource);
|
|
28
|
-
const stat = await fs.
|
|
29
|
-
const hash = await this.getHash(
|
|
29
|
+
const stat = await fs.stat(file);
|
|
30
|
+
const hash = await this.getHash(createReadStream(file));
|
|
30
31
|
|
|
31
32
|
return [
|
|
32
33
|
{ size: stat.size, contentType: '', hash, filename: resource },
|
|
33
|
-
|
|
34
|
+
createReadStream(file)
|
|
34
35
|
] as const;
|
|
35
36
|
}
|
|
36
37
|
|