@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 ArcSine Technologies
3
+ Copyright (c) 2020 ArcSine Technologies
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model",
3
- "version": "5.0.14",
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.13",
30
- "@travetto/di": "^5.0.13",
31
- "@travetto/registry": "^5.0.13",
32
- "@travetto/schema": "^5.0.13"
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.16",
36
- "@travetto/test": "^5.0.15"
35
+ "@travetto/cli": "^5.0.18",
36
+ "@travetto/test": "^5.0.17"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
@@ -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?: Record<string, unknown>) {
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
  */
@@ -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 { AllViewⲐ } from '@travetto/schema/src/internal/types';
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[AllViewⲐ].schema;
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) {
@@ -44,7 +44,6 @@ export abstract class ModelBasicSuite extends BaseModelSuite<ModelCrudSupport> {
44
44
  }, NotFoundError);
45
45
  }
46
46
 
47
-
48
47
  @Test('create, read, delete')
49
48
  async createRaw() {
50
49
  const service = await this.service;
@@ -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);
@@ -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;
@@ -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 User {
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(User, User.from({
37
+ const res = await service.upsert(ExpiryUser, ExpiryUser.from({
37
38
  expiresAt: this.timeFromNow('2s')
38
39
  }));
39
- assert(res instanceof User);
40
+ assert(res instanceof ExpiryUser);
40
41
 
41
- const expiry = ModelExpiryUtil.getExpiryState(User, await service.get(User, res.id));
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(User, User.from({
49
+ const res = await service.upsert(ExpiryUser, ExpiryUser.from({
49
50
  expiresAt: this.timeFromNow(100)
50
51
  }));
51
52
 
52
- assert(res instanceof User);
53
+ assert(res instanceof ExpiryUser);
53
54
 
54
55
  await this.wait(200);
55
56
 
56
- await assert.rejects(() => service.get(User, res.id), NotFoundError);
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(User, User.from({
63
+ const res = await service.upsert(ExpiryUser, ExpiryUser.from({
63
64
  expiresAt: this.timeFromNow(100)
64
65
  }));
65
66
 
66
- assert(res instanceof User);
67
+ assert(res instanceof ExpiryUser);
67
68
 
68
69
  await this.wait(200);
69
70
 
70
- await assert.rejects(() => service.update(User, User.from({ id: res.id })), NotFoundError);
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(User, User.from({
77
+ const res = await service.upsert(ExpiryUser, ExpiryUser.from({
77
78
  expiresAt: this.timeFromNow('2s')
78
79
  }));
79
- assert(res instanceof User);
80
+ assert(res instanceof ExpiryUser);
80
81
 
81
82
  await this.wait(50);
82
83
 
83
- assert(!ModelExpiryUtil.getExpiryState(User, (await service.get(User, res.id))).expired);
84
+ assert(!ModelExpiryUtil.getExpiryState(ExpiryUser, (await service.get(ExpiryUser, res.id))).expired);
84
85
 
85
- await service.updatePartial(User, {
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(User, res.id), NotFoundError);
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(User);
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(User, User.from({
107
- expiresAt: this.timeFromNow(1000 + i * this.delayFactor)
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(User);
115
+ total = await this.getSize(ExpiryUser);
115
116
  assert(total === 10);
116
117
 
117
118
  // Let expire
118
- await this.wait(1100);
119
+ await this.wait(400);
119
120
 
120
- total = await this.getSize(User);
121
+ total = await this.getSize(ExpiryUser);
121
122
  assert(total === 0);
122
123
 
123
- total = await this.getSize(User);
124
+ total = await this.getSize(ExpiryUser);
124
125
  assert(total === 0);
125
126
  }
126
127
  }
@@ -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;
@@ -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
- if (service.truncateModel || service.deleteModel) {
55
- for (const m of ModelRegistry.getClasses()) {
56
- if (m === ModelRegistry.getBaseModel(m)) {
57
- if (service.truncateModel) {
58
- await service.truncateModel(m);
59
- } else if (service.deleteModel) {
60
- await service.deleteModel(m);
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
  }