@travetto/model 4.1.3 → 5.0.0-rc.1
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/README.md +9 -15
- package/package.json +7 -7
- package/src/internal/service/crud.ts +4 -4
- package/src/internal/service/expiry.ts +1 -1
- package/src/internal/service/stream.ts +18 -2
- package/src/provider/file.ts +15 -22
- package/src/provider/memory.ts +12 -16
- package/src/registry/types.ts +2 -1
- package/src/service/bulk.ts +1 -1
- package/src/service/stream.ts +2 -11
- package/support/doc.support.tsx +1 -1
- package/support/test/expiry.ts +2 -2
- package/support/test/indexed.ts +3 -3
- package/support/test/stream.ts +17 -14
package/LICENSE
CHANGED
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,8 +225,8 @@ 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#
|
|
235
|
-
|[FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#
|
|
228
|
+
|[MemoryModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/memory.ts#L54)|X|X|X|X|X|X|
|
|
229
|
+
|[FileModelService](https://github.com/travetto/travetto/tree/main/module/model/src/provider/file.ts#L49)|X|X| |X|X|X|
|
|
236
230
|
|
|
237
231
|
## Custom Model Service
|
|
238
232
|
In addition to the provided contracts, the module also provides common utilities and shared test suites. The common utilities are useful for repetitive functionality, that is unable to be shared due to not relying upon inheritance (this was an intentional design decision). This allows for all the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementations to completely own the functionality and also to be able to provide additional/unique functionality that goes beyond the interface.
|
|
@@ -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 {
|
|
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,
|
|
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 {
|
|
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": "
|
|
3
|
+
"version": "5.0.0-rc.1",
|
|
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": "^
|
|
30
|
-
"@travetto/di": "^
|
|
31
|
-
"@travetto/registry": "^
|
|
32
|
-
"@travetto/schema": "^
|
|
29
|
+
"@travetto/config": "^5.0.0-rc.1",
|
|
30
|
+
"@travetto/di": "^5.0.0-rc.0",
|
|
31
|
+
"@travetto/registry": "^5.0.0-rc.1",
|
|
32
|
+
"@travetto/schema": "^5.0.0-rc.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^
|
|
36
|
-
"@travetto/test": "^
|
|
35
|
+
"@travetto/cli": "^5.0.0-rc.1",
|
|
36
|
+
"@travetto/test": "^5.0.0-rc.1"
|
|
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,
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
+
}
|
package/src/provider/file.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
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 {
|
|
9
|
-
import { StreamUtil, Class, TimeSpan } from '@travetto/base';
|
|
8
|
+
import { Class, TimeSpan, RuntimeContext } from '@travetto/base';
|
|
10
9
|
import { Injectable } from '@travetto/di';
|
|
11
10
|
import { Config } from '@travetto/config';
|
|
12
11
|
import { Required } from '@travetto/schema';
|
|
13
12
|
|
|
14
13
|
import { ModelCrudSupport } from '../service/crud';
|
|
15
|
-
import { ModelStreamSupport,
|
|
14
|
+
import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
|
|
16
15
|
import { ModelType, OptionalId } from '../types/model';
|
|
17
16
|
import { ModelExpirySupport } from '../service/expiry';
|
|
18
17
|
import { ModelRegistry } from '../registry/model';
|
|
@@ -21,7 +20,7 @@ import { ModelCrudUtil } from '../internal/service/crud';
|
|
|
21
20
|
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
22
21
|
import { NotFoundError } from '../error/not-found';
|
|
23
22
|
import { ExistsError } from '../error/exists';
|
|
24
|
-
import { StreamModel, STREAMS } from '../internal/service/stream';
|
|
23
|
+
import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
|
|
25
24
|
|
|
26
25
|
type Suffix = '.bin' | '.meta' | '.json' | '.expires';
|
|
27
26
|
|
|
@@ -112,7 +111,7 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
112
111
|
const file = await this.#resolveName(cls, '.json', id);
|
|
113
112
|
|
|
114
113
|
if (await exists(file)) {
|
|
115
|
-
const content = await
|
|
114
|
+
const content = await fs.readFile(file);
|
|
116
115
|
return this.checkExpiry(cls, await ModelCrudUtil.load(cls, content));
|
|
117
116
|
}
|
|
118
117
|
|
|
@@ -180,29 +179,23 @@ export class FileModelService implements ModelCrudSupport, ModelStreamSupport, M
|
|
|
180
179
|
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
181
180
|
const file = await this.#resolveName(STREAMS, BIN, location);
|
|
182
181
|
await Promise.all([
|
|
183
|
-
|
|
182
|
+
await pipeline(input, createWriteStream(file)),
|
|
184
183
|
fs.writeFile(file.replace(BIN, META), JSON.stringify(meta), 'utf8')
|
|
185
184
|
]);
|
|
186
185
|
}
|
|
187
186
|
|
|
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> {
|
|
187
|
+
async getStream(location: string, range?: StreamRange): Promise<Readable> {
|
|
194
188
|
const file = await this.#find(STREAMS, BIN, location);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return { stream, range: [start, end] };
|
|
189
|
+
if (range) {
|
|
190
|
+
const meta = await this.describeStream(location);
|
|
191
|
+
range = enforceRange(range, meta.size);
|
|
192
|
+
}
|
|
193
|
+
return createReadStream(file, range);
|
|
201
194
|
}
|
|
202
195
|
|
|
203
196
|
async describeStream(location: string): Promise<StreamMeta> {
|
|
204
197
|
const file = await this.#find(STREAMS, META, location);
|
|
205
|
-
const content = await
|
|
198
|
+
const content = await fs.readFile(file);
|
|
206
199
|
const text: StreamMeta = JSON.parse(content.toString('utf8'));
|
|
207
200
|
return text;
|
|
208
201
|
}
|
package/src/provider/memory.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
+
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
2
3
|
|
|
3
|
-
import {
|
|
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,
|
|
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
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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> {
|
package/src/registry/types.ts
CHANGED
package/src/service/bulk.ts
CHANGED
|
@@ -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
|
});
|
package/src/service/stream.ts
CHANGED
|
@@ -35,10 +35,7 @@ export interface StreamMeta {
|
|
|
35
35
|
cacheControl?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export
|
|
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
|
package/support/doc.support.tsx
CHANGED
|
@@ -31,7 +31,7 @@ export const ModelTypes = (file: string | Function): DocJSXElement[] => {
|
|
|
31
31
|
return found.map(v => <li>{v}</li>);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
export const ModelCustomConfig = ({ cfg }: { cfg: Function }):
|
|
34
|
+
export const ModelCustomConfig = ({ cfg }: { cfg: Function }): DocJSXElement => <>
|
|
35
35
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source
|
|
36
36
|
or config, you can override and register it with the {d.mod('Di')} module.
|
|
37
37
|
|
package/support/test/expiry.ts
CHANGED
|
@@ -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.
|
|
26
|
+
await timers.setTimeout(TimeUtil.asMillis(n) * this.delayFactor);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
timeFromNow(v: number | TimeSpan, unit?: TimeUnit) {
|
|
30
|
-
return
|
|
30
|
+
return TimeUtil.fromNow(TimeUtil.asMillis(v, unit) * this.delayFactor);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
@Test()
|
package/support/test/indexed.ts
CHANGED
|
@@ -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.
|
|
153
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.
|
|
154
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.
|
|
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
|
|
package/support/test/stream.ts
CHANGED
|
@@ -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
|
|
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.
|
|
84
|
-
const subContent = (await
|
|
85
|
-
|
|
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.
|
|
89
|
-
const subContent2 = (await
|
|
90
|
-
|
|
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.
|
|
95
|
-
const subContent3 = (await
|
|
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.
|
|
100
|
-
const subContent4 = (await
|
|
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.
|
|
105
|
-
await assert.rejects(() => service.
|
|
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
|
}
|