@travetto/model 5.0.14 → 5.0.16
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/LICENSE +1 -1
- package/package.json +7 -7
- package/src/error/not-found.ts +1 -1
- package/src/internal/service/crud.ts +7 -0
- package/src/internal/service/expiry.ts +0 -19
- package/src/internal/service/indexed.ts +3 -1
- package/src/registry/decorator.ts +4 -2
- package/src/registry/model.ts +2 -2
- package/support/test/basic.ts +0 -1
- package/support/test/blob.ts +0 -4
- package/support/test/crud.ts +4 -5
- package/support/test/expiry.ts +23 -22
- package/support/test/indexed.ts +0 -1
- package/support/test/suite.ts +9 -17
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.16",
|
|
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.
|
|
30
|
-
"@travetto/di": "^5.0.
|
|
31
|
-
"@travetto/registry": "^5.0.
|
|
32
|
-
"@travetto/schema": "^5.0.
|
|
29
|
+
"@travetto/config": "^5.0.15",
|
|
30
|
+
"@travetto/di": "^5.0.15",
|
|
31
|
+
"@travetto/registry": "^5.0.15",
|
|
32
|
+
"@travetto/schema": "^5.0.15"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^5.0.
|
|
36
|
-
"@travetto/test": "^5.0.
|
|
35
|
+
"@travetto/cli": "^5.0.18",
|
|
36
|
+
"@travetto/test": "^5.0.17"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
package/src/error/not-found.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Class, AppError } from '@travetto/runtime';
|
|
|
4
4
|
* Represents when a model of cls and id cannot be found
|
|
5
5
|
*/
|
|
6
6
|
export class NotFoundError extends AppError {
|
|
7
|
-
constructor(cls: Class | string, id: string, details
|
|
7
|
+
constructor(cls: Class | string, id: string, details: Record<string, unknown> = {}) {
|
|
8
8
|
super(`${typeof cls === 'string' ? cls : cls.name} with id ${id} not found`, { category: 'notfound', details });
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -142,6 +142,13 @@ export class ModelCrudUtil {
|
|
|
142
142
|
if (!DataUtil.isPlainObject(item)) {
|
|
143
143
|
throw new AppError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, { category: 'data' });
|
|
144
144
|
}
|
|
145
|
+
const keys = Object.keys(item);
|
|
146
|
+
if ((keys.length === 1 && item.id) || keys.length === 0) {
|
|
147
|
+
throw new AppError('No fields to update');
|
|
148
|
+
} else {
|
|
149
|
+
item = { ...item };
|
|
150
|
+
delete item.id;
|
|
151
|
+
}
|
|
145
152
|
const res = await this.prePersist(cls, castTo(item), 'partial');
|
|
146
153
|
await SchemaValidator.validatePartial(cls, item, view);
|
|
147
154
|
return res;
|
|
@@ -3,7 +3,6 @@ import { ShutdownManager, Class, TimeSpan, TimeUtil, Util, castTo } from '@trave
|
|
|
3
3
|
import { ModelRegistry } from '../../registry/model';
|
|
4
4
|
import { ModelExpirySupport } from '../../service/expiry';
|
|
5
5
|
import { ModelType } from '../../types/model';
|
|
6
|
-
import { NotFoundError } from '../../error/not-found';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Utils for model expiry
|
|
@@ -44,22 +43,4 @@ export class ModelExpiryUtil {
|
|
|
44
43
|
})();
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Simple cull operation for a given model type
|
|
50
|
-
* @param svc
|
|
51
|
-
*/
|
|
52
|
-
static async naiveDeleteExpired<T extends ModelType>(svc: ModelExpirySupport, cls: Class<T>, suppressErrors = false): Promise<number> {
|
|
53
|
-
const deleting = [];
|
|
54
|
-
for await (const el of svc.list(cls)) {
|
|
55
|
-
if (this.getExpiryState(cls, el).expired) {
|
|
56
|
-
deleting.push(svc.delete(cls, el.id).catch(err => {
|
|
57
|
-
if (!suppressErrors && !(err instanceof NotFoundError)) {
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return (await Promise.all(deleting)).length;
|
|
64
|
-
}
|
|
65
46
|
}
|
|
@@ -17,6 +17,8 @@ type ComputeConfig = {
|
|
|
17
17
|
type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
|
|
18
18
|
type IndexSortPart = { path: string[], dir: number, value: number | Date };
|
|
19
19
|
|
|
20
|
+
const DEFAULT_SEP = '\u8203';
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* Utils for working with indexed model services
|
|
22
24
|
*/
|
|
@@ -107,7 +109,7 @@ export class ModelIndexedUtil {
|
|
|
107
109
|
opts?: ComputeConfig & { sep?: string }
|
|
108
110
|
): { type: string, key: string, sort?: number | Date } {
|
|
109
111
|
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(opts ?? {}), includeSortInFields: false });
|
|
110
|
-
const key = fields.map(({ value }) => value).map(x => `${x}`).join(opts?.sep ??
|
|
112
|
+
const key = fields.map(({ value }) => value).map(x => `${x}`).join(opts?.sep ?? DEFAULT_SEP);
|
|
111
113
|
const cfg = typeof idx === 'string' ? ModelRegistry.getIndex(cls, idx) : idx;
|
|
112
114
|
return !sorted ? { type: cfg.type, key } : { type: cfg.type, key, sort: sorted.value };
|
|
113
115
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asConstructable, castTo, Class } from '@travetto/runtime';
|
|
1
|
+
import { AppError, asConstructable, castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistry } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
@@ -25,6 +25,9 @@ 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
|
+
if (indices.some(x => x.fields.some(f => f === 'id'))) {
|
|
29
|
+
throw new AppError('Cannot create an index with the id field');
|
|
30
|
+
}
|
|
28
31
|
return function (target: Class<T>): void {
|
|
29
32
|
ModelRegistry.getOrCreatePending(target).indices!.push(...indices);
|
|
30
33
|
};
|
|
@@ -71,7 +74,6 @@ export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PreP
|
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
|
|
75
77
|
/**
|
|
76
78
|
* Model class decorator for post-load behavior
|
|
77
79
|
*/
|
package/src/registry/model.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { SchemaRegistry } from '@travetto/schema';
|
|
|
2
2
|
import { MetadataRegistry } from '@travetto/registry';
|
|
3
3
|
import { DependencyRegistry } from '@travetto/di';
|
|
4
4
|
import { AppError, castTo, Class, describeFunction, asFull } from '@travetto/runtime';
|
|
5
|
-
import {
|
|
5
|
+
import { AllViewSymbol } from '@travetto/schema/src/internal/types';
|
|
6
6
|
|
|
7
7
|
import { IndexConfig, IndexType, ModelOptions } from './types';
|
|
8
8
|
import { NotFoundError } from '../error/not-found';
|
|
@@ -74,7 +74,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
74
74
|
const config = asFull(this.pending.get(cls.Ⲑid)!);
|
|
75
75
|
|
|
76
76
|
const schema = SchemaRegistry.get(cls);
|
|
77
|
-
const view = schema.views[
|
|
77
|
+
const view = schema.views[AllViewSymbol].schema;
|
|
78
78
|
delete view.id.required; // Allow ids to be optional
|
|
79
79
|
|
|
80
80
|
if (schema.subTypeField in view && this.getBaseModel(cls) !== cls) {
|
package/support/test/basic.ts
CHANGED
package/support/test/blob.ts
CHANGED
|
@@ -119,7 +119,6 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
119
119
|
await assert.rejects(() => service.getBlob(id, { start: 30, end: 37 }));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
123
122
|
@Test()
|
|
124
123
|
async writeAndGet() {
|
|
125
124
|
const service = await this.service;
|
|
@@ -134,7 +133,6 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
134
133
|
assert(undefined === savedMeta.hash);
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
136
|
@Test()
|
|
139
137
|
async metadataUpdate() {
|
|
140
138
|
const service = await this.service;
|
|
@@ -153,13 +151,11 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
153
151
|
assert(undefined === savedMeta.hash);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
|
-
|
|
157
154
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
158
155
|
@Test({ skip: (x: unknown) => !(x as ModelBlobSuite).serviceClass.prototype.getBlobWriteUrl })
|
|
159
156
|
async signedUrl() {
|
|
160
157
|
const service = await this.service;
|
|
161
158
|
|
|
162
|
-
|
|
163
159
|
const buffer = Buffer.alloc(1.5 * 10000);
|
|
164
160
|
for (let i = 0; i < buffer.length; i++) {
|
|
165
161
|
buffer.writeUInt8(Math.trunc(Math.random() * 255), i);
|
package/support/test/crud.ts
CHANGED
|
@@ -34,7 +34,6 @@ class SimpleItem {
|
|
|
34
34
|
name: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
@Model()
|
|
39
38
|
class SimpleList {
|
|
40
39
|
id: string;
|
|
@@ -57,6 +56,8 @@ class User2 {
|
|
|
57
56
|
class Dated {
|
|
58
57
|
id: string;
|
|
59
58
|
|
|
59
|
+
value?: string;
|
|
60
|
+
|
|
60
61
|
@PersistValue(v => v ?? new Date(), 'full')
|
|
61
62
|
@Required(false)
|
|
62
63
|
createdDate: Date;
|
|
@@ -154,7 +155,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
154
155
|
@Test('Verify update partial on missing item fails')
|
|
155
156
|
async testMissingUpdatePartial() {
|
|
156
157
|
const service = await this.service;
|
|
157
|
-
await assert.rejects(() => service.updatePartial(User2, { id: '-1' }), NotFoundError);
|
|
158
|
+
await assert.rejects(() => service.updatePartial(User2, { id: '-1', name: 'bob' }), NotFoundError);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
@Test('Verify partial update with field removal and lists')
|
|
@@ -227,14 +228,13 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
227
228
|
|
|
228
229
|
await timers.setTimeout(100);
|
|
229
230
|
|
|
230
|
-
const final = await service.updatePartial(Dated, { id: res.id });
|
|
231
|
+
const final = await service.updatePartial(Dated, { id: res.id, value: 'random' });
|
|
231
232
|
assert(final.createdDate instanceof Date);
|
|
232
233
|
assert(final.createdDate.getTime() === created?.getTime());
|
|
233
234
|
assert(final.updatedDate instanceof Date);
|
|
234
235
|
assert(final.createdDate.getTime() < final.updatedDate?.getTime());
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
|
|
238
238
|
@Test('verify list')
|
|
239
239
|
async list() {
|
|
240
240
|
const service = await this.service;
|
|
@@ -282,7 +282,6 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
282
282
|
assert(single.age === 23);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
|
|
286
285
|
@Test('Verify update')
|
|
287
286
|
async testRawUpdate() {
|
|
288
287
|
const service = await this.service;
|
package/support/test/expiry.ts
CHANGED
|
@@ -11,10 +11,11 @@ import { NotFoundError } from '../../src/error/not-found';
|
|
|
11
11
|
import { BaseModelSuite } from './base';
|
|
12
12
|
|
|
13
13
|
@Model('expiry-user')
|
|
14
|
-
class
|
|
14
|
+
export class ExpiryUser {
|
|
15
15
|
id: string;
|
|
16
16
|
@ExpiresAt()
|
|
17
17
|
expiresAt?: Date;
|
|
18
|
+
payload?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
@Suite()
|
|
@@ -33,63 +34,63 @@ export abstract class ModelExpirySuite extends BaseModelSuite<ModelExpirySupport
|
|
|
33
34
|
@Test()
|
|
34
35
|
async basic() {
|
|
35
36
|
const service = await this.service;
|
|
36
|
-
const res = await service.upsert(
|
|
37
|
+
const res = await service.upsert(ExpiryUser, ExpiryUser.from({
|
|
37
38
|
expiresAt: this.timeFromNow('2s')
|
|
38
39
|
}));
|
|
39
|
-
assert(res instanceof
|
|
40
|
+
assert(res instanceof ExpiryUser);
|
|
40
41
|
|
|
41
|
-
const expiry = ModelExpiryUtil.getExpiryState(
|
|
42
|
+
const expiry = ModelExpiryUtil.getExpiryState(ExpiryUser, await service.get(ExpiryUser, res.id));
|
|
42
43
|
assert(!expiry.expired);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
@Test()
|
|
46
47
|
async aging() {
|
|
47
48
|
const service = await this.service;
|
|
48
|
-
const res = await service.upsert(
|
|
49
|
+
const res = await service.upsert(ExpiryUser, ExpiryUser.from({
|
|
49
50
|
expiresAt: this.timeFromNow(100)
|
|
50
51
|
}));
|
|
51
52
|
|
|
52
|
-
assert(res instanceof
|
|
53
|
+
assert(res instanceof ExpiryUser);
|
|
53
54
|
|
|
54
55
|
await this.wait(200);
|
|
55
56
|
|
|
56
|
-
await assert.rejects(() => service.get(
|
|
57
|
+
await assert.rejects(() => service.get(ExpiryUser, res.id), NotFoundError);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
@Test()
|
|
60
61
|
async updateExpired() {
|
|
61
62
|
const service = await this.service;
|
|
62
|
-
const res = await service.upsert(
|
|
63
|
+
const res = await service.upsert(ExpiryUser, ExpiryUser.from({
|
|
63
64
|
expiresAt: this.timeFromNow(100)
|
|
64
65
|
}));
|
|
65
66
|
|
|
66
|
-
assert(res instanceof
|
|
67
|
+
assert(res instanceof ExpiryUser);
|
|
67
68
|
|
|
68
69
|
await this.wait(200);
|
|
69
70
|
|
|
70
|
-
await assert.rejects(() => service.update(
|
|
71
|
+
await assert.rejects(() => service.update(ExpiryUser, ExpiryUser.from({ id: res.id })), NotFoundError);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
@Test()
|
|
74
75
|
async ageWithExtension() {
|
|
75
76
|
const service = await this.service;
|
|
76
|
-
const res = await service.upsert(
|
|
77
|
+
const res = await service.upsert(ExpiryUser, ExpiryUser.from({
|
|
77
78
|
expiresAt: this.timeFromNow('2s')
|
|
78
79
|
}));
|
|
79
|
-
assert(res instanceof
|
|
80
|
+
assert(res instanceof ExpiryUser);
|
|
80
81
|
|
|
81
82
|
await this.wait(50);
|
|
82
83
|
|
|
83
|
-
assert(!ModelExpiryUtil.getExpiryState(
|
|
84
|
+
assert(!ModelExpiryUtil.getExpiryState(ExpiryUser, (await service.get(ExpiryUser, res.id))).expired);
|
|
84
85
|
|
|
85
|
-
await service.updatePartial(
|
|
86
|
+
await service.updatePartial(ExpiryUser, {
|
|
86
87
|
id: res.id,
|
|
87
88
|
expiresAt: this.timeFromNow(100)
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
await this.wait(200);
|
|
91
92
|
|
|
92
|
-
await assert.rejects(() => service.get(
|
|
93
|
+
await assert.rejects(() => service.get(ExpiryUser, res.id), NotFoundError);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
@Test()
|
|
@@ -98,29 +99,29 @@ export abstract class ModelExpirySuite extends BaseModelSuite<ModelExpirySupport
|
|
|
98
99
|
|
|
99
100
|
let total;
|
|
100
101
|
|
|
101
|
-
total = await this.getSize(
|
|
102
|
+
total = await this.getSize(ExpiryUser);
|
|
102
103
|
assert(total === 0);
|
|
103
104
|
|
|
104
105
|
// Create
|
|
105
106
|
await Promise.all(
|
|
106
|
-
Array(10).fill(0).map((x, i) => service.upsert(
|
|
107
|
-
expiresAt: this.timeFromNow(
|
|
107
|
+
Array(10).fill(0).map((x, i) => service.upsert(ExpiryUser, ExpiryUser.from({
|
|
108
|
+
expiresAt: this.timeFromNow(300 + i * this.delayFactor)
|
|
108
109
|
})))
|
|
109
110
|
);
|
|
110
111
|
|
|
111
112
|
// Let expire
|
|
112
113
|
await this.wait(1);
|
|
113
114
|
|
|
114
|
-
total = await this.getSize(
|
|
115
|
+
total = await this.getSize(ExpiryUser);
|
|
115
116
|
assert(total === 10);
|
|
116
117
|
|
|
117
118
|
// Let expire
|
|
118
|
-
await this.wait(
|
|
119
|
+
await this.wait(400);
|
|
119
120
|
|
|
120
|
-
total = await this.getSize(
|
|
121
|
+
total = await this.getSize(ExpiryUser);
|
|
121
122
|
assert(total === 0);
|
|
122
123
|
|
|
123
|
-
total = await this.getSize(
|
|
124
|
+
total = await this.getSize(ExpiryUser);
|
|
124
125
|
assert(total === 0);
|
|
125
126
|
}
|
|
126
127
|
}
|
package/support/test/indexed.ts
CHANGED
|
@@ -111,7 +111,6 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
|
|
|
111
111
|
await assert.rejects(() => service.getByIndex(User3, 'userAge', { name: 'bob' }));
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
114
|
@Test()
|
|
116
115
|
async queryList() {
|
|
117
116
|
const service = await this.service;
|
package/support/test/suite.ts
CHANGED
|
@@ -51,23 +51,15 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
|
51
51
|
async function (this: T) {
|
|
52
52
|
const service = await DependencyRegistry.getInstance(this.serviceClass, qualifier);
|
|
53
53
|
if (isStorageSupported(service)) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
if (isBlobSupported(service)) {
|
|
65
|
-
if (service.truncateModel) {
|
|
66
|
-
await service.truncateModel(MODEL_BLOB);
|
|
67
|
-
} else if (service.deleteModel) {
|
|
68
|
-
await service.deleteModel(MODEL_BLOB);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
54
|
+
const models = ModelRegistry.getClasses().filter(m => m === ModelRegistry.getBaseModel(m));
|
|
55
|
+
if (isBlobSupported(service)) {
|
|
56
|
+
models.push(MODEL_BLOB);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (service.truncateModel) {
|
|
60
|
+
await Promise.all(models.map(x => service.truncateModel!(x)));
|
|
61
|
+
} else if (service.deleteModel) {
|
|
62
|
+
await Promise.all(models.map(x => service.deleteModel!(x)));
|
|
71
63
|
} else {
|
|
72
64
|
await service.deleteStorage(); // Purge it all
|
|
73
65
|
}
|