@travetto/model 4.1.3 → 5.0.0-rc.0

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) 2020 ArcSine Technologies
3
+ Copyright (c) 2023 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/README.md CHANGED
@@ -172,13 +172,7 @@ export interface ModelStreamSupport {
172
172
  * Get stream from asset store
173
173
  * @param location The location of the stream
174
174
  */
175
- getStream(location: string): Promise<Readable>;
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>;
175
+ getStream(location: string, range?: StreamRange): Promise<Readable>;
182
176
 
183
177
  /**
184
178
  * Get metadata for stream
@@ -231,7 +225,7 @@ The `id` is the only required field for a model, as this is a hard requirement o
231
225
  |[Redis Model Support](https://github.com/travetto/travetto/tree/main/module/model-redis#readme "Redis backing for the travetto model module.")|X|X|X|X| ||
232
226
  |[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| |
233
227
  |[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|
234
- |[MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L53)|X|X|X|X|X|X|
228
+ |[MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L54)|X|X|X|X|X|X|
235
229
  |[FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#L50)|X|X| |X|X|X|
236
230
 
237
231
  ## Custom Model Service
@@ -240,12 +234,13 @@ In addition to the provided contracts, the module also provides common utilities
240
234
  **Code: Memory Service**
241
235
  ```typescript
242
236
  import { Readable } from 'node:stream';
243
- import { StreamUtil, Class, TimeSpan } from '@travetto/base';
237
+ import { buffer as toBuffer } from 'node:stream/consumers';
238
+ import { Class, TimeSpan } from '@travetto/base';
244
239
  import { DeepPartial } from '@travetto/schema';
245
240
  import { Injectable } from '@travetto/di';
246
241
  import { Config } from '@travetto/config';
247
242
  import { ModelCrudSupport } from '../service/crud';
248
- import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
243
+ import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
249
244
  import { ModelType, OptionalId } from '../types/model';
250
245
  import { ModelExpirySupport } from '../service/expiry';
251
246
  import { ModelRegistry } from '../registry/model';
@@ -257,13 +252,13 @@ import { ExistsError } from '../error/exists';
257
252
  import { ModelIndexedSupport } from '../service/indexed';
258
253
  import { ModelIndexedUtil } from '../internal/service/indexed';
259
254
  import { ModelStorageUtil } from '../internal/service/storage';
260
- import { ModelStreamUtil, StreamModel, STREAMS } from '../internal/service/stream';
255
+ import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
261
256
  import { IndexConfig } from '../registry/types';
262
257
  const STREAM_META = `${STREAMS}_meta`;
263
258
  type StoreType = Map<string, Buffer>;
264
259
  @Config('model.memory')
265
260
  export class MemoryModelConfig {
266
- autoCreate?: boolean;
261
+ autoCreate?: boolean = true;
267
262
  namespace?: string;
268
263
  cullRate?: number | TimeSpan;
269
264
  }
@@ -301,8 +296,7 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
301
296
  async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T>;
302
297
  // Stream Support
303
298
  async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
304
- async getStream(location: string): Promise<Readable>;
305
- async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
299
+ async getStream(location: string, range?: StreamRange): Promise<Readable>;
306
300
  async describeStream(location: string): Promise<StreamMeta>;
307
301
  async deleteStream(location: string): Promise<void>;
308
302
  // Expiry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model",
3
- "version": "4.1.3",
3
+ "version": "5.0.0-rc.0",
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": "^4.1.1",
30
- "@travetto/di": "^4.1.1",
31
- "@travetto/registry": "^4.1.1",
32
- "@travetto/schema": "^4.1.1"
29
+ "@travetto/config": "^5.0.0-rc.0",
30
+ "@travetto/di": "^5.0.0-rc.0",
31
+ "@travetto/registry": "^5.0.0-rc.0",
32
+ "@travetto/schema": "^5.0.0-rc.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@travetto/cli": "^4.1.1",
36
- "@travetto/test": "^4.1.1"
35
+ "@travetto/cli": "^5.0.0-rc.0",
36
+ "@travetto/test": "^5.0.0-rc.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
 
3
- import { Class, ObjectUtil, Util } from '@travetto/base';
4
- import { SchemaRegistry, SchemaValidator, ValidationError, ValidationResultError } from '@travetto/schema';
3
+ import { Class, Util } from '@travetto/base';
4
+ import { DataUtil, SchemaRegistry, SchemaValidator, ValidationError, ValidationResultError } from '@travetto/schema';
5
5
 
6
6
  import { ModelRegistry } from '../../registry/model';
7
7
  import { ModelIdSource, ModelType, OptionalId } from '../../types/model';
@@ -77,7 +77,7 @@ export class ModelCrudUtil {
77
77
  item.id = provider.idSource.create();
78
78
  }
79
79
 
80
- if (ObjectUtil.isPlainObject(item)) {
80
+ if (DataUtil.isPlainObject(item)) {
81
81
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
82
82
  item = cls.from(item as object);
83
83
  }
@@ -118,7 +118,7 @@ export class ModelCrudUtil {
118
118
  * @param getExisting How to fetch an existing item
119
119
  */
120
120
  static async naivePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view: undefined | string, getExisting: () => Promise<T>): Promise<T> {
121
- if (ObjectUtil.isPlainObject(item)) {
121
+ if (DataUtil.isPlainObject(item)) {
122
122
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
123
123
  item = cls.from(item as object);
124
124
  }
@@ -34,7 +34,7 @@ export class ModelExpiryUtil {
34
34
  const cullable = ModelRegistry.getClasses().filter(cls => !!ModelRegistry.get(cls).expiresAt);
35
35
  if (svc.deleteExpired && cullable.length) {
36
36
  const running = new AbortController();
37
- const cullInterval = TimeUtil.timeToMs(svc.config?.cullRate ?? '10m');
37
+ const cullInterval = TimeUtil.asMillis(svc.config?.cullRate ?? '10m');
38
38
 
39
39
  ShutdownManager.onGracefulShutdown(async () => running.abort(), this);
40
40
 
@@ -1,6 +1,22 @@
1
- import { Class } from '@travetto/base';
1
+ import { AppError, Class } from '@travetto/base';
2
+
2
3
  import { ModelType } from '../../types/model';
4
+ import { StreamRange } from '../../service/stream';
3
5
 
4
6
  class Cls { id: string; }
5
7
  export const StreamModel: Class<ModelType> = Cls;
6
- export const STREAMS = '_streams';
8
+ export const STREAMS = '_streams';
9
+
10
+
11
+ /**
12
+ * Enforce byte range for stream stream/file of a certain size
13
+ */
14
+ export function enforceRange({ start, end }: StreamRange, size: number): Required<StreamRange> {
15
+ end = Math.min(end ?? size - 1, size - 1);
16
+
17
+ if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
18
+ throw new AppError('Invalid position, out of range', 'data');
19
+ }
20
+
21
+ return { start, end };
22
+ }
@@ -1,18 +1,18 @@
1
1
  import fs from 'node:fs/promises';
2
- import { createReadStream } from 'node:fs';
3
-
2
+ import { createReadStream, createWriteStream } from 'node:fs';
4
3
  import os from 'node:os';
5
-
6
4
  import { Readable } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import path from 'node:path';
7
7
 
8
- import { path, RuntimeContext } from '@travetto/manifest';
9
- import { StreamUtil, Class, TimeSpan } from '@travetto/base';
8
+ import { RuntimeContext } from '@travetto/manifest';
9
+ import { Class, TimeSpan } from '@travetto/base';
10
10
  import { Injectable } from '@travetto/di';
11
11
  import { Config } from '@travetto/config';
12
12
  import { Required } from '@travetto/schema';
13
13
 
14
14
  import { ModelCrudSupport } from '../service/crud';
15
- import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
15
+ import { ModelStreamSupport, StreamMeta, StreamRange } 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 { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
25
25
 
26
26
  type Suffix = '.bin' | '.meta' | '.json' | '.expires';
27
27
 
@@ -112,7 +112,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
112
112
  const file = await this.#resolveName(cls, '.json', id);
113
113
 
114
114
  if (await exists(file)) {
115
- const content = await StreamUtil.streamToBuffer(createReadStream(file));
115
+ const content = await fs.readFile(file);
116
116
  return this.checkExpiry(cls, await ModelCrudUtil.load(cls, content));
117
117
  }
118
118
 
@@ -180,29 +180,23 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
180
180
  async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
181
181
  const file = await this.#resolveName(STREAMS, BIN, location);
182
182
  await Promise.all([
183
- StreamUtil.writeToFile(input, file),
183
+ await pipeline(input, createWriteStream(file)),
184
184
  fs.writeFile(file.replace(BIN, META), JSON.stringify(meta), 'utf8')
185
185
  ]);
186
186
  }
187
187
 
188
- async getStream(location: string): Promise<Readable> {
189
- const file = await this.#find(STREAMS, BIN, location);
190
- return createReadStream(file);
191
- }
192
-
193
- async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
188
+ async getStream(location: string, range?: StreamRange): Promise<Readable> {
194
189
  const file = await this.#find(STREAMS, BIN, location);
195
- const meta = await this.describeStream(location);
196
-
197
- [start, end] = StreamUtil.enforceRange(start, end, meta.size);
198
-
199
- const stream = createReadStream(file, { start, end });
200
- return { stream, range: [start, end] };
190
+ if (range) {
191
+ const meta = await this.describeStream(location);
192
+ range = enforceRange(range, meta.size);
193
+ }
194
+ return createReadStream(file, range);
201
195
  }
202
196
 
203
197
  async describeStream(location: string): Promise<StreamMeta> {
204
198
  const file = await this.#find(STREAMS, META, location);
205
- const content = await StreamUtil.streamToBuffer(createReadStream(file));
199
+ const content = await fs.readFile(file);
206
200
  const text: StreamMeta = JSON.parse(content.toString('utf8'));
207
201
  return text;
208
202
  }
@@ -1,12 +1,13 @@
1
1
  import { Readable } from 'node:stream';
2
+ import { buffer as toBuffer } from 'node:stream/consumers';
2
3
 
3
- import { StreamUtil, Class, TimeSpan } from '@travetto/base';
4
+ import { Class, TimeSpan } from '@travetto/base';
4
5
  import { DeepPartial } from '@travetto/schema';
5
6
  import { Injectable } from '@travetto/di';
6
7
  import { Config } from '@travetto/config';
7
8
 
8
9
  import { ModelCrudSupport } from '../service/crud';
9
- import { ModelStreamSupport, PartialStream, StreamMeta } from '../service/stream';
10
+ import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
10
11
  import { ModelType, OptionalId } from '../types/model';
11
12
  import { ModelExpirySupport } from '../service/expiry';
12
13
  import { ModelRegistry } from '../registry/model';
@@ -18,7 +19,7 @@ import { ExistsError } from '../error/exists';
18
19
  import { ModelIndexedSupport } from '../service/indexed';
19
20
  import { ModelIndexedUtil } from '../internal/service/indexed';
20
21
  import { ModelStorageUtil } from '../internal/service/storage';
21
- import { StreamModel, STREAMS } from '../internal/service/stream';
22
+ import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
22
23
  import { IndexConfig } from '../registry/types';
23
24
 
24
25
  const STREAM_META = `${STREAMS}_meta`;
@@ -244,22 +245,17 @@ export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport,
244
245
  const streams = this.#getStore(STREAMS);
245
246
  const metaContent = this.#getStore(STREAM_META);
246
247
  metaContent.set(location, Buffer.from(JSON.stringify(meta)));
247
- streams.set(location, await StreamUtil.streamToBuffer(input));
248
+ streams.set(location, await toBuffer(input));
248
249
  }
249
250
 
250
- async getStream(location: string): Promise<Readable> {
251
+ async getStream(location: string, range?: StreamRange): Promise<Readable> {
251
252
  const streams = this.#find(STREAMS, location, 'notfound');
252
- return StreamUtil.bufferToStream(streams.get(location)!);
253
- }
254
-
255
- async getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream> {
256
- const streams = this.#find(STREAMS, location, 'notfound');
257
- const buffer = streams.get(location)!;
258
-
259
- [start, end] = StreamUtil.enforceRange(start, end, buffer.length);
260
-
261
- const stream = await StreamUtil.bufferToStream(buffer.subarray(start, end + 1));
262
- return { stream, range: [start, end] };
253
+ let buffer = streams.get(location)!;
254
+ if (range) {
255
+ range = enforceRange(range, buffer.length);
256
+ buffer = buffer.subarray(range.start, range.end! + 1);
257
+ }
258
+ return Readable.from(buffer);
263
259
  }
264
260
 
265
261
  async describeStream(location: string): Promise<StreamMeta> {
@@ -1,4 +1,5 @@
1
- import { Primitive, Class } from '@travetto/base';
1
+ import { Class } from '@travetto/base';
2
+ import { Primitive } from '@travetto/schema';
2
3
 
3
4
  import { ModelType } from '../types/model';
4
5
 
@@ -44,7 +44,7 @@ export class BulkProcessError extends AppError {
44
44
  constructor(public errors: { idx: number, error: ValidationResultError }[]) {
45
45
  super('Bulk processing errors have occurred', 'data', {
46
46
  errors: errors.map(x => {
47
- const { message, type, errors: subErrors, details } = x.error;
47
+ const { message, type, details: { errors: subErrors } = {}, details } = x.error;
48
48
  return { message, type, errors: subErrors ?? details, idx: x.idx };
49
49
  })
50
50
  });
@@ -35,10 +35,7 @@ export interface StreamMeta {
35
35
  cacheControl?: string;
36
36
  }
37
37
 
38
- export interface PartialStream {
39
- stream: Readable;
40
- range: [number, number];
41
- }
38
+ export type StreamRange = { start: number, end?: number };
42
39
 
43
40
  /**
44
41
  * Support for Streams CRD. Stream update is not supported.
@@ -59,13 +56,7 @@ export interface ModelStreamSupport {
59
56
  * Get stream from asset store
60
57
  * @param location The location of the stream
61
58
  */
62
- getStream(location: string): Promise<Readable>;
63
-
64
- /**
65
- * Get partial stream from asset store given a starting byte and an optional ending byte
66
- * @param location The location of the stream
67
- */
68
- getStreamPartial(location: string, start: number, end?: number): Promise<PartialStream>;
59
+ getStream(location: string, range?: StreamRange): Promise<Readable>;
69
60
 
70
61
  /**
71
62
  * Get metadata for stream
@@ -23,11 +23,11 @@ export abstract class ModelExpirySuite extends BaseModelSuite<ModelExpirySupport
23
23
  delayFactor: number = 1;
24
24
 
25
25
  async wait(n: number | TimeSpan) {
26
- await timers.setTimeout(TimeUtil.timeToMs(n) * this.delayFactor);
26
+ await timers.setTimeout(TimeUtil.asMillis(n) * this.delayFactor);
27
27
  }
28
28
 
29
29
  timeFromNow(v: number | TimeSpan, unit?: TimeUnit) {
30
- return new Date(Date.now() + TimeUtil.timeToMs(v, unit) * this.delayFactor);
30
+ return TimeUtil.fromNow(TimeUtil.asMillis(v, unit) * this.delayFactor);
31
31
  }
32
32
 
33
33
  @Test()
@@ -149,9 +149,9 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
149
149
  async queryComplexDateList() {
150
150
  const service = await this.service;
151
151
 
152
- await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, createdDate: TimeUtil.timeFromNow('3d'), color: 'blue' }));
153
- await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.timeFromNow('2d'), color: 'red' }));
154
- await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.timeFromNow('-1d'), color: 'green' }));
152
+ await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, createdDate: TimeUtil.fromNow('3d'), color: 'blue' }));
153
+ await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.fromNow('2d'), color: 'red' }));
154
+ await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.fromNow('-1d'), color: 'green' }));
155
155
 
156
156
  const arr = await this.toArray(service.listByIndex(User4, 'nameCreated', { child: { name: 'bob' } }));
157
157
 
@@ -3,12 +3,13 @@ import assert from 'node:assert';
3
3
  import crypto from 'node:crypto';
4
4
  import { Readable } from 'node:stream';
5
5
  import { pipeline } from 'node:stream/promises';
6
+ import { buffer as toBuffer } from 'node:stream/consumers';
6
7
 
7
8
  import { Suite, Test, TestFixtures } from '@travetto/test';
8
- import { StreamUtil } from '@travetto/base';
9
9
 
10
10
  import { BaseModelSuite } from './base';
11
11
  import { ModelStreamSupport } from '../../src/service/stream';
12
+ import { enforceRange } from '../../src/internal/service/stream';
12
13
 
13
14
  @Suite()
14
15
  export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport> {
@@ -76,32 +77,34 @@ export abstract class ModelStreamSuite extends BaseModelSuite<ModelStreamSupport
76
77
  await service.upsertStream(meta.hash, stream, meta);
77
78
 
78
79
  const retrieved = await service.getStream(meta.hash);
79
- const content = (await StreamUtil.toBuffer(retrieved)).toString('utf8');
80
+ const content = (await toBuffer(retrieved)).toString('utf8');
80
81
  assert(content.startsWith('abc'));
81
82
  assert(content.endsWith('xyz'));
82
83
 
83
- const partial = await service.getStreamPartial(meta.hash, 10, 20);
84
- const subContent = (await StreamUtil.toBuffer(partial.stream)).toString('utf8');
85
- assert(subContent.length === (partial.range[1] - partial.range[0]) + 1);
84
+ const partial = await service.getStream(meta.hash, { start: 10, end: 20 });
85
+ const subContent = (await toBuffer(partial)).toString('utf8');
86
+ const range = await enforceRange({ start: 10, end: 20 }, meta.size);
87
+ assert(subContent.length === (range.end - range.start) + 1);
86
88
  assert(subContent === 'klmnopqrstu');
87
89
 
88
- const partialUnbounded = await service.getStreamPartial(meta.hash, 10);
89
- const subContent2 = (await StreamUtil.toBuffer(partialUnbounded.stream)).toString('utf8');
90
- assert(subContent2.length === (partialUnbounded.range[1] - partialUnbounded.range[0]) + 1);
90
+ const partialUnbounded = await service.getStream(meta.hash, { start: 10 });
91
+ const subContent2 = (await toBuffer(partialUnbounded)).toString('utf8');
92
+ const range2 = await enforceRange({ start: 10 }, meta.size);
93
+ assert(subContent2.length === (range2.end - range2.start) + 1);
91
94
  assert(subContent2.startsWith('klm'));
92
95
  assert(subContent2.endsWith('xyz'));
93
96
 
94
- const partialSingle = await service.getStreamPartial(meta.hash, 10, 10);
95
- const subContent3 = (await StreamUtil.toBuffer(partialSingle.stream)).toString('utf8');
97
+ const partialSingle = await service.getStream(meta.hash, { start: 10, end: 10 });
98
+ const subContent3 = (await toBuffer(partialSingle)).toString('utf8');
96
99
  assert(subContent3.length === 1);
97
100
  assert(subContent3 === 'k');
98
101
 
99
- const partialOverbounded = await service.getStreamPartial(meta.hash, 20, 40);
100
- const subContent4 = (await StreamUtil.toBuffer(partialOverbounded.stream)).toString('utf8');
102
+ const partialOverbounded = await service.getStream(meta.hash, { start: 20, end: 40 });
103
+ const subContent4 = (await toBuffer(partialOverbounded)).toString('utf8');
101
104
  assert(subContent4.length === 6);
102
105
  assert(subContent4.endsWith('xyz'));
103
106
 
104
- await assert.rejects(() => service.getStreamPartial(meta.hash, -10, 10));
105
- await assert.rejects(() => service.getStreamPartial(meta.hash, 30, 37));
107
+ await assert.rejects(() => service.getStream(meta.hash, { start: -10, end: 10 }));
108
+ await assert.rejects(() => service.getStream(meta.hash, { start: 30, end: 37 }));
106
109
  }
107
110
  }