@travetto/model 4.1.2 → 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/README.md +8 -14
- package/package.json +7 -7
- package/src/internal/service/crud.ts +15 -12
- package/src/internal/service/expiry.ts +1 -1
- package/src/internal/service/stream.ts +18 -2
- package/src/provider/file.ts +16 -22
- package/src/provider/memory.ts +12 -16
- package/src/registry/decorator.ts +14 -8
- package/src/registry/types.ts +5 -2
- package/src/service/bulk.ts +1 -1
- package/src/service/stream.ts +2 -11
- package/support/test/crud.ts +30 -5
- package/support/test/expiry.ts +2 -2
- package/support/test/indexed.ts +3 -3
- package/support/test/stream.ts +17 -14
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#
|
|
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 {
|
|
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.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": "^
|
|
30
|
-
"@travetto/di": "^
|
|
31
|
-
"@travetto/registry": "^
|
|
32
|
-
"@travetto/schema": "^
|
|
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": "^
|
|
36
|
-
"@travetto/test": "^
|
|
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,14 +1,14 @@
|
|
|
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';
|
|
8
8
|
import { NotFoundError } from '../../error/not-found';
|
|
9
9
|
import { ExistsError } from '../../error/exists';
|
|
10
10
|
import { SubTypeNotSupportedError } from '../../error/invalid-sub-type';
|
|
11
|
-
import { DataHandler } from '../../registry/types';
|
|
11
|
+
import { DataHandler, PrePersistScope } from '../../registry/types';
|
|
12
12
|
|
|
13
13
|
export type ModelCrudProvider = {
|
|
14
14
|
idSource: ModelIdSource;
|
|
@@ -72,12 +72,12 @@ export class ModelCrudUtil {
|
|
|
72
72
|
* @param cls Type to store for
|
|
73
73
|
* @param item Item to store
|
|
74
74
|
*/
|
|
75
|
-
static async preStore<T extends ModelType>(cls: Class<T>, item: Partial<OptionalId<T>>, provider: ModelCrudProvider): Promise<T> {
|
|
75
|
+
static async preStore<T extends ModelType>(cls: Class<T>, item: Partial<OptionalId<T>>, provider: ModelCrudProvider, scope: PrePersistScope = 'all'): Promise<T> {
|
|
76
76
|
if (!item.id) {
|
|
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
|
}
|
|
@@ -88,7 +88,7 @@ export class ModelCrudUtil {
|
|
|
88
88
|
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
item = await this.prePersist(cls, item);
|
|
91
|
+
item = await this.prePersist(cls, item, scope);
|
|
92
92
|
|
|
93
93
|
let errors: ValidationError[] = [];
|
|
94
94
|
try {
|
|
@@ -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
|
}
|
|
@@ -137,7 +137,7 @@ export class ModelCrudUtil {
|
|
|
137
137
|
|
|
138
138
|
item = Object.assign(existing, item);
|
|
139
139
|
|
|
140
|
-
item = await this.prePersist(cls, item);
|
|
140
|
+
item = await this.prePersist(cls, item, 'partial');
|
|
141
141
|
|
|
142
142
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
143
143
|
return item as T;
|
|
@@ -155,11 +155,14 @@ export class ModelCrudUtil {
|
|
|
155
155
|
/**
|
|
156
156
|
* Pre persist behavior
|
|
157
157
|
*/
|
|
158
|
-
static async prePersist<T>(cls: Class<T>, item: T): Promise<T> {
|
|
158
|
+
static async prePersist<T>(cls: Class<T>, item: T, scope: PrePersistScope): Promise<T> {
|
|
159
159
|
const config = ModelRegistry.get(cls);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
for (const state of (config.prePersist ?? [])) {
|
|
161
|
+
if (state.scope === scope || scope === 'all' || state.scope === 'all') {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
163
|
+
const handler = state.handler as unknown as DataHandler<T>;
|
|
164
|
+
item = await handler(item) ?? item;
|
|
165
|
+
}
|
|
163
166
|
}
|
|
164
167
|
if (typeof item === 'object' && item && 'prePersist' in item && typeof item['prePersist'] === 'function') {
|
|
165
168
|
item = await item.prePersist() ?? item;
|
|
@@ -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,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 {
|
|
9
|
-
import {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
199
|
+
const content = await fs.readFile(file);
|
|
206
200
|
const text: StreamMeta = JSON.parse(content.toString('utf8'));
|
|
207
201
|
return text;
|
|
208
202
|
}
|
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> {
|
|
@@ -3,7 +3,7 @@ import { SchemaRegistry } from '@travetto/schema';
|
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
5
5
|
import { ModelRegistry } from './model';
|
|
6
|
-
import { DataHandler, IndexConfig, ModelOptions } from './types';
|
|
6
|
+
import { DataHandler, IndexConfig, ModelOptions, PrePersistScope } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Model decorator, extends `@Schema`
|
|
@@ -45,27 +45,33 @@ export function ExpiresAt() {
|
|
|
45
45
|
/**
|
|
46
46
|
* Model class decorator for pre-persist behavior
|
|
47
47
|
*/
|
|
48
|
-
export function PrePersist<T>(handler: DataHandler<T
|
|
48
|
+
export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope = 'all') {
|
|
49
49
|
return function (tgt: Class<T>): void {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
ModelRegistry.registerDataHandlers(tgt, {
|
|
51
|
+
prePersist: [{
|
|
52
|
+
scope,
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
54
|
+
handler: handler as DataHandler
|
|
55
|
+
}]
|
|
56
|
+
});
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Model field decorator for pre-persist value setting
|
|
57
62
|
*/
|
|
58
|
-
export function PersistValue<T>(handler: (curr: T | undefined) => T) {
|
|
63
|
+
export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
59
64
|
return function <K extends string, C extends Partial<Record<K, T>>>(tgt: C, prop: K): void {
|
|
60
65
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
61
66
|
ModelRegistry.registerDataHandlers(tgt.constructor as Class<C>, {
|
|
62
|
-
prePersist: [
|
|
63
|
-
|
|
67
|
+
prePersist: [{
|
|
68
|
+
scope,
|
|
69
|
+
handler: (inst): void => {
|
|
64
70
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
65
71
|
const cInst = (inst as unknown as Record<K, T>);
|
|
66
72
|
cInst[prop] = handler(cInst[prop]);
|
|
67
73
|
}
|
|
68
|
-
]
|
|
74
|
+
}]
|
|
69
75
|
});
|
|
70
76
|
};
|
|
71
77
|
}
|
package/src/registry/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Class } from '@travetto/base';
|
|
2
|
+
import { Primitive } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { ModelType } from '../types/model';
|
|
4
5
|
|
|
@@ -23,6 +24,8 @@ type IndexClauseRaw<T> = {
|
|
|
23
24
|
|
|
24
25
|
export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
|
|
25
26
|
|
|
27
|
+
export type PrePersistScope = 'full' | 'partial' | 'all';
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* Model options
|
|
28
31
|
*/
|
|
@@ -62,7 +65,7 @@ export class ModelOptions<T extends ModelType = ModelType> {
|
|
|
62
65
|
/**
|
|
63
66
|
* Pre-persist handlers
|
|
64
67
|
*/
|
|
65
|
-
prePersist?: DataHandler<unknown>[];
|
|
68
|
+
prePersist?: { scope: PrePersistScope, handler: DataHandler<unknown> }[];
|
|
66
69
|
/**
|
|
67
70
|
* Post-load handlers
|
|
68
71
|
*/
|
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/test/crud.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Suite, Test } from '@travetto/test';
|
|
4
|
-
import { Schema, Text, Precision, } from '@travetto/schema';
|
|
5
|
-
import { ModelCrudSupport, Model, NotFoundError } from '@travetto/model';
|
|
4
|
+
import { Schema, Text, Precision, Required, } from '@travetto/schema';
|
|
5
|
+
import { ModelCrudSupport, Model, NotFoundError, PersistValue } from '@travetto/model';
|
|
6
6
|
|
|
7
7
|
import { BaseModelSuite } from './base';
|
|
8
8
|
|
|
@@ -55,7 +55,14 @@ class User2 {
|
|
|
55
55
|
@Model()
|
|
56
56
|
class Dated {
|
|
57
57
|
id: string;
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
@PersistValue(v => v ?? new Date(), 'full')
|
|
60
|
+
@Required(false)
|
|
61
|
+
createdDate: Date;
|
|
62
|
+
|
|
63
|
+
@PersistValue(v => new Date())
|
|
64
|
+
@Required(false)
|
|
65
|
+
updatedDate: Date;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
@Suite()
|
|
@@ -204,11 +211,29 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
204
211
|
@Test('verify dates')
|
|
205
212
|
async testDates() {
|
|
206
213
|
const service = await this.service;
|
|
207
|
-
const res = await service.create(Dated, Dated.from({
|
|
214
|
+
const res = await service.create(Dated, Dated.from({ createdDate: new Date() }));
|
|
215
|
+
|
|
216
|
+
assert(res.createdDate instanceof Date);
|
|
217
|
+
}
|
|
208
218
|
|
|
209
|
-
|
|
219
|
+
@Test('verify prepersist on create/update')
|
|
220
|
+
async testPrePersist() {
|
|
221
|
+
const service = await this.service;
|
|
222
|
+
const res = await service.create(Dated, Dated.from({}));
|
|
223
|
+
const created = res.createdDate;
|
|
224
|
+
assert(res.createdDate instanceof Date);
|
|
225
|
+
assert(res.updatedDate instanceof Date);
|
|
226
|
+
|
|
227
|
+
await new Promise(r => setTimeout(r, 100));
|
|
228
|
+
|
|
229
|
+
const final = await service.updatePartial(Dated, { id: res.id });
|
|
230
|
+
assert(final.createdDate instanceof Date);
|
|
231
|
+
assert(final.createdDate.getTime() === created?.getTime());
|
|
232
|
+
assert(final.updatedDate instanceof Date);
|
|
233
|
+
assert(final.createdDate.getTime() < final.updatedDate?.getTime());
|
|
210
234
|
}
|
|
211
235
|
|
|
236
|
+
|
|
212
237
|
@Test('verify list')
|
|
213
238
|
async list() {
|
|
214
239
|
const service = await this.service;
|
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
|
}
|