@travetto/model 3.1.5 → 3.1.7

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
@@ -79,7 +79,11 @@ export interface ModelCrudSupport extends ModelBasicSupport {
79
79
  upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T>;
80
80
 
81
81
  /**
82
- * Update Partial
82
+ * Update partial, respecting only top level keys.
83
+ *
84
+ * When invoking this method, any top level keys that are null/undefined are treated as removals/deletes. Any properties
85
+ * that point to sub objects/arrays are treated as wholesale replacements.
86
+ *
83
87
  * @param id The document identifier to update
84
88
  * @param item The document to partially update.
85
89
  * @param view The schema view to validate against
@@ -170,6 +174,12 @@ export interface ModelStreamSupport {
170
174
  */
171
175
  getStream(location: string): Promise<Readable>;
172
176
 
177
+ /**
178
+ * Get partial stream from asset store given a starting byte and an optional ending byte
179
+ * @param location The location of the stream
180
+ */
181
+ getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
182
+
173
183
  /**
174
184
  * Get metadata for stream
175
185
  * @param location The location of the stream
@@ -247,7 +257,7 @@ import { DeepPartial } from '@travetto/schema';
247
257
  import { Injectable } from '@travetto/di';
248
258
  import { Config } from '@travetto/config';
249
259
  import { ModelCrudSupport } from '../service/crud';
250
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
260
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
251
261
  import { ModelType, OptionalId } from '../types/model';
252
262
  import { ModelExpirySupport } from '../service/expiry';
253
263
  import { ModelRegistry } from '../registry/model';
@@ -259,7 +269,7 @@ import { ExistsError } from '../error/exists';
259
269
  import { ModelIndexedSupport } from '../service/indexed';
260
270
  import { ModelIndexedUtil } from '../internal/service/indexed';
261
271
  import { ModelStorageUtil } from '../internal/service/storage';
262
- import { StreamModel, STREAMS } from '../internal/service/stream';
272
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
263
273
  import { IndexConfig } from '../registry/types';
264
274
  const STREAM_META = `${STREAMS}_meta`;
265
275
  type StoreType = Map<string, Buffer>;
@@ -304,6 +314,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
304
314
  // Stream Support
305
315
  async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
306
316
  async getStream(location: string): Promise<Readable>;
317
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
307
318
  async describeStream(location: string): Promise<StreamMeta>;
308
319
  async deleteStream(location: string): Promise<void>;
309
320
  // Expiry Support
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model",
3
- "version": "3.1.5",
3
+ "version": "3.1.7",
4
4
  "description": "Datastore abstraction for core operations.",
5
5
  "keywords": [
6
6
  "datastore",
@@ -3,6 +3,7 @@ import { ShutdownManager, Class, TimeSpan, TimeUtil } from '@travetto/base';
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';
6
7
 
7
8
  /**
8
9
  * Utils for model expiry
@@ -50,4 +51,22 @@ export class ModelExpiryUtil {
50
51
  })();
51
52
  }
52
53
  }
54
+
55
+ /**
56
+ * Simple cull operation for a given model type
57
+ * @param svc
58
+ */
59
+ static async naiveDeleteExpired<T extends ModelType>(svc: ModelExpirySupport, cls: Class<T>, suppressErrors = false): Promise<number> {
60
+ const deleting = [];
61
+ for await (const el of svc.list(cls)) {
62
+ if (this.getExpiryState(cls, el).expired) {
63
+ deleting.push(svc.delete(cls, el.id).catch(err => {
64
+ if (!suppressErrors && !(err instanceof NotFoundError)) {
65
+ throw err;
66
+ }
67
+ }));
68
+ }
69
+ }
70
+ return (await Promise.all(deleting)).length;
71
+ }
53
72
  }
@@ -1,6 +1,14 @@
1
- import { Class } from '@travetto/base';
1
+ import { AppError, Class } from '@travetto/base';
2
2
  import { ModelType } from '../../types/model';
3
3
 
4
4
  class Cls { id: string; }
5
5
  export const StreamModel: Class<ModelType> = Cls;
6
- export const STREAMS = '_streams';
6
+ export const STREAMS = '_streams';
7
+
8
+ export class ModelStreamUtil {
9
+ static checkRange(start: number, end: number, size: number): void {
10
+ if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start < 0 || end >= size) {
11
+ throw new AppError('Invalid position, out of range', 'data');
12
+ }
13
+ }
14
+ }
@@ -12,7 +12,7 @@ import { Config } from '@travetto/config';
12
12
  import { Required } from '@travetto/schema';
13
13
 
14
14
  import { ModelCrudSupport } from '../service/crud';
15
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
15
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
16
16
  import { ModelType, OptionalId } from '../types/model';
17
17
  import { ModelExpirySupport } from '../service/expiry';
18
18
  import { ModelRegistry } from '../registry/model';
@@ -21,7 +21,7 @@ import { ModelCrudUtil } from '../internal/service/crud';
21
21
  import { ModelExpiryUtil } from '../internal/service/expiry';
22
22
  import { NotFoundError } from '../error/not-found';
23
23
  import { ExistsError } from '../error/exists';
24
- import { StreamModel, STREAMS } from '../internal/service/stream';
24
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
25
25
 
26
26
  type Suffix = '.bin' | '.meta' | '.json' | '.expires';
27
27
 
@@ -190,6 +190,17 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
190
190
  return createReadStream(file);
191
191
  }
192
192
 
193
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
194
+ const file = await this.#find(STREAMS, BIN, location);
195
+ const meta = await this.describeStream(location);
196
+ end ??= meta.size - 1;
197
+
198
+ ModelStreamUtil.checkRange(start, end, meta.size);
199
+
200
+ const stream = createReadStream(file, { start, end });
201
+ return { stream, range: [start, end] };
202
+ }
203
+
193
204
  async describeStream(location: string): Promise<StreamMeta> {
194
205
  const file = await this.#find(STREAMS, META, location);
195
206
  const content = await StreamUtil.streamToBuffer(createReadStream(file));
@@ -211,17 +222,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
211
222
 
212
223
  // Expiry
213
224
  async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
214
- const deleting = [];
215
- for await (const el of this.list(cls)) {
216
- if (ModelExpiryUtil.getExpiryState(cls, el).expired) {
217
- deleting.push(this.delete(cls, el.id).catch(err => {
218
- if (!(err instanceof NotFoundError)) {
219
- throw err;
220
- }
221
- }));
222
- }
223
- }
224
- return (await Promise.all(deleting)).length;
225
+ return ModelExpiryUtil.naiveDeleteExpired(this, cls);
225
226
  }
226
227
 
227
228
  // Storage management
@@ -6,7 +6,7 @@ import { Injectable } from '@travetto/di';
6
6
  import { Config } from '@travetto/config';
7
7
 
8
8
  import { ModelCrudSupport } from '../service/crud';
9
- import { ModelStreamSupport, StreamMeta } from '../service/stream';
9
+ import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
10
10
  import { ModelType, OptionalId } from '../types/model';
11
11
  import { ModelExpirySupport } from '../service/expiry';
12
12
  import { ModelRegistry } from '../registry/model';
@@ -18,7 +18,7 @@ import { ExistsError } from '../error/exists';
18
18
  import { ModelIndexedSupport } from '../service/indexed';
19
19
  import { ModelIndexedUtil } from '../internal/service/indexed';
20
20
  import { ModelStorageUtil } from '../internal/service/storage';
21
- import { StreamModel, STREAMS } from '../internal/service/stream';
21
+ import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
22
22
  import { IndexConfig } from '../registry/types';
23
23
 
24
24
  const STREAM_META = `${STREAMS}_meta`;
@@ -249,6 +249,15 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
249
249
  return StreamUtil.bufferToStream(streams.get(location)!);
250
250
  }
251
251
 
252
+ async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
253
+ const streams = this.#find(STREAMS, location, 'notfound');
254
+ const buffer = streams.get(location)!;
255
+ end ??= (buffer.length - 1);
256
+ ModelStreamUtil.checkRange(start, end, buffer.length);
257
+ const stream = await StreamUtil.bufferToStream(buffer.subarray(start, end + 1));
258
+ return { stream, range: [start, end] };
259
+ }
260
+
252
261
  async describeStream(location: string): Promise<StreamMeta> {
253
262
  const metaContent = this.#find(STREAM_META, location, 'notfound');
254
263
  const meta: StreamMeta = JSON.parse(metaContent.get(location)!.toString('utf8'));
@@ -266,20 +275,9 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
266
275
  }
267
276
  }
268
277
 
269
- // Expiry Support
278
+ // Expiry
270
279
  async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
271
- const deleting = [];
272
- const store = this.#getStore(cls);
273
- for (const id of [...store.keys()]) {
274
- if ((ModelExpiryUtil.getExpiryState(cls, await this.get(cls, id))).expired) {
275
- deleting.push(this.delete(cls, id).catch(err => {
276
- if (!(err instanceof NotFoundError)) {
277
- throw err;
278
- }
279
- }));
280
- }
281
- }
282
- return (await Promise.all(deleting)).length;
280
+ return ModelExpiryUtil.naiveDeleteExpired(this, cls);
283
281
  }
284
282
 
285
283
  // Storage Support
@@ -30,7 +30,11 @@ export interface ModelCrudSupport extends ModelBasicSupport {
30
30
  upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T>;
31
31
 
32
32
  /**
33
- * Update Partial
33
+ * Update partial, respecting only top level keys.
34
+ *
35
+ * When invoking this method, any top level keys that are null/undefined are treated as removals/deletes. Any properties
36
+ * that point to sub objects/arrays are treated as wholesale replacements.
37
+ *
34
38
  * @param id The document identifier to update
35
39
  * @param item The document to partially update.
36
40
  * @param view The schema view to validate against
@@ -14,9 +14,18 @@ export interface StreamMeta {
14
14
  */
15
15
  hash: string;
16
16
  /**
17
- * The original filename of the file
17
+ * The original base filename of the file
18
18
  */
19
19
  filename: string;
20
+ /**
21
+ * Filenames title, optional for elements like images, audio, videos
22
+ */
23
+ title?: string;
24
+ }
25
+
26
+ export interface PartialStream {
27
+ stream: Readable;
28
+ range: [number, number];
20
29
  }
21
30
 
22
31
  /**
@@ -40,6 +49,12 @@ export interface ModelStreamSupport {
40
49
  */
41
50
  getStream(location: string): Promise<Readable>;
42
51
 
52
+ /**
53
+ * Get partial stream from asset store given a starting byte and an optional ending byte
54
+ * @param location The location of the stream
55
+ */
56
+ getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
57
+
43
58
  /**
44
59
  * Get metadata for stream
45
60
  * @param location The location of the stream
@@ -0,0 +1 @@
1
+ abcdefghijklmnopqrstuvwxyz
@@ -127,7 +127,6 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
127
127
  gender: 'f',
128
128
  address: {
129
129
  street1: 'changed\n',
130
- street2: undefined
131
130
  }
132
131
  }));
133
132
 
@@ -172,7 +171,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
172
171
  async testBlankPartialUpdate() {
173
172
  const service = await this.service;
174
173
  const o = await service.create(User2, User2.from({
175
- name: 'bob'
174
+ name: 'bob',
176
175
  }));
177
176
 
178
177
  assert(o.address === undefined);
@@ -187,7 +186,8 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
187
186
  const o3 = await service.get(User2, o.id);
188
187
 
189
188
  assert(o3.address !== undefined);
190
- assert(o3.address!.street1 === 'blue');
189
+ assert(o3.address.street1 === 'blue');
190
+ assert(o3.address.street2 === undefined);
191
191
  }
192
192
 
193
193
  @Test('verify dates')
@@ -282,4 +282,29 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
282
282
  assert(o2.age === 20);
283
283
  assert(o2.address.street2 === 'roader');
284
284
  }
285
+
286
+ @Test('Verify nested list in partial update')
287
+ async testPartialUpdateOnLists() {
288
+ const service = await this.service;
289
+ const o = await service.create(SimpleList, {
290
+ names: ['rob', 'tom'],
291
+ simples: [
292
+ { name: 'roger' },
293
+ { name: 'dodger' }
294
+ ]
295
+ });
296
+ assert(o.names.length === 2);
297
+ assert(o.simples);
298
+ assert(o.simples.length === 2);
299
+
300
+ const o2 = await service.updatePartial(SimpleList, {
301
+ id: o.id,
302
+ names: ['dawn'],
303
+ simples: [{ name: 'jim' }]
304
+ });
305
+
306
+ assert(o2.names.length === 1);
307
+ assert(o2.simples);
308
+ assert(o2.simples.length === 1);
309
+ }
285
310
  }
@@ -6,6 +6,7 @@ import { Suite, Test, TestFixtures } from '@travetto/test';
6
6
 
7
7
  import { BaseModelSuite } from './base';
8
8
  import { ModelStreamSupport } from '../../src/service/stream';
9
+ import { StreamUtil } from '@travetto/base';
9
10
 
10
11
  @Suite()
11
12
  export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport> {
@@ -68,4 +69,36 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
68
69
  await service.getStream(meta.hash);
69
70
  });
70
71
  }
72
+
73
+ @Test()
74
+ async partialStream(): Promise<void> {
75
+ const service = await this.service;
76
+ const [meta, stream] = await this.getStream('/text.txt');
77
+
78
+ await service.upsertStream(meta.hash, stream, meta);
79
+
80
+ const retrieved = await service.getStream(meta.hash);
81
+ const content = (await StreamUtil.toBuffer(retrieved)).toString('utf8');
82
+ assert(content.startsWith('abc'));
83
+ assert(content.endsWith('xyz'));
84
+
85
+ const partial = await service.getStreamPartial(meta.hash, 10, 20);
86
+ const subContent = (await StreamUtil.toBuffer(partial.stream)).toString('utf8');
87
+ assert(subContent.length === (partial.range[1] - partial.range[0]) + 1);
88
+ assert(subContent === 'klmnopqrstu');
89
+
90
+ const partialUnbounded = await service.getStreamPartial(meta.hash, 10);
91
+ const subContent2 = (await StreamUtil.toBuffer(partialUnbounded.stream)).toString('utf8');
92
+ assert(subContent2.length === (partialUnbounded.range[1] - partialUnbounded.range[0]) + 1);
93
+ assert(subContent2.startsWith('klm'));
94
+ assert(subContent2.endsWith('xyz'));
95
+
96
+ const partialSingle = await service.getStreamPartial(meta.hash, 10, 10);
97
+ const subContent3 = (await StreamUtil.toBuffer(partialSingle.stream)).toString('utf8');
98
+ assert(subContent3.length === 1);
99
+ assert(subContent3 === 'k');
100
+
101
+ await assert.rejects(() => service.getStreamPartial(meta.hash, -10, 10));
102
+ await assert.rejects(() => service.getStreamPartial(meta.hash, 0, 27));
103
+ }
71
104
  }