@travetto/model 5.0.0-rc.9 → 5.0.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/README.md +80 -146
- package/__index__.ts +1 -4
- package/package.json +7 -7
- package/src/internal/service/blob.ts +47 -0
- package/src/internal/service/common.ts +14 -36
- package/src/internal/service/crud.ts +22 -45
- package/src/internal/service/indexed.ts +0 -1
- package/src/service/blob.ts +36 -0
- package/support/doc.support.tsx +2 -2
- package/support/fixtures/alpha.txt +1 -0
- package/support/fixtures/empty +0 -0
- package/support/fixtures/empty.m4a +0 -0
- package/support/fixtures/logo.gif +0 -0
- package/support/fixtures/logo.png +0 -0
- package/support/fixtures/small-audio +0 -0
- package/support/fixtures/small-audio.mp3 +0 -0
- package/support/test/blob.ts +136 -0
- package/support/test/crud.ts +8 -8
- package/support/test/polymorphism.ts +3 -0
- package/support/test/suite.ts +5 -5
- package/src/internal/service/stream.ts +0 -22
- package/src/provider/file.ts +0 -231
- package/src/provider/memory.ts +0 -339
- package/src/service/stream.ts +0 -72
- package/support/test/stream.ts +0 -113
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ yarn add @travetto/model
|
|
|
16
16
|
This module provides a set of contracts/interfaces to data model persistence, modification and retrieval. This module builds heavily upon the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding."), which is used for data model validation.
|
|
17
17
|
|
|
18
18
|
## Contracts
|
|
19
|
-
The module is mainly composed of contracts. The contracts define the expected interface for various model patterns. The primary contracts are [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/service/basic.ts#L9), [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11), [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/service/indexed.ts#L12), [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11), [
|
|
19
|
+
The module is mainly composed of contracts. The contracts define the expected interface for various model patterns. The primary contracts are [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/service/basic.ts#L9), [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11), [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/service/indexed.ts#L12), [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11), [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8) and [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L19).
|
|
20
20
|
|
|
21
21
|
### Basic
|
|
22
22
|
All [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementations, must honor the [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/service/basic.ts#L9) contract to be able to participate in the model ecosystem. This contract represents the bare minimum for a model service.
|
|
@@ -153,38 +153,39 @@ export interface ModelExpirySupport extends ModelCrudSupport {
|
|
|
153
153
|
}
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
###
|
|
157
|
-
Some implementations also allow for the ability to read/write binary data as
|
|
156
|
+
### Blob
|
|
157
|
+
Some implementations also allow for the ability to read/write binary data as [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8). Given that all implementations can store [Base64](https://en.wikipedia.org/wiki/Base64) encoded data, the key differentiator here, is native support for streaming data, as well as being able to store binary data of significant sizes.
|
|
158
158
|
|
|
159
|
-
**Code:
|
|
159
|
+
**Code: Blob Contract**
|
|
160
160
|
```typescript
|
|
161
|
-
export interface
|
|
161
|
+
export interface ModelBlobSupport {
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
* Upsert
|
|
165
|
-
* @param location The location of the
|
|
166
|
-
* @param input The actual
|
|
167
|
-
* @param meta
|
|
164
|
+
* Upsert blob to storage
|
|
165
|
+
* @param location The location of the blob
|
|
166
|
+
* @param input The actual blob to write
|
|
167
|
+
* @param meta Additional metadata to store with the blob
|
|
168
|
+
* @param overwrite Should we replace content if already found, defaults to true
|
|
168
169
|
*/
|
|
169
|
-
|
|
170
|
+
upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite?: boolean): Promise<void>;
|
|
170
171
|
|
|
171
172
|
/**
|
|
172
|
-
* Get
|
|
173
|
-
* @param location The location of the
|
|
173
|
+
* Get blob from storage
|
|
174
|
+
* @param location The location of the blob
|
|
174
175
|
*/
|
|
175
|
-
|
|
176
|
+
getBlob(location: string, range?: ByteRange): Promise<Blob>;
|
|
176
177
|
|
|
177
178
|
/**
|
|
178
|
-
* Get metadata for
|
|
179
|
-
* @param location The location of the
|
|
179
|
+
* Get metadata for blob
|
|
180
|
+
* @param location The location of the blob
|
|
180
181
|
*/
|
|
181
|
-
|
|
182
|
+
describeBlob(location: string): Promise<BlobMeta>;
|
|
182
183
|
|
|
183
184
|
/**
|
|
184
|
-
* Delete
|
|
185
|
-
* @param location The location of the
|
|
185
|
+
* Delete blob by location
|
|
186
|
+
* @param location The location of the blob
|
|
186
187
|
*/
|
|
187
|
-
|
|
188
|
+
deleteBlob(location: string): Promise<void>;
|
|
188
189
|
}
|
|
189
190
|
```
|
|
190
191
|
|
|
@@ -216,8 +217,8 @@ export interface ModelType {
|
|
|
216
217
|
The `id` is the only required field for a model, as this is a hard requirement on naming and type. This may make using existing data models impossible if types other than strings are required. Additionally, the `type` field, is intended to record the base model type, but can be remapped. This is important to support polymorphism, not only in [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."), but also in [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.").
|
|
217
218
|
|
|
218
219
|
## Implementations
|
|
219
|
-
|Service|Basic|CRUD|Indexed|Expiry|
|
|
220
|
-
|
|
220
|
+
|Service|Basic|CRUD|Indexed|Expiry|Blob|Bulk|
|
|
221
|
+
|-------|-----|----|-------|------|----|----|
|
|
221
222
|
|[DynamoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-dynamodb#readme "DynamoDB backing for the travetto model module.")|X|X|X|X| | |
|
|
222
223
|
|[Elasticsearch Model Source](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch#readme "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.")|X|X|X|X| |X|
|
|
223
224
|
|[Firestore Model Support](https://github.com/travetto/travetto/tree/main/module/model-firestore#readme "Firestore backing for the travetto model module.")|X|X|X| | | |
|
|
@@ -225,142 +226,76 @@ The `id` is the only required field for a model, as this is a hard requirement o
|
|
|
225
226
|
|[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| ||
|
|
226
227
|
|[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| |
|
|
227
228
|
|[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|
|
|
228
|
-
|[
|
|
229
|
-
|[
|
|
229
|
+
|[Memory Model Support](https://github.com/travetto/travetto/tree/main/module/model-memory#readme "Memory backing for the travetto model module.")|X|X|X|X|X|X|
|
|
230
|
+
|[File Model Support](https://github.com/travetto/travetto/tree/main/module/model-file#readme "File system backing for the travetto model module.")|X|X| |X|X|X|
|
|
230
231
|
|
|
231
232
|
## Custom Model Service
|
|
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.
|
|
233
|
-
|
|
234
|
-
**Code: Memory Service**
|
|
235
|
-
```typescript
|
|
236
|
-
import { Readable } from 'node:stream';
|
|
237
|
-
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
238
|
-
import { Class, TimeSpan, DeepPartial, castTo } from '@travetto/runtime';
|
|
239
|
-
import { Injectable } from '@travetto/di';
|
|
240
|
-
import { Config } from '@travetto/config';
|
|
241
|
-
import { ModelCrudSupport } from '../service/crud';
|
|
242
|
-
import { ModelStreamSupport, StreamMeta, StreamRange } from '../service/stream';
|
|
243
|
-
import { ModelType, OptionalId } from '../types/model';
|
|
244
|
-
import { ModelExpirySupport } from '../service/expiry';
|
|
245
|
-
import { ModelRegistry } from '../registry/model';
|
|
246
|
-
import { ModelStorageSupport } from '../service/storage';
|
|
247
|
-
import { ModelCrudUtil } from '../internal/service/crud';
|
|
248
|
-
import { ModelExpiryUtil } from '../internal/service/expiry';
|
|
249
|
-
import { NotFoundError } from '../error/not-found';
|
|
250
|
-
import { ExistsError } from '../error/exists';
|
|
251
|
-
import { ModelIndexedSupport } from '../service/indexed';
|
|
252
|
-
import { ModelIndexedUtil } from '../internal/service/indexed';
|
|
253
|
-
import { ModelStorageUtil } from '../internal/service/storage';
|
|
254
|
-
import { enforceRange, StreamModel, STREAMS } from '../internal/service/stream';
|
|
255
|
-
import { IndexConfig } from '../registry/types';
|
|
256
|
-
const STREAM_META = `${STREAMS}_meta`;
|
|
257
|
-
type StoreType = Map<string, Buffer>;
|
|
258
|
-
@Config('model.memory')
|
|
259
|
-
export class MemoryModelConfig {
|
|
260
|
-
autoCreate?: boolean = true;
|
|
261
|
-
namespace?: string;
|
|
262
|
-
cullRate?: number | TimeSpan;
|
|
263
|
-
}
|
|
264
|
-
function indexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, suffix?: string): string {
|
|
265
|
-
return [cls.Ⲑid, typeof idx === 'string' ? idx : idx.name, suffix].filter(x => !!x).join(':');
|
|
266
|
-
}
|
|
267
|
-
function getFirstId(data: Map<string, unknown> | Set<string>, value?: string | number): string | undefined {
|
|
268
|
-
let id: string | undefined;
|
|
269
|
-
if (data instanceof Set) {
|
|
270
|
-
id = data.values().next().value;
|
|
271
|
-
} else {
|
|
272
|
-
id = [...data.entries()].find(([k, v]) => value === undefined || v === value)?.[0];
|
|
273
|
-
}
|
|
274
|
-
return id;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Standard in-memory support
|
|
278
|
-
*/
|
|
279
|
-
@Injectable()
|
|
280
|
-
export class MemoryModelService implements ModelCrudSupport, ModelStreamSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
281
|
-
sorted: new Map<string, Map<string, Map<string, number>>>(),
|
|
282
|
-
unsorted: new Map<string, Map<string, Set<string>>>()
|
|
283
|
-
};
|
|
284
|
-
idSource = ModelCrudUtil.uuidSource();
|
|
285
|
-
get client(): Map<string, StoreType>;
|
|
286
|
-
constructor(public readonly config: MemoryModelConfig) { }
|
|
287
|
-
async postConstruct(): Promise<void>;
|
|
288
|
-
// CRUD Support
|
|
289
|
-
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T>;
|
|
290
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T>;
|
|
291
|
-
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T>;
|
|
292
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T>;
|
|
293
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T>;
|
|
294
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void>;
|
|
295
|
-
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T>;
|
|
296
|
-
// Stream Support
|
|
297
|
-
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void>;
|
|
298
|
-
async getStream(location: string, range?: StreamRange): Promise<Readable>;
|
|
299
|
-
async describeStream(location: string): Promise<StreamMeta>;
|
|
300
|
-
async deleteStream(location: string): Promise<void>;
|
|
301
|
-
// Expiry
|
|
302
|
-
async deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number>;
|
|
303
|
-
// Storage Support
|
|
304
|
-
async createStorage(): Promise<void>;
|
|
305
|
-
async deleteStorage(): Promise<void>;
|
|
306
|
-
async createModel<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
307
|
-
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
308
|
-
// Indexed
|
|
309
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
|
|
310
|
-
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
|
|
311
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
|
|
312
|
-
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
|
|
313
|
-
}
|
|
314
|
-
```
|
|
233
|
+
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. [Memory Model Support](https://github.com/travetto/travetto/tree/main/module/model-memory#readme "Memory backing for the travetto model module.") serves as a great example of what a full featured implementation can look like.
|
|
315
234
|
|
|
316
235
|
To enforce that these contracts are honored, the module provides shared test suites to allow for custom implementations to ensure they are adhering to the contract's expected behavior.
|
|
317
236
|
|
|
318
237
|
**Code: Memory Service Test Configuration**
|
|
319
238
|
```typescript
|
|
320
|
-
import {
|
|
321
|
-
|
|
322
|
-
import { MemoryModelConfig, MemoryModelService } from '../src/provider/memory';
|
|
323
|
-
import { ModelCrudSuite } from '../support/test/crud';
|
|
324
|
-
import { ModelExpirySuite } from '../support/test/expiry';
|
|
325
|
-
import { ModelStreamSuite } from '../support/test/stream';
|
|
326
|
-
import { ModelIndexedSuite } from '../support/test/indexed';
|
|
327
|
-
import { ModelBasicSuite } from '../support/test/basic';
|
|
328
|
-
import { ModelPolymorphismSuite } from '../support/test/polymorphism';
|
|
329
|
-
|
|
330
|
-
@Suite()
|
|
331
|
-
export class MemoryBasicSuite extends ModelBasicSuite {
|
|
332
|
-
serviceClass = MemoryModelService;
|
|
333
|
-
configClass = MemoryModelConfig;
|
|
334
|
-
}
|
|
239
|
+
import { DependencyRegistry } from '@travetto/di';
|
|
240
|
+
import { AppError, castTo, Class, classConstruct } from '@travetto/runtime';
|
|
335
241
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
configClass = MemoryModelConfig;
|
|
340
|
-
}
|
|
242
|
+
import { isBulkSupported, isCrudSupported } from '../../src/internal/service/common';
|
|
243
|
+
import { ModelType } from '../../src/types/model';
|
|
244
|
+
import { ModelSuite } from './suite';
|
|
341
245
|
|
|
342
|
-
|
|
343
|
-
export class MemoryStreamSuite extends ModelStreamSuite {
|
|
344
|
-
serviceClass = MemoryModelService;
|
|
345
|
-
configClass = MemoryModelConfig;
|
|
346
|
-
}
|
|
246
|
+
type ServiceClass = { serviceClass: { new(): unknown } };
|
|
347
247
|
|
|
348
|
-
@
|
|
349
|
-
export class
|
|
350
|
-
serviceClass = MemoryModelService;
|
|
351
|
-
configClass = MemoryModelConfig;
|
|
352
|
-
}
|
|
248
|
+
@ModelSuite()
|
|
249
|
+
export abstract class BaseModelSuite<T> {
|
|
353
250
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
251
|
+
static ifNot(pred: (svc: unknown) => boolean): (x: unknown) => Promise<boolean> {
|
|
252
|
+
return async (x: unknown) => !pred(classConstruct(castTo<ServiceClass>(x).serviceClass));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
serviceClass: Class<T>;
|
|
256
|
+
configClass: Class;
|
|
257
|
+
|
|
258
|
+
async getSize<U extends ModelType>(cls: Class<U>): Promise<number> {
|
|
259
|
+
const svc = (await this.service);
|
|
260
|
+
if (isCrudSupported(svc)) {
|
|
261
|
+
let i = 0;
|
|
262
|
+
for await (const __el of svc.list(cls)) {
|
|
263
|
+
i += 1;
|
|
264
|
+
}
|
|
265
|
+
return i;
|
|
266
|
+
} else {
|
|
267
|
+
throw new AppError(`Size is not supported for this service: ${this.serviceClass.name}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
359
270
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
271
|
+
async saveAll<M extends ModelType>(cls: Class<M>, items: M[]): Promise<number> {
|
|
272
|
+
const svc = await this.service;
|
|
273
|
+
if (isBulkSupported(svc)) {
|
|
274
|
+
const res = await svc.processBulk(cls, items.map(x => ({ insert: x })));
|
|
275
|
+
return res.counts.insert;
|
|
276
|
+
} else if (isCrudSupported(svc)) {
|
|
277
|
+
const out: Promise<M>[] = [];
|
|
278
|
+
for (const el of items) {
|
|
279
|
+
out.push(svc.create(cls, el));
|
|
280
|
+
}
|
|
281
|
+
await Promise.all(out);
|
|
282
|
+
return out.length;
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error('Service does not support crud operations');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get service(): Promise<T> {
|
|
289
|
+
return DependencyRegistry.getInstance(this.serviceClass);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
293
|
+
const out: U[] = [];
|
|
294
|
+
for await (const el of src) {
|
|
295
|
+
out.push(el);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
364
299
|
}
|
|
365
300
|
```
|
|
366
301
|
|
|
@@ -403,7 +338,6 @@ Options:
|
|
|
403
338
|
|
|
404
339
|
Providers
|
|
405
340
|
--------------------
|
|
406
|
-
* Memory
|
|
407
341
|
* SQL
|
|
408
342
|
|
|
409
343
|
Models
|
package/__index__.ts
CHANGED
|
@@ -3,15 +3,12 @@ export * from './src/registry/model';
|
|
|
3
3
|
export * from './src/registry/types';
|
|
4
4
|
export * from './src/types/model';
|
|
5
5
|
export * from './src/service/basic';
|
|
6
|
+
export * from './src/service/blob';
|
|
6
7
|
export * from './src/service/bulk';
|
|
7
8
|
export * from './src/service/crud';
|
|
8
9
|
export * from './src/service/indexed';
|
|
9
10
|
export * from './src/service/expiry';
|
|
10
11
|
export * from './src/service/storage';
|
|
11
|
-
export * from './src/service/stream';
|
|
12
|
-
|
|
13
|
-
export * from './src/provider/file';
|
|
14
|
-
export * from './src/provider/memory';
|
|
15
12
|
|
|
16
13
|
export * from './src/error/exists';
|
|
17
14
|
export * from './src/error/not-found';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.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": "^5.0.
|
|
30
|
-
"@travetto/di": "^5.0.
|
|
31
|
-
"@travetto/registry": "^5.0.
|
|
32
|
-
"@travetto/schema": "^5.0.
|
|
29
|
+
"@travetto/config": "^5.0.1",
|
|
30
|
+
"@travetto/di": "^5.0.1",
|
|
31
|
+
"@travetto/registry": "^5.0.1",
|
|
32
|
+
"@travetto/schema": "^5.0.1"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^5.0.
|
|
36
|
-
"@travetto/test": "^5.0.
|
|
35
|
+
"@travetto/cli": "^5.0.1",
|
|
36
|
+
"@travetto/test": "^5.0.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { Class, AppError, BinaryInput, BinaryUtil, BlobMeta, ByteRange } from '@travetto/runtime';
|
|
3
|
+
import { ModelType } from '../../types/model';
|
|
4
|
+
|
|
5
|
+
export const ModelBlobNamespace = '__blobs';
|
|
6
|
+
export const MODEL_BLOB: Class<ModelType> = class { id: string; };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Utilities for processing assets
|
|
10
|
+
*/
|
|
11
|
+
export class ModelBlobUtil {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert input to a Readable, and get what metadata is available
|
|
15
|
+
*/
|
|
16
|
+
static async getInput(src: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
|
|
17
|
+
let input: Readable;
|
|
18
|
+
if (src instanceof Blob) {
|
|
19
|
+
metadata = { ...BinaryUtil.getBlobMeta(src), ...metadata };
|
|
20
|
+
metadata.size ??= src.size;
|
|
21
|
+
input = Readable.fromWeb(src.stream());
|
|
22
|
+
} else if (typeof src === 'object' && 'pipeThrough' in src) {
|
|
23
|
+
input = Readable.fromWeb(src);
|
|
24
|
+
} else if (typeof src === 'object' && 'pipe' in src) {
|
|
25
|
+
input = src;
|
|
26
|
+
} else {
|
|
27
|
+
metadata.size = src.length;
|
|
28
|
+
input = Readable.from(src);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [input, metadata ?? {}];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Enforce byte range for stream stream/file of a certain size
|
|
36
|
+
*/
|
|
37
|
+
static enforceRange({ start, end }: ByteRange, size: number): Required<ByteRange> {
|
|
38
|
+
// End is inclusive
|
|
39
|
+
end = Math.min(end ?? (size - 1), size - 1);
|
|
40
|
+
|
|
41
|
+
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
|
|
42
|
+
throw new AppError('Invalid position, out of range', 'data');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { start, end };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,72 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasFunction } from '@travetto/runtime';
|
|
2
2
|
import type { ModelBulkSupport } from '../../service/bulk';
|
|
3
|
-
import { ModelCrudSupport } from '../../service/crud';
|
|
3
|
+
import type { ModelCrudSupport } from '../../service/crud';
|
|
4
4
|
import type { ModelExpirySupport } from '../../service/expiry';
|
|
5
|
-
import { ModelIndexedSupport } from '../../service/indexed';
|
|
5
|
+
import type { ModelIndexedSupport } from '../../service/indexed';
|
|
6
6
|
import type { ModelStorageSupport } from '../../service/storage';
|
|
7
|
-
import type {
|
|
7
|
+
import type { ModelBlobSupport } from '../../service/blob';
|
|
8
8
|
|
|
9
9
|
export class ModelBasicSupportTarget { }
|
|
10
10
|
export class ModelCrudSupportTarget { }
|
|
11
11
|
export class ModelBulkSupportTarget { }
|
|
12
12
|
export class ModelStorageSupportTarget { }
|
|
13
|
+
export class ModelBlobSupportTarget { }
|
|
13
14
|
export class ModelExpirySupportTarget { }
|
|
14
|
-
export class ModelStreamSupportTarget { }
|
|
15
15
|
export class ModelIndexedSupportTarget { }
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Type guard for determining if service supports basic operations
|
|
19
|
-
* @param o
|
|
20
19
|
*/
|
|
21
|
-
export
|
|
22
|
-
return !!o && 'create' in o;
|
|
23
|
-
}
|
|
20
|
+
export const isBasicSupported = hasFunction<ModelBulkSupport>('create');
|
|
24
21
|
|
|
25
22
|
/**
|
|
26
23
|
* Type guard for determining if service supports crud operations
|
|
27
|
-
* @param o
|
|
28
24
|
*/
|
|
29
|
-
export
|
|
30
|
-
return !!o && 'upsert' in o;
|
|
31
|
-
}
|
|
25
|
+
export const isCrudSupported = hasFunction<ModelCrudSupport>('upsert');
|
|
32
26
|
|
|
33
27
|
/**
|
|
34
28
|
* Type guard for determining if model supports expiry
|
|
35
|
-
* @param o
|
|
36
29
|
*/
|
|
37
|
-
export
|
|
38
|
-
return !!o && 'deleteExpired' in o;
|
|
39
|
-
}
|
|
30
|
+
export const isExpirySupported = hasFunction<ModelExpirySupport>('deleteExpired');
|
|
40
31
|
|
|
41
32
|
/**
|
|
42
|
-
* Type guard for determining if service supports
|
|
43
|
-
* @param o
|
|
33
|
+
* Type guard for determining if service supports streaming operation
|
|
44
34
|
*/
|
|
45
|
-
export
|
|
46
|
-
return !!o && 'createStorage' in o;
|
|
47
|
-
}
|
|
35
|
+
export const isBlobSupported = hasFunction<ModelBlobSupport>('getBlob');
|
|
48
36
|
|
|
49
37
|
/**
|
|
50
|
-
* Type guard for determining if service supports
|
|
51
|
-
* @param o
|
|
38
|
+
* Type guard for determining if service supports storage operation
|
|
52
39
|
*/
|
|
53
|
-
export
|
|
54
|
-
return !!o && 'getStream' in o;
|
|
55
|
-
}
|
|
40
|
+
export const isStorageSupported = hasFunction<ModelStorageSupport>('createStorage');
|
|
56
41
|
|
|
57
42
|
/**
|
|
58
43
|
* Type guard for determining if service supports streaming operation
|
|
59
|
-
* @param o
|
|
60
44
|
*/
|
|
61
|
-
export
|
|
62
|
-
return !!o && 'processBulk' in o;
|
|
63
|
-
}
|
|
45
|
+
export const isBulkSupported = hasFunction<ModelBulkSupport>('processBulk');
|
|
64
46
|
|
|
65
47
|
/**
|
|
66
48
|
* Type guard for determining if service supports indexed operation
|
|
67
|
-
* @param o
|
|
68
49
|
*/
|
|
69
|
-
export
|
|
70
|
-
return !!o && 'getByIndex' in o;
|
|
71
|
-
}
|
|
72
|
-
|
|
50
|
+
export const isIndexedSupported = hasFunction<ModelIndexedSupport>('getByIndex');
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { castTo, Class, asFull, Util, asConstructable } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class, Util, asConstructable, AppError } from '@travetto/runtime';
|
|
4
2
|
import { DataUtil, SchemaRegistry, SchemaValidator, ValidationError, ValidationResultError } from '@travetto/schema';
|
|
5
3
|
|
|
6
4
|
import { ModelRegistry } from '../../registry/model';
|
|
@@ -28,18 +26,6 @@ export class ModelCrudUtil {
|
|
|
28
26
|
return { create, valid };
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
* Provide hash value
|
|
33
|
-
* @param value Input value
|
|
34
|
-
* @param length Number of characters to produce
|
|
35
|
-
*/
|
|
36
|
-
static hashValue(value: string, length = 32): string {
|
|
37
|
-
if (value.length < 32) {
|
|
38
|
-
value = value.padEnd(32, ' ');
|
|
39
|
-
}
|
|
40
|
-
return crypto.createHash('sha1').update(value).digest('hex').substring(0, length);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
29
|
/**
|
|
44
30
|
* Load model
|
|
45
31
|
* @param cls Class to load model for
|
|
@@ -109,36 +95,6 @@ export class ModelCrudUtil {
|
|
|
109
95
|
return castTo(item);
|
|
110
96
|
}
|
|
111
97
|
|
|
112
|
-
/**
|
|
113
|
-
* Performs a naive partial update by fetching, patching, and then storing
|
|
114
|
-
* @param cls Type to store for
|
|
115
|
-
* @param item The object to use for a partial update
|
|
116
|
-
* @param view The schema view to validate against
|
|
117
|
-
* @param getExisting How to fetch an existing item
|
|
118
|
-
*/
|
|
119
|
-
static async naivePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view: undefined | string, getExisting: () => Promise<T>): Promise<T> {
|
|
120
|
-
if (DataUtil.isPlainObject(item)) {
|
|
121
|
-
item = cls.from(castTo(item));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const config = ModelRegistry.get(asConstructable(item).constructor);
|
|
125
|
-
if (config.subType) { // Sub-typing, assign type
|
|
126
|
-
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (view) {
|
|
130
|
-
await SchemaValidator.validate(cls, item, view);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const existing = await getExisting();
|
|
134
|
-
|
|
135
|
-
item = Object.assign(existing, item);
|
|
136
|
-
|
|
137
|
-
item = await this.prePersist(cls, item, 'partial');
|
|
138
|
-
|
|
139
|
-
return asFull(item);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
98
|
/**
|
|
143
99
|
* Ensure subtype is not supported
|
|
144
100
|
*/
|
|
@@ -178,4 +134,25 @@ export class ModelCrudUtil {
|
|
|
178
134
|
}
|
|
179
135
|
return item;
|
|
180
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Ensure everything is correct for a partial update
|
|
140
|
+
*/
|
|
141
|
+
static async prePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view?: string): Promise<Partial<T>> {
|
|
142
|
+
if (!DataUtil.isPlainObject(item)) {
|
|
143
|
+
throw new AppError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, 'data');
|
|
144
|
+
}
|
|
145
|
+
const res = await this.prePersist(cls, castTo(item), 'partial');
|
|
146
|
+
await SchemaValidator.validatePartial(cls, item, view);
|
|
147
|
+
return res;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ensure everything is correct for a partial update
|
|
152
|
+
*/
|
|
153
|
+
static async naivePartialUpdate<T extends ModelType>(cls: Class<T>, get: () => Promise<T>, item: Partial<T>, view?: string): Promise<T> {
|
|
154
|
+
const prepared = await this.prePartialUpdate(cls, item, view);
|
|
155
|
+
const full = await get();
|
|
156
|
+
return cls.from(castTo({ ...full, ...prepared }));
|
|
157
|
+
}
|
|
181
158
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BinaryInput, BlobMeta, ByteRange } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Support for Blobs CRUD.
|
|
5
|
+
*
|
|
6
|
+
* @concrete ../internal/service/common#ModelBlobSupportTarget
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelBlobSupport {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upsert blob to storage
|
|
12
|
+
* @param location The location of the blob
|
|
13
|
+
* @param input The actual blob to write
|
|
14
|
+
* @param meta Additional metadata to store with the blob
|
|
15
|
+
* @param overwrite Should we replace content if already found, defaults to true
|
|
16
|
+
*/
|
|
17
|
+
upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite?: boolean): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get blob from storage
|
|
21
|
+
* @param location The location of the blob
|
|
22
|
+
*/
|
|
23
|
+
getBlob(location: string, range?: ByteRange): Promise<Blob>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get metadata for blob
|
|
27
|
+
* @param location The location of the blob
|
|
28
|
+
*/
|
|
29
|
+
describeBlob(location: string): Promise<BlobMeta>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Delete blob by location
|
|
33
|
+
* @param location The location of the blob
|
|
34
|
+
*/
|
|
35
|
+
deleteBlob(location: string): Promise<void>;
|
|
36
|
+
}
|
package/support/doc.support.tsx
CHANGED
|
@@ -8,14 +8,14 @@ export const Links = {
|
|
|
8
8
|
Expiry: d.codeLink('Expiry', '@travetto/model/src/service/expiry.ts', /export interface/),
|
|
9
9
|
Indexed: d.codeLink('Indexed', '@travetto/model/src/service/indexed.ts', /export interface/),
|
|
10
10
|
Bulk: d.codeLink('Bulk', '@travetto/model/src/service/bulk.ts', /export interface/),
|
|
11
|
-
|
|
11
|
+
Blob: d.codeLink('Blob', '@travetto/model/src/service/blob.ts', /export interface/),
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const ModelTypes = (fn: | Function): DocJSXElement[] => {
|
|
15
15
|
const { content } = DocFileUtil.readSource(fn);
|
|
16
16
|
const found: DocJSXElementByFn<'CodeLink'>[] = [];
|
|
17
17
|
const seen = new Set();
|
|
18
|
-
for (const [, key] of content.matchAll(/Model(Crud|Expiry|Indexed|Bulk|
|
|
18
|
+
for (const [, key] of content.matchAll(/Model(Crud|Expiry|Indexed|Bulk|Blob)Support/g)) {
|
|
19
19
|
if (!seen.has(key)) {
|
|
20
20
|
seen.add(key);
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abcdefghijklmnopqrstuvwxyz
|
|
File without changes
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|