@travetto/model 5.0.0-rc.1 → 5.0.0-rc.11
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 +85 -147
- package/__index__.ts +2 -4
- package/package.json +7 -7
- package/src/error/exists.ts +1 -1
- package/src/error/invalid-index.ts +1 -1
- package/src/error/invalid-sub-type.ts +1 -1
- package/src/error/not-found.ts +1 -1
- package/src/internal/service/blob.ts +5 -0
- package/src/internal/service/bulk.ts +1 -1
- 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 -26
- package/src/internal/service/storage.ts +3 -7
- package/src/registry/decorator.ts +7 -13
- package/src/registry/model.ts +13 -8
- package/src/registry/types.ts +1 -2
- package/src/service/basic.ts +1 -1
- package/src/service/blob.ts +41 -0
- package/src/service/bulk.ts +1 -1
- package/src/service/crud.ts +1 -1
- package/src/service/expiry.ts +1 -1
- package/src/service/indexed.ts +2 -2
- package/src/service/storage.ts +1 -1
- package/src/util/blob.ts +60 -0
- package/support/base-command.ts +1 -1
- package/support/bin/candidate.ts +2 -3
- package/support/bin/export.ts +1 -1
- package/support/bin/install.ts +1 -1
- package/support/cli.model_export.ts +1 -1
- package/support/cli.model_install.ts +1 -1
- package/support/doc.support.tsx +5 -11
- 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 +117 -0
- package/support/test/crud.ts +5 -4
- package/support/test/expiry.ts +1 -1
- package/support/test/indexed.ts +1 -1
- package/support/test/polymorphism.ts +7 -6
- package/support/test/suite.ts +6 -6
- 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,5 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/
|
|
2
|
-
import { DeepPartial } from '@travetto/schema';
|
|
1
|
+
import { castTo, Class, DeepPartial, TypedObject } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
import { IndexNotSupported } from '../../error/invalid-index';
|
|
5
4
|
import { NotFoundError } from '../../error/not-found';
|
|
@@ -45,38 +44,30 @@ export class ModelIndexedUtil {
|
|
|
45
44
|
const parts = [];
|
|
46
45
|
|
|
47
46
|
while (o !== undefined && o !== null) {
|
|
48
|
-
const k =
|
|
49
|
-
|
|
50
|
-
o = (o[k] as Record<string, unknown>);
|
|
47
|
+
const k = TypedObject.keys(f)[0];
|
|
48
|
+
o = castTo(o[k]);
|
|
51
49
|
parts.push(k);
|
|
52
|
-
|
|
53
|
-
const fk = k as (keyof typeof f);
|
|
54
|
-
if (typeof f[fk] === 'boolean' || typeof f[fk] === 'number') {
|
|
50
|
+
if (typeof f[k] === 'boolean' || typeof f[k] === 'number') {
|
|
55
51
|
if (cfg.type === 'sorted') {
|
|
56
|
-
|
|
57
|
-
sortDir = f[fk] === true ? 1 : f[fk] as number;
|
|
52
|
+
sortDir = f[k] === true ? 1 : f[k] === false ? 0 : f[k];
|
|
58
53
|
}
|
|
59
54
|
break; // At the bottom
|
|
60
55
|
} else {
|
|
61
|
-
|
|
62
|
-
f = f[fk] as Record<string, unknown>;
|
|
56
|
+
f = castTo(f[k]);
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
if (field === sortField) {
|
|
66
|
-
|
|
67
|
-
sorted = { path: parts, dir: sortDir, value: o as unknown as number | Date };
|
|
60
|
+
sorted = { path: parts, dir: sortDir, value: castTo(o) };
|
|
68
61
|
}
|
|
69
62
|
if (o === undefined || o === null) {
|
|
70
63
|
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
71
64
|
if (empty === undefined || empty === Error) {
|
|
72
65
|
throw new IndexNotSupported(cls, cfg, `Missing field value for ${parts.join('.')}`);
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
o = empty as Record<string, unknown>;
|
|
67
|
+
o = castTo(empty!);
|
|
76
68
|
} else {
|
|
77
69
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
78
|
-
|
|
79
|
-
fields.push({ path: parts, value: o as unknown as string | boolean | Date | number });
|
|
70
|
+
fields.push({ path: parts, value: castTo(o) });
|
|
80
71
|
}
|
|
81
72
|
}
|
|
82
73
|
}
|
|
@@ -84,7 +75,6 @@ export class ModelIndexedUtil {
|
|
|
84
75
|
return { fields, sorted };
|
|
85
76
|
}
|
|
86
77
|
|
|
87
|
-
|
|
88
78
|
/**
|
|
89
79
|
* Project item via index
|
|
90
80
|
* @param cls Type to get index for
|
|
@@ -93,12 +83,11 @@ export class ModelIndexedUtil {
|
|
|
93
83
|
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, cfg?: ComputeConfig): Record<string, unknown> {
|
|
94
84
|
const res: Record<string, unknown> = {};
|
|
95
85
|
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, cfg).fields) {
|
|
96
|
-
let sub = res;
|
|
86
|
+
let sub: Record<string, unknown> = res;
|
|
97
87
|
const all = path.slice(0);
|
|
98
88
|
const last = all.pop()!;
|
|
99
89
|
for (const k of all) {
|
|
100
|
-
|
|
101
|
-
sub = (sub[k] ??= {}) as typeof res;
|
|
90
|
+
sub = castTo(sub[k] ??= {});
|
|
102
91
|
}
|
|
103
92
|
sub[last] = value;
|
|
104
93
|
}
|
|
@@ -135,11 +124,9 @@ export class ModelIndexedUtil {
|
|
|
135
124
|
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
136
125
|
): Promise<T> {
|
|
137
126
|
try {
|
|
138
|
-
|
|
139
|
-
const { id } = await service.getByIndex(cls, idx, body as DeepPartial<T>);
|
|
127
|
+
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
140
128
|
body.id = id;
|
|
141
|
-
|
|
142
|
-
return await service.update(cls, body as T);
|
|
129
|
+
return await service.update(cls, castTo(body));
|
|
143
130
|
} catch (err) {
|
|
144
131
|
if (err instanceof NotFoundError) {
|
|
145
132
|
return await service.create(cls, body);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class,
|
|
1
|
+
import { Class, Runtime } from '@travetto/runtime';
|
|
2
2
|
import { SchemaChangeListener } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelRegistry } from '../../registry/model';
|
|
@@ -12,15 +12,11 @@ export class ModelStorageUtil {
|
|
|
12
12
|
/**
|
|
13
13
|
* Register change listener on startup
|
|
14
14
|
*/
|
|
15
|
-
static async registerModelChangeListener(storage: ModelStorageSupport
|
|
16
|
-
if (!
|
|
15
|
+
static async registerModelChangeListener(storage: ModelStorageSupport): Promise<void> {
|
|
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
|
-
|
|
24
20
|
const checkType = (cls: Class, enforceBase = true): boolean => {
|
|
25
21
|
if (enforceBase && ModelRegistry.getBaseModel(cls) !== cls) {
|
|
26
22
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/
|
|
1
|
+
import { asConstructable, castTo, Class } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistry } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model';
|
|
@@ -24,8 +24,7 @@ export function Model(conf: Partial<ModelOptions<ModelType>> | string = {}) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Defines an index on a model
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
export function Index<T>(...indices: IndexConfig<any>[]) {
|
|
27
|
+
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
29
28
|
return function (target: Class<T>): void {
|
|
30
29
|
ModelRegistry.getOrCreatePending(target).indices!.push(...indices);
|
|
31
30
|
};
|
|
@@ -37,8 +36,7 @@ export function Index<T>(...indices: IndexConfig<any>[]) {
|
|
|
37
36
|
*/
|
|
38
37
|
export function ExpiresAt() {
|
|
39
38
|
return <K extends string, T extends Partial<Record<K, Date>>>(tgt: T, prop: K): void => {
|
|
40
|
-
|
|
41
|
-
ModelRegistry.register(tgt.constructor as Class<T>, { expiresAt: prop });
|
|
39
|
+
ModelRegistry.register(asConstructable(tgt).constructor, { expiresAt: prop });
|
|
42
40
|
};
|
|
43
41
|
}
|
|
44
42
|
|
|
@@ -50,8 +48,7 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
50
48
|
ModelRegistry.registerDataHandlers(tgt, {
|
|
51
49
|
prePersist: [{
|
|
52
50
|
scope,
|
|
53
|
-
|
|
54
|
-
handler: handler as DataHandler
|
|
51
|
+
handler: castTo(handler)
|
|
55
52
|
}]
|
|
56
53
|
});
|
|
57
54
|
};
|
|
@@ -62,13 +59,11 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
62
59
|
*/
|
|
63
60
|
export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
64
61
|
return function <K extends string, C extends Partial<Record<K, T>>>(tgt: C, prop: K): void {
|
|
65
|
-
|
|
66
|
-
ModelRegistry.registerDataHandlers(tgt.constructor as Class<C>, {
|
|
62
|
+
ModelRegistry.registerDataHandlers(asConstructable(tgt).constructor, {
|
|
67
63
|
prePersist: [{
|
|
68
64
|
scope,
|
|
69
65
|
handler: (inst): void => {
|
|
70
|
-
|
|
71
|
-
const cInst = (inst as unknown as Record<K, T>);
|
|
66
|
+
const cInst: Record<K, T> = castTo(inst);
|
|
72
67
|
cInst[prop] = handler(cInst[prop]);
|
|
73
68
|
}
|
|
74
69
|
}]
|
|
@@ -82,7 +77,6 @@ export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PreP
|
|
|
82
77
|
*/
|
|
83
78
|
export function PostLoad<T>(handler: DataHandler<T>) {
|
|
84
79
|
return function (tgt: Class<T>): void {
|
|
85
|
-
|
|
86
|
-
ModelRegistry.registerDataHandlers(tgt, { postLoad: [handler as DataHandler] });
|
|
80
|
+
ModelRegistry.registerDataHandlers(tgt, { postLoad: [castTo(handler)] });
|
|
87
81
|
};
|
|
88
82
|
}
|
package/src/registry/model.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { RuntimeIndex } from '@travetto/manifest';
|
|
2
1
|
import { SchemaRegistry } from '@travetto/schema';
|
|
3
2
|
import { MetadataRegistry } from '@travetto/registry';
|
|
4
3
|
import { DependencyRegistry } from '@travetto/di';
|
|
5
|
-
import { AppError, Class } from '@travetto/
|
|
4
|
+
import { AppError, castTo, Class, describeFunction, asFull } from '@travetto/runtime';
|
|
6
5
|
import { AllViewⲐ } from '@travetto/schema/src/internal/types';
|
|
7
6
|
|
|
8
7
|
import { IndexConfig, IndexType, ModelOptions } from './types';
|
|
@@ -52,7 +51,14 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
createPending(cls: Class): Partial<ModelOptions<ModelType>> {
|
|
55
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
class: cls,
|
|
56
|
+
indices: [],
|
|
57
|
+
autoCreate: true,
|
|
58
|
+
baseType: describeFunction(cls).abstract,
|
|
59
|
+
postLoad: [],
|
|
60
|
+
prePersist: []
|
|
61
|
+
};
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
registerDataHandlers(cls: Class, pConfig?: Partial<ModelOptions<ModelType>>): void {
|
|
@@ -65,8 +71,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
onInstallFinalize(cls: Class): ModelOptions<ModelType> {
|
|
68
|
-
|
|
69
|
-
const config = this.pending.get(cls.Ⲑid)! as ModelOptions<ModelType>;
|
|
74
|
+
const config = asFull(this.pending.get(cls.Ⲑid)!);
|
|
70
75
|
|
|
71
76
|
const schema = SchemaRegistry.get(cls);
|
|
72
77
|
const view = schema.views[AllViewⲐ].schema;
|
|
@@ -97,7 +102,7 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
97
102
|
/**
|
|
98
103
|
* Find base class for a given model
|
|
99
104
|
*/
|
|
100
|
-
getBaseModel(cls: Class): Class<
|
|
105
|
+
getBaseModel<T extends ModelType>(cls: Class<T>): Class<T> {
|
|
101
106
|
if (!this.baseModels.has(cls)) {
|
|
102
107
|
let conf = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
103
108
|
let parent = cls;
|
|
@@ -202,12 +207,12 @@ class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
|
202
207
|
* Get expiry field
|
|
203
208
|
* @param cls
|
|
204
209
|
*/
|
|
205
|
-
getExpiry(cls: Class):
|
|
210
|
+
getExpiry<T extends ModelType>(cls: Class<T>): keyof T {
|
|
206
211
|
const expiry = this.get(cls).expiresAt;
|
|
207
212
|
if (!expiry) {
|
|
208
213
|
throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`, 'general');
|
|
209
214
|
}
|
|
210
|
-
return expiry;
|
|
215
|
+
return castTo(expiry);
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
218
|
|
package/src/registry/types.ts
CHANGED
package/src/service/basic.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BinaryInput, BlobMeta, ByteRange } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Support for Blobs CRD. Blob update is not supported.
|
|
5
|
+
*
|
|
6
|
+
* @concrete ../internal/service/common#ModelBlobSupportTarget
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelBlobSupport {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Insert blob to storage
|
|
12
|
+
* @param location The location of the blob
|
|
13
|
+
* @param input The actual blob to write
|
|
14
|
+
*/
|
|
15
|
+
insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting?: boolean): Promise<void>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Upsert blob to storage
|
|
19
|
+
* @param location The location of the blob
|
|
20
|
+
* @param input The actual blob to write
|
|
21
|
+
*/
|
|
22
|
+
upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get blob from storage
|
|
26
|
+
* @param location The location of the blob
|
|
27
|
+
*/
|
|
28
|
+
getBlob(location: string, range?: ByteRange): Promise<Blob>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get metadata for blob
|
|
32
|
+
* @param location The location of the blob
|
|
33
|
+
*/
|
|
34
|
+
describeBlob(location: string): Promise<BlobMeta>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delete blob by location
|
|
38
|
+
* @param location The location of the blob
|
|
39
|
+
*/
|
|
40
|
+
deleteBlob(location: string): Promise<void>;
|
|
41
|
+
}
|
package/src/service/bulk.ts
CHANGED
package/src/service/crud.ts
CHANGED
package/src/service/expiry.ts
CHANGED
package/src/service/indexed.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/
|
|
2
|
-
import { DeepPartial } from '@travetto/schema';
|
|
1
|
+
import { Class, DeepPartial } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
import { ModelType, OptionalId } from '../types/model';
|
|
5
4
|
import { ModelBasicSupport } from './basic';
|
|
@@ -7,6 +6,7 @@ import { ModelBasicSupport } from './basic';
|
|
|
7
6
|
/**
|
|
8
7
|
* Support for simple indexed activity
|
|
9
8
|
*
|
|
9
|
+
*
|
|
10
10
|
* @concrete ../internal/service/common#ModelIndexedSupportTarget
|
|
11
11
|
*/
|
|
12
12
|
export interface ModelIndexedSupport extends ModelBasicSupport {
|
package/src/service/storage.ts
CHANGED
package/src/util/blob.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { AppError, BinaryInput, BlobMeta, ByteRange, Util } from '@travetto/runtime';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Utilities for processing assets
|
|
8
|
+
*/
|
|
9
|
+
export class ModelBlobUtil {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a hashed location/path for a blob
|
|
13
|
+
*/
|
|
14
|
+
static getHashedLocation(meta: BlobMeta, prefix = ''): string {
|
|
15
|
+
const hash = meta.hash ?? Util.uuid();
|
|
16
|
+
|
|
17
|
+
let parts = hash.match(/(.{1,4})/g)!.slice();
|
|
18
|
+
if (parts.length > 4) {
|
|
19
|
+
parts = [...parts.slice(0, 4), parts.slice(4).join('')];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ext = path.extname(meta.filename ?? '') || '.bin';
|
|
23
|
+
return `${parts.join('/')}${ext}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert input to a blob, containing all data in memory
|
|
28
|
+
*/
|
|
29
|
+
static async getInput(src: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
|
|
30
|
+
let input: Readable;
|
|
31
|
+
if (src instanceof Blob) {
|
|
32
|
+
metadata = { ...src.meta, ...metadata };
|
|
33
|
+
metadata.size ??= src.size;
|
|
34
|
+
input = Readable.fromWeb(src.stream());
|
|
35
|
+
} else if (typeof src === 'object' && 'pipeThrough' in src) {
|
|
36
|
+
input = Readable.fromWeb(src);
|
|
37
|
+
} else if (typeof src === 'object' && 'pipe' in src) {
|
|
38
|
+
input = src;
|
|
39
|
+
} else {
|
|
40
|
+
metadata.size = src.length;
|
|
41
|
+
input = Readable.from(src);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [input, metadata ?? {}];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enforce byte range for stream stream/file of a certain size
|
|
49
|
+
*/
|
|
50
|
+
static enforceRange({ start, end }: ByteRange, size: number): Required<ByteRange> {
|
|
51
|
+
// End is inclusive
|
|
52
|
+
end = Math.min(end ?? (size - 1), size - 1);
|
|
53
|
+
|
|
54
|
+
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
|
|
55
|
+
throw new AppError('Invalid position, out of range', 'data');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { start, end };
|
|
59
|
+
}
|
|
60
|
+
}
|
package/support/base-command.ts
CHANGED
package/support/bin/candidate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class } from '@travetto/
|
|
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/bin/export.ts
CHANGED
package/support/bin/install.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { ModelCandidateUtil } from './bin/candidate';
|
|
|
7
7
|
/**
|
|
8
8
|
* Exports model schemas
|
|
9
9
|
*/
|
|
10
|
-
@CliCommand({
|
|
10
|
+
@CliCommand({ with: { env: true, module: true } })
|
|
11
11
|
export class ModelExportCommand extends BaseModelCommand {
|
|
12
12
|
|
|
13
13
|
getOp(): 'exportModel' { return 'exportModel'; }
|
|
@@ -7,7 +7,7 @@ import { ModelCandidateUtil } from './bin/candidate';
|
|
|
7
7
|
/**
|
|
8
8
|
* Installing models
|
|
9
9
|
*/
|
|
10
|
-
@CliCommand({
|
|
10
|
+
@CliCommand({ with: { env: true, module: true } })
|
|
11
11
|
export class ModelInstallCommand extends BaseModelCommand {
|
|
12
12
|
|
|
13
13
|
getOp(): 'createModel' { return 'createModel'; }
|
package/support/doc.support.tsx
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @travetto/doc */
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { RuntimeIndex } from '@travetto/manifest';
|
|
5
|
-
import { d, c, DocJSXElementByFn, DocJSXElement } from '@travetto/doc';
|
|
2
|
+
import { d, c, DocJSXElementByFn, DocJSXElement, DocFileUtil } from '@travetto/doc';
|
|
6
3
|
import { Config } from '@travetto/config';
|
|
7
4
|
|
|
8
5
|
export const Links = {
|
|
@@ -11,17 +8,14 @@ export const Links = {
|
|
|
11
8
|
Expiry: d.codeLink('Expiry', '@travetto/model/src/service/expiry.ts', /export interface/),
|
|
12
9
|
Indexed: d.codeLink('Indexed', '@travetto/model/src/service/indexed.ts', /export interface/),
|
|
13
10
|
Bulk: d.codeLink('Bulk', '@travetto/model/src/service/bulk.ts', /export interface/),
|
|
14
|
-
|
|
11
|
+
Blob: d.codeLink('Blob', '@travetto/model/src/service/blob.ts', /export interface/),
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
export const ModelTypes = (
|
|
18
|
-
|
|
19
|
-
file = RuntimeIndex.getFunctionMetadata(file)!.source;
|
|
20
|
-
}
|
|
21
|
-
const contents = readFileSync(file, 'utf8');
|
|
14
|
+
export const ModelTypes = (fn: | Function): DocJSXElement[] => {
|
|
15
|
+
const { content } = DocFileUtil.readSource(fn);
|
|
22
16
|
const found: DocJSXElementByFn<'CodeLink'>[] = [];
|
|
23
17
|
const seen = new Set();
|
|
24
|
-
for (const [, key] of
|
|
18
|
+
for (const [, key] of content.matchAll(/Model(Crud|Expiry|Indexed|Bulk|Blob)Support/g)) {
|
|
25
19
|
if (!seen.has(key)) {
|
|
26
20
|
seen.add(key);
|
|
27
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/
|
|
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,117 @@
|
|
|
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 { Util } from '@travetto/runtime';
|
|
6
|
+
|
|
7
|
+
import { ModelBlobSupport } from '../../src/service/blob';
|
|
8
|
+
import { ModelBlobUtil } from '../../src/util/blob';
|
|
9
|
+
|
|
10
|
+
@Suite()
|
|
11
|
+
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
12
|
+
|
|
13
|
+
fixture = new TestFixtures(['@travetto/model']);
|
|
14
|
+
|
|
15
|
+
@Test()
|
|
16
|
+
async writeBasic(): Promise<void> {
|
|
17
|
+
const service = await this.service;
|
|
18
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
19
|
+
|
|
20
|
+
const id = Util.uuid();
|
|
21
|
+
|
|
22
|
+
await service.upsertBlob(id, buffer);
|
|
23
|
+
const meta = await service.describeBlob(id);
|
|
24
|
+
const retrieved = await service.describeBlob(id);
|
|
25
|
+
assert.deepStrictEqual(meta, retrieved);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test()
|
|
29
|
+
async writeStream(): Promise<void> {
|
|
30
|
+
const service = await this.service;
|
|
31
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
32
|
+
|
|
33
|
+
const id = Util.uuid();
|
|
34
|
+
await service.upsertBlob(id, buffer);
|
|
35
|
+
const meta = await service.describeBlob(id);
|
|
36
|
+
|
|
37
|
+
const retrieved = await service.getBlob(id);
|
|
38
|
+
const retrievedMeta = retrieved.meta!;
|
|
39
|
+
assert(meta.hash === retrievedMeta.hash);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test()
|
|
43
|
+
async writeAndDelete(): Promise<void> {
|
|
44
|
+
const service = await this.service;
|
|
45
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
46
|
+
|
|
47
|
+
const id = Util.uuid();
|
|
48
|
+
await service.upsertBlob(id, buffer);
|
|
49
|
+
|
|
50
|
+
await service.deleteBlob(id);
|
|
51
|
+
|
|
52
|
+
await assert.rejects(async () => {
|
|
53
|
+
await service.getBlob(id);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test()
|
|
58
|
+
async partialStream(): Promise<void> {
|
|
59
|
+
const service = await this.service;
|
|
60
|
+
const buffer = await this.fixture.read('/text.txt', true);
|
|
61
|
+
|
|
62
|
+
const id = Util.uuid();
|
|
63
|
+
await service.upsertBlob(id, buffer);
|
|
64
|
+
|
|
65
|
+
const retrieved = await service.getBlob(id);
|
|
66
|
+
const content = await retrieved.text();
|
|
67
|
+
assert(content.startsWith('abc'));
|
|
68
|
+
assert(content.endsWith('xyz'));
|
|
69
|
+
|
|
70
|
+
const partial = await service.getBlob(id, { start: 10, end: 20 });
|
|
71
|
+
assert(partial.size === 11);
|
|
72
|
+
const partialMeta = partial.meta!;
|
|
73
|
+
const subContent = await partial.text();
|
|
74
|
+
const range = await ModelBlobUtil.enforceRange({ start: 10, end: 20 }, partialMeta.size!);
|
|
75
|
+
assert(subContent.length === (range.end - range.start) + 1);
|
|
76
|
+
|
|
77
|
+
const og = await this.fixture.read('/text.txt');
|
|
78
|
+
|
|
79
|
+
assert(subContent === og.substring(10, 21));
|
|
80
|
+
|
|
81
|
+
const partialUnbounded = await service.getBlob(id, { start: 10 });
|
|
82
|
+
const partialUnboundedMeta = partial.meta!;
|
|
83
|
+
const subContent2 = await partialUnbounded.text();
|
|
84
|
+
const range2 = await ModelBlobUtil.enforceRange({ start: 10 }, partialUnboundedMeta.size!);
|
|
85
|
+
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
86
|
+
assert(subContent2.startsWith('klm'));
|
|
87
|
+
assert(subContent2.endsWith('xyz'));
|
|
88
|
+
|
|
89
|
+
const partialSingle = await service.getBlob(id, { start: 10, end: 10 });
|
|
90
|
+
const subContent3 = await partialSingle.text();
|
|
91
|
+
assert(subContent3.length === 1);
|
|
92
|
+
assert(subContent3 === 'k');
|
|
93
|
+
|
|
94
|
+
const partialOverBounded = await service.getBlob(id, { start: 20, end: 40 });
|
|
95
|
+
const subContent4 = await partialOverBounded.text();
|
|
96
|
+
assert(subContent4.length === 6);
|
|
97
|
+
assert(subContent4.endsWith('xyz'));
|
|
98
|
+
|
|
99
|
+
await assert.rejects(() => service.getBlob(id, { start: -10, end: 10 }));
|
|
100
|
+
await assert.rejects(() => service.getBlob(id, { start: 30, end: 37 }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@Test()
|
|
105
|
+
async writeAndGet() {
|
|
106
|
+
const service = await this.service;
|
|
107
|
+
const buffer = await this.fixture.read('/asset.yml', true);
|
|
108
|
+
await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
|
|
109
|
+
const saved = await service.getBlob('orange');
|
|
110
|
+
const savedMeta = saved.meta!;
|
|
111
|
+
|
|
112
|
+
assert('text/yaml' === savedMeta.contentType);
|
|
113
|
+
assert(buffer.length === savedMeta.size);
|
|
114
|
+
assert('asset.yml' === savedMeta.filename);
|
|
115
|
+
assert(undefined === savedMeta.hash);
|
|
116
|
+
}
|
|
117
|
+
}
|