@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 +14 -3
- package/package.json +1 -1
- package/src/internal/service/expiry.ts +19 -0
- package/src/internal/service/stream.ts +10 -2
- package/src/provider/file.ts +14 -13
- package/src/provider/memory.ts +13 -15
- package/src/service/crud.ts +5 -1
- package/src/service/stream.ts +16 -1
- package/support/fixtures/text.txt +1 -0
- package/support/test/crud.ts +28 -3
- package/support/test/stream.ts +33 -0
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
|
|
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
|
@@ -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
|
+
}
|
package/src/provider/file.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/provider/memory.ts
CHANGED
|
@@ -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
|
|
278
|
+
// Expiry
|
|
270
279
|
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
271
|
-
|
|
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
|
package/src/service/crud.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/service/stream.ts
CHANGED
|
@@ -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
|
package/support/test/crud.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/support/test/stream.ts
CHANGED
|
@@ -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
|
}
|