@travetto/model 5.0.0-rc.8 → 5.0.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 +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 +20 -26
- package/src/internal/service/crud.ts +15 -21
- package/src/internal/service/expiry.ts +2 -5
- package/src/internal/service/indexed.ts +13 -25
- package/src/internal/service/storage.ts +1 -4
- package/src/registry/decorator.ts +7 -12
- package/src/registry/model.ts +5 -6
- package/src/service/blob.ts +36 -0
- package/support/bin/candidate.ts +2 -3
- 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/base.ts +3 -3
- package/support/test/blob.ts +136 -0
- package/support/test/crud.ts +3 -3
- package/support/test/polymorphism.ts +7 -6
- package/support/test/suite.ts +5 -5
- package/src/internal/service/stream.ts +0 -22
- package/src/provider/file.ts +0 -233
- package/src/provider/memory.ts +0 -341
- package/src/service/stream.ts +0 -72
- package/support/test/stream.ts +0 -110
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, DeepPartial } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class, DeepPartial, TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { IndexNotSupported } from '../../error/invalid-index';
|
|
4
4
|
import { NotFoundError } from '../../error/not-found';
|
|
@@ -44,38 +44,30 @@ export class ModelIndexedUtil {
|
|
|
44
44
|
const parts = [];
|
|
45
45
|
|
|
46
46
|
while (o !== undefined && o !== null) {
|
|
47
|
-
const k =
|
|
48
|
-
|
|
49
|
-
o = (o[k] as Record<string, unknown>);
|
|
47
|
+
const k = TypedObject.keys(f)[0];
|
|
48
|
+
o = castTo(o[k]);
|
|
50
49
|
parts.push(k);
|
|
51
|
-
|
|
52
|
-
const fk = k as (keyof typeof f);
|
|
53
|
-
if (typeof f[fk] === 'boolean' || typeof f[fk] === 'number') {
|
|
50
|
+
if (typeof f[k] === 'boolean' || typeof f[k] === 'number') {
|
|
54
51
|
if (cfg.type === 'sorted') {
|
|
55
|
-
|
|
56
|
-
sortDir = f[fk] === true ? 1 : f[fk] as number;
|
|
52
|
+
sortDir = f[k] === true ? 1 : f[k] === false ? 0 : f[k];
|
|
57
53
|
}
|
|
58
54
|
break; // At the bottom
|
|
59
55
|
} else {
|
|
60
|
-
|
|
61
|
-
f = f[fk] as Record<string, unknown>;
|
|
56
|
+
f = castTo(f[k]);
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
if (field === sortField) {
|
|
65
|
-
|
|
66
|
-
sorted = { path: parts, dir: sortDir, value: o as unknown as number | Date };
|
|
60
|
+
sorted = { path: parts, dir: sortDir, value: castTo(o) };
|
|
67
61
|
}
|
|
68
62
|
if (o === undefined || o === null) {
|
|
69
63
|
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
70
64
|
if (empty === undefined || empty === Error) {
|
|
71
65
|
throw new IndexNotSupported(cls, cfg, `Missing field value for ${parts.join('.')}`);
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
o = empty as Record<string, unknown>;
|
|
67
|
+
o = castTo(empty!);
|
|
75
68
|
} else {
|
|
76
69
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
77
|
-
|
|
78
|
-
fields.push({ path: parts, value: o as unknown as string | boolean | Date | number });
|
|
70
|
+
fields.push({ path: parts, value: castTo(o) });
|
|
79
71
|
}
|
|
80
72
|
}
|
|
81
73
|
}
|
|
@@ -83,7 +75,6 @@ export class ModelIndexedUtil {
|
|
|
83
75
|
return { fields, sorted };
|
|
84
76
|
}
|
|
85
77
|
|
|
86
|
-
|
|
87
78
|
/**
|
|
88
79
|
* Project item via index
|
|
89
80
|
* @param cls Type to get index for
|
|
@@ -92,12 +83,11 @@ export class ModelIndexedUtil {
|
|
|
92
83
|
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, cfg?: ComputeConfig): Record<string, unknown> {
|
|
93
84
|
const res: Record<string, unknown> = {};
|
|
94
85
|
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, cfg).fields) {
|
|
95
|
-
let sub = res;
|
|
86
|
+
let sub: Record<string, unknown> = res;
|
|
96
87
|
const all = path.slice(0);
|
|
97
88
|
const last = all.pop()!;
|
|
98
89
|
for (const k of all) {
|
|
99
|
-
|
|
100
|
-
sub = (sub[k] ??= {}) as typeof res;
|
|
90
|
+
sub = castTo(sub[k] ??= {});
|
|
101
91
|
}
|
|
102
92
|
sub[last] = value;
|
|
103
93
|
}
|
|
@@ -134,11 +124,9 @@ export class ModelIndexedUtil {
|
|
|
134
124
|
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
135
125
|
): Promise<T> {
|
|
136
126
|
try {
|
|
137
|
-
|
|
138
|
-
const { id } = await service.getByIndex(cls, idx, body as DeepPartial<T>);
|
|
127
|
+
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
139
128
|
body.id = id;
|
|
140
|
-
|
|
141
|
-
return await service.update(cls, body as T);
|
|
129
|
+
return await service.update(cls, castTo(body));
|
|
142
130
|
} catch (err) {
|
|
143
131
|
if (err instanceof NotFoundError) {
|
|
144
132
|
return await service.create(cls, body);
|
|
@@ -12,14 +12,11 @@ export class ModelStorageUtil {
|
|
|
12
12
|
/**
|
|
13
13
|
* Register change listener on startup
|
|
14
14
|
*/
|
|
15
|
-
static async registerModelChangeListener(storage: ModelStorageSupport
|
|
15
|
+
static async registerModelChangeListener(storage: ModelStorageSupport): Promise<void> {
|
|
16
16
|
if (!Runtime.dynamic || !(storage?.config?.autoCreate ?? !Runtime.production)) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
|
-
target = target ?? storage.constructor as Class<ModelStorageSupport>;
|
|
22
|
-
|
|
23
20
|
const checkType = (cls: Class, enforceBase = true): boolean => {
|
|
24
21
|
if (enforceBase && ModelRegistry.getBaseModel(cls) !== cls) {
|
|
25
22
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/runtime';
|
|
1
|
+
import { asConstructable, castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistry } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
@@ -25,7 +25,7 @@ export function Model(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
|
25
25
|
* Defines an index on a model
|
|
26
26
|
*/
|
|
27
27
|
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
28
|
-
return function (target: Class<T>) {
|
|
28
|
+
return function (target: Class<T>): void {
|
|
29
29
|
ModelRegistry.getOrCreatePending(target).indices!.push(...indices);
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -36,8 +36,7 @@ export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
|
36
36
|
*/
|
|
37
37
|
export function ExpiresAt() {
|
|
38
38
|
return <K extends string, T extends Partial<Record<K, Date>>>(tgt: T, prop: K): void => {
|
|
39
|
-
|
|
40
|
-
ModelRegistry.register(tgt.constructor as Class<T>, { expiresAt: prop });
|
|
39
|
+
ModelRegistry.register(asConstructable(tgt).constructor, { expiresAt: prop });
|
|
41
40
|
};
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -49,8 +48,7 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
49
48
|
ModelRegistry.registerDataHandlers(tgt, {
|
|
50
49
|
prePersist: [{
|
|
51
50
|
scope,
|
|
52
|
-
|
|
53
|
-
handler: handler as DataHandler
|
|
51
|
+
handler: castTo(handler)
|
|
54
52
|
}]
|
|
55
53
|
});
|
|
56
54
|
};
|
|
@@ -61,13 +59,11 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
61
59
|
*/
|
|
62
60
|
export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
63
61
|
return function <K extends string, C extends Partial<Record<K, T>>>(tgt: C, prop: K): void {
|
|
64
|
-
|
|
65
|
-
ModelRegistry.registerDataHandlers(tgt.constructor as Class<C>, {
|
|
62
|
+
ModelRegistry.registerDataHandlers(asConstructable(tgt).constructor, {
|
|
66
63
|
prePersist: [{
|
|
67
64
|
scope,
|
|
68
65
|
handler: (inst): void => {
|
|
69
|
-
|
|
70
|
-
const cInst = (inst as unknown as Record<K, T>);
|
|
66
|
+
const cInst: Record<K, T> = castTo(inst);
|
|
71
67
|
cInst[prop] = handler(cInst[prop]);
|
|
72
68
|
}
|
|
73
69
|
}]
|
|
@@ -81,7 +77,6 @@ export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PreP
|
|
|
81
77
|
*/
|
|
82
78
|
export function PostLoad<T>(handler: DataHandler<T>) {
|
|
83
79
|
return function (tgt: Class<T>): void {
|
|
84
|
-
|
|
85
|
-
ModelRegistry.registerDataHandlers(tgt, { postLoad: [handler as DataHandler] });
|
|
80
|
+
ModelRegistry.registerDataHandlers(tgt, { postLoad: [castTo(handler)] });
|
|
86
81
|
};
|
|
87
82
|
}
|
package/src/registry/model.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SchemaRegistry } from '@travetto/schema';
|
|
2
2
|
import { MetadataRegistry } from '@travetto/registry';
|
|
3
3
|
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
-
import { AppError, Class, describeFunction } from '@travetto/runtime';
|
|
4
|
+
import { AppError, castTo, Class, describeFunction, asFull } from '@travetto/runtime';
|
|
5
5
|
import { AllViewⲐ } from '@travetto/schema/src/internal/types';
|
|
6
6
|
|
|
7
7
|
import { IndexConfig, IndexType, ModelOptions } from './types';
|
|
@@ -71,8 +71,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
onInstallFinalize(cls: Class): ModelOptions<ModelType> {
|
|
74
|
-
|
|
75
|
-
const config = this.pending.get(cls.Ⲑid)! as ModelOptions<ModelType>;
|
|
74
|
+
const config = asFull(this.pending.get(cls.Ⲑid)!);
|
|
76
75
|
|
|
77
76
|
const schema = SchemaRegistry.get(cls);
|
|
78
77
|
const view = schema.views[AllViewⲐ].schema;
|
|
@@ -103,7 +102,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
103
102
|
/**
|
|
104
103
|
* Find base class for a given model
|
|
105
104
|
*/
|
|
106
|
-
getBaseModel(cls: Class): Class<
|
|
105
|
+
getBaseModel<T extends ModelType>(cls: Class<T>): Class<T> {
|
|
107
106
|
if (!this.baseModels.has(cls)) {
|
|
108
107
|
let conf = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
109
108
|
let parent = cls;
|
|
@@ -208,12 +207,12 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
208
207
|
* Get expiry field
|
|
209
208
|
* @param cls
|
|
210
209
|
*/
|
|
211
|
-
getExpiry(cls: Class):
|
|
210
|
+
getExpiry<T extends ModelType>(cls: Class<T>): keyof T {
|
|
212
211
|
const expiry = this.get(cls).expiresAt;
|
|
213
212
|
if (!expiry) {
|
|
214
213
|
throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`, 'general');
|
|
215
214
|
}
|
|
216
|
-
return expiry;
|
|
215
|
+
return castTo(expiry);
|
|
217
216
|
}
|
|
218
217
|
}
|
|
219
218
|
|
|
@@ -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/bin/candidate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { ModelRegistry } from '@travetto/model/src/registry/model';
|
|
3
3
|
import { InjectableConfig, DependencyRegistry } from '@travetto/di';
|
|
4
4
|
import { ModelStorageSupportTarget } from '@travetto/model/src/internal/service/common';
|
|
@@ -40,8 +40,7 @@ export class ModelCandidateUtil {
|
|
|
40
40
|
* Get all providers that are viable candidates
|
|
41
41
|
*/
|
|
42
42
|
static async getProviders(op?: keyof ModelStorageSupport): Promise<InjectableConfig[]> {
|
|
43
|
-
|
|
44
|
-
const types = DependencyRegistry.getCandidateTypes<ModelStorageSupport>(ModelStorageSupportTarget as unknown as Class<ModelStorageSupport>);
|
|
43
|
+
const types = DependencyRegistry.getCandidateTypes<ModelStorageSupport>(castTo(ModelStorageSupportTarget));
|
|
45
44
|
return types.filter(x => !op || x.class.prototype?.[op]);
|
|
46
45
|
}
|
|
47
46
|
|
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
|
package/support/test/base.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DependencyRegistry } from '@travetto/di';
|
|
2
|
-
import { AppError, Class } from '@travetto/runtime';
|
|
2
|
+
import { AppError, castTo, Class, classConstruct } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { isBulkSupported, isCrudSupported } from '../../src/internal/service/common';
|
|
5
5
|
import { ModelType } from '../../src/types/model';
|
|
@@ -11,7 +11,7 @@ type ServiceClass = { serviceClass: { new(): unknown } };
|
|
|
11
11
|
export abstract class BaseModelSuite<T> {
|
|
12
12
|
|
|
13
13
|
static ifNot(pred: (svc: unknown) => boolean): (x: unknown) => Promise<boolean> {
|
|
14
|
-
return async (x: unknown) => !pred(
|
|
14
|
+
return async (x: unknown) => !pred(classConstruct(castTo<ServiceClass>(x).serviceClass));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
serviceClass: Class<T>;
|
|
@@ -36,7 +36,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
36
36
|
const res = await svc.processBulk(cls, items.map(x => ({ insert: x })));
|
|
37
37
|
return res.counts.insert;
|
|
38
38
|
} else if (isCrudSupported(svc)) {
|
|
39
|
-
const out
|
|
39
|
+
const out: Promise<M>[] = [];
|
|
40
40
|
for (const el of items) {
|
|
41
41
|
out.push(svc.create(cls, el));
|
|
42
42
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
4
|
+
import { BaseModelSuite } from '@travetto/model/support/test/base';
|
|
5
|
+
import { BinaryUtil, Util } from '@travetto/runtime';
|
|
6
|
+
|
|
7
|
+
import { ModelBlobSupport } from '../../src/service/blob';
|
|
8
|
+
import { ModelBlobUtil } from '../../src/internal/service/blob';
|
|
9
|
+
|
|
10
|
+
const meta = BinaryUtil.getBlobMeta;
|
|
11
|
+
|
|
12
|
+
@Suite()
|
|
13
|
+
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
14
|
+
|
|
15
|
+
fixture = new TestFixtures(['@travetto/model']);
|
|
16
|
+
|
|
17
|
+
@Test()
|
|
18
|
+
async writeBasic(): Promise<void> {
|
|
19
|
+
const service = await this.service;
|
|
20
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
21
|
+
|
|
22
|
+
const id = Util.uuid();
|
|
23
|
+
|
|
24
|
+
await service.upsertBlob(id, buffer);
|
|
25
|
+
const m = await service.describeBlob(id);
|
|
26
|
+
const retrieved = await service.describeBlob(id);
|
|
27
|
+
assert.deepStrictEqual(m, retrieved);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test()
|
|
31
|
+
async upsert(): Promise<void> {
|
|
32
|
+
const service = await this.service;
|
|
33
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
34
|
+
|
|
35
|
+
const id = Util.uuid();
|
|
36
|
+
|
|
37
|
+
await service.upsertBlob(id, buffer, { hash: '10' });
|
|
38
|
+
assert((await service.describeBlob(id)).hash === '10');
|
|
39
|
+
|
|
40
|
+
await service.upsertBlob(id, buffer, { hash: '20' });
|
|
41
|
+
assert((await service.describeBlob(id)).hash === '20');
|
|
42
|
+
|
|
43
|
+
await service.upsertBlob(id, buffer, { hash: '30' }, false);
|
|
44
|
+
assert((await service.describeBlob(id)).hash === '20');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test()
|
|
48
|
+
async writeStream(): Promise<void> {
|
|
49
|
+
const service = await this.service;
|
|
50
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
51
|
+
|
|
52
|
+
const id = Util.uuid();
|
|
53
|
+
await service.upsertBlob(id, buffer);
|
|
54
|
+
const { hash } = await service.describeBlob(id);
|
|
55
|
+
|
|
56
|
+
const retrieved = await service.getBlob(id);
|
|
57
|
+
const { hash: received } = meta(retrieved)!;
|
|
58
|
+
assert(hash === received);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test()
|
|
62
|
+
async writeAndDelete(): Promise<void> {
|
|
63
|
+
const service = await this.service;
|
|
64
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
65
|
+
|
|
66
|
+
const id = Util.uuid();
|
|
67
|
+
await service.upsertBlob(id, buffer);
|
|
68
|
+
|
|
69
|
+
await service.deleteBlob(id);
|
|
70
|
+
|
|
71
|
+
await assert.rejects(async () => {
|
|
72
|
+
await service.getBlob(id);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test()
|
|
77
|
+
async partialStream(): Promise<void> {
|
|
78
|
+
const service = await this.service;
|
|
79
|
+
const buffer = await this.fixture.read('/text.txt', true);
|
|
80
|
+
|
|
81
|
+
const id = Util.uuid();
|
|
82
|
+
await service.upsertBlob(id, buffer);
|
|
83
|
+
|
|
84
|
+
const retrieved = await service.getBlob(id);
|
|
85
|
+
const content = await retrieved.text();
|
|
86
|
+
assert(content.startsWith('abc'));
|
|
87
|
+
assert(content.endsWith('xyz'));
|
|
88
|
+
|
|
89
|
+
const partial = await service.getBlob(id, { start: 10, end: 20 });
|
|
90
|
+
assert(partial.size === 11);
|
|
91
|
+
const partialMeta = meta(partial)!;
|
|
92
|
+
const subContent = await partial.text();
|
|
93
|
+
const range = await ModelBlobUtil.enforceRange({ start: 10, end: 20 }, partialMeta.size!);
|
|
94
|
+
assert(subContent.length === (range.end - range.start) + 1);
|
|
95
|
+
|
|
96
|
+
const og = await this.fixture.read('/text.txt');
|
|
97
|
+
|
|
98
|
+
assert(subContent === og.substring(10, 21));
|
|
99
|
+
|
|
100
|
+
const partialUnbounded = await service.getBlob(id, { start: 10 });
|
|
101
|
+
const partialUnboundedMeta = meta(partial)!;
|
|
102
|
+
const subContent2 = await partialUnbounded.text();
|
|
103
|
+
const range2 = await ModelBlobUtil.enforceRange({ start: 10 }, partialUnboundedMeta.size!);
|
|
104
|
+
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
105
|
+
assert(subContent2.startsWith('klm'));
|
|
106
|
+
assert(subContent2.endsWith('xyz'));
|
|
107
|
+
|
|
108
|
+
const partialSingle = await service.getBlob(id, { start: 10, end: 10 });
|
|
109
|
+
const subContent3 = await partialSingle.text();
|
|
110
|
+
assert(subContent3.length === 1);
|
|
111
|
+
assert(subContent3 === 'k');
|
|
112
|
+
|
|
113
|
+
const partialOverBounded = await service.getBlob(id, { start: 20, end: 40 });
|
|
114
|
+
const subContent4 = await partialOverBounded.text();
|
|
115
|
+
assert(subContent4.length === 6);
|
|
116
|
+
assert(subContent4.endsWith('xyz'));
|
|
117
|
+
|
|
118
|
+
await assert.rejects(() => service.getBlob(id, { start: -10, end: 10 }));
|
|
119
|
+
await assert.rejects(() => service.getBlob(id, { start: 30, end: 37 }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@Test()
|
|
124
|
+
async writeAndGet() {
|
|
125
|
+
const service = await this.service;
|
|
126
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
127
|
+
await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
|
|
128
|
+
const saved = await service.getBlob('orange');
|
|
129
|
+
const savedMeta = meta(saved)!;
|
|
130
|
+
|
|
131
|
+
assert('text/yaml' === savedMeta.contentType);
|
|
132
|
+
assert(buffer.length === savedMeta.size);
|
|
133
|
+
assert('asset.yml' === savedMeta.filename);
|
|
134
|
+
assert(undefined === savedMeta.hash);
|
|
135
|
+
}
|
|
136
|
+
}
|
package/support/test/crud.ts
CHANGED
|
@@ -49,7 +49,7 @@ class User2 {
|
|
|
49
49
|
name: string;
|
|
50
50
|
|
|
51
51
|
prePersist() {
|
|
52
|
-
this.name = `${this.name}-
|
|
52
|
+
this.name = `${this.name}-suffix`;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -193,7 +193,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
193
193
|
}));
|
|
194
194
|
|
|
195
195
|
assert(o.address === undefined);
|
|
196
|
-
assert(o.name === 'bob-
|
|
196
|
+
assert(o.name === 'bob-suffix');
|
|
197
197
|
|
|
198
198
|
await service.updatePartial(User2, User2.from({
|
|
199
199
|
id: o.id,
|
|
@@ -217,7 +217,7 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
217
217
|
assert(res.createdDate instanceof Date);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
@Test('verify
|
|
220
|
+
@Test('verify pre-persist on create/update')
|
|
221
221
|
async testPrePersist() {
|
|
222
222
|
const service = await this.service;
|
|
223
223
|
const res = await service.create(Dated, Dated.from({}));
|
|
@@ -12,6 +12,7 @@ import { isIndexedSupported } from '../../src/internal/service/common';
|
|
|
12
12
|
import { ExistsError } from '../../src/error/exists';
|
|
13
13
|
|
|
14
14
|
import { BaseModelSuite } from './base';
|
|
15
|
+
import { castTo } from '@travetto/runtime';
|
|
15
16
|
|
|
16
17
|
@Model({ baseType: true })
|
|
17
18
|
export class Worker {
|
|
@@ -70,7 +71,7 @@ export class IndexedEngineer extends IndexedWorker {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
73
|
-
const out
|
|
74
|
+
const out: T[] = [];
|
|
74
75
|
for await (const el of iterable) {
|
|
75
76
|
out.push(el);
|
|
76
77
|
}
|
|
@@ -116,12 +117,12 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
116
117
|
|
|
117
118
|
const fire3 = all.find(x => x instanceof Firefighter);
|
|
118
119
|
assert(fire3 instanceof Firefighter);
|
|
119
|
-
assert(
|
|
120
|
+
assert(fire3.firehouse === 20);
|
|
120
121
|
assert(fire3.name === 'rob');
|
|
121
122
|
|
|
122
123
|
const eng3 = all.find(x => x instanceof Engineer);
|
|
123
124
|
assert(eng3 instanceof Engineer);
|
|
124
|
-
assert(
|
|
125
|
+
assert(eng3.major === 'oranges');
|
|
125
126
|
assert(eng3.name === 'cob');
|
|
126
127
|
|
|
127
128
|
const engineers = await collect(service.list(Engineer));
|
|
@@ -161,7 +162,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
161
162
|
);
|
|
162
163
|
|
|
163
164
|
await assert.rejects(
|
|
164
|
-
() => service.update(Engineer, Doctor.from({ ...doc })
|
|
165
|
+
() => service.update(Engineer, castTo(Doctor.from({ ...doc }))),
|
|
165
166
|
(e: Error) => (e instanceof NotFoundError || e instanceof SubTypeNotSupportedError || e instanceof TypeMismatchError) ? undefined : e);
|
|
166
167
|
|
|
167
168
|
await timers.setTimeout(15);
|
|
@@ -205,7 +206,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
205
206
|
|
|
206
207
|
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
|
|
207
208
|
async polymorphicIndexGet() {
|
|
208
|
-
const service = (await this.service)
|
|
209
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
209
210
|
const now = 30;
|
|
210
211
|
const [doc, fire, eng] = [
|
|
211
212
|
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
@@ -235,7 +236,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
235
236
|
|
|
236
237
|
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(isIndexedSupported) })
|
|
237
238
|
async polymorphicIndexDelete() {
|
|
238
|
-
const service = (await this.service)
|
|
239
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
239
240
|
const now = 30;
|
|
240
241
|
const [doc, fire, eng] = [
|
|
241
242
|
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
package/support/test/suite.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { DependencyRegistry } from '@travetto/di';
|
|
|
3
3
|
import { RootRegistry } from '@travetto/registry';
|
|
4
4
|
import { SuiteRegistry, TestFixtures } from '@travetto/test';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { isBlobSupported, isStorageSupported } from '../../src/internal/service/common';
|
|
7
|
+
import { MODEL_BLOB } from '../../src/internal/service/blob';
|
|
8
8
|
import { ModelRegistry } from '../../src/registry/model';
|
|
9
9
|
|
|
10
10
|
const Loaded = Symbol();
|
|
@@ -61,11 +61,11 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
if (
|
|
64
|
+
if (isBlobSupported(service)) {
|
|
65
65
|
if (service.truncateModel) {
|
|
66
|
-
await service.truncateModel(
|
|
66
|
+
await service.truncateModel(MODEL_BLOB);
|
|
67
67
|
} else if (service.deleteModel) {
|
|
68
|
-
await service.deleteModel(
|
|
68
|
+
await service.deleteModel(MODEL_BLOB);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
} else {
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { AppError, Class } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import { ModelType } from '../../types/model';
|
|
4
|
-
import { StreamRange } from '../../service/stream';
|
|
5
|
-
|
|
6
|
-
class Cls { id: string; }
|
|
7
|
-
export const StreamModel: Class<ModelType> = Cls;
|
|
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
|
-
}
|