@travetto/model 7.1.4 → 8.0.0-alpha.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 +12 -11
- package/package.json +7 -7
- package/src/error/exists.ts +2 -2
- package/src/error/invalid-index.ts +2 -2
- package/src/error/invalid-sub-type.ts +2 -2
- package/src/error/not-found.ts +2 -2
- package/src/registry/decorator.ts +2 -2
- package/src/registry/registry-index.ts +3 -3
- package/src/registry/types.ts +3 -1
- package/src/types/blob.ts +11 -10
- package/src/types/bulk.ts +2 -2
- package/src/util/blob.ts +3 -34
- package/src/util/crud.ts +10 -10
- package/src/util/expiry.ts +1 -1
- package/src/util/indexed.ts +0 -1
- package/support/base-command.ts +20 -16
- package/support/cli.model_export.ts +3 -1
- package/support/cli.model_install.ts +3 -1
- package/support/doc.support.tsx +1 -1
- package/support/test/base.ts +2 -2
- package/support/test/blob.ts +37 -39
- package/support/test/crud.ts +42 -0
- package/support/test/expiry.ts +5 -5
- package/support/test/suite.ts +71 -66
package/README.md
CHANGED
|
@@ -182,10 +182,10 @@ export interface ModelBlobSupport {
|
|
|
182
182
|
* Upsert blob to storage
|
|
183
183
|
* @param location The location of the blob
|
|
184
184
|
* @param input The actual blob to write
|
|
185
|
-
* @param
|
|
185
|
+
* @param metadata Additional metadata to store with the blob
|
|
186
186
|
* @param overwrite Should we replace content if already found, defaults to true
|
|
187
187
|
*/
|
|
188
|
-
upsertBlob(location: string, input:
|
|
188
|
+
upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite?: boolean): Promise<void>;
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* Get blob from storage
|
|
@@ -197,7 +197,7 @@ export interface ModelBlobSupport {
|
|
|
197
197
|
* Get metadata for blob
|
|
198
198
|
* @param location The location of the blob
|
|
199
199
|
*/
|
|
200
|
-
|
|
200
|
+
getBlobMetadata(location: string): Promise<BinaryMetadata>;
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
203
|
* Delete blob by location
|
|
@@ -208,25 +208,26 @@ export interface ModelBlobSupport {
|
|
|
208
208
|
/**
|
|
209
209
|
* Update blob metadata
|
|
210
210
|
* @param location The location of the blob
|
|
211
|
+
* @param metadata The metadata to update
|
|
211
212
|
*/
|
|
212
|
-
|
|
213
|
+
updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
|
|
213
214
|
|
|
214
215
|
/**
|
|
215
216
|
* Produces an externally usable URL for sharing limited read access to a specific resource
|
|
216
217
|
*
|
|
217
218
|
* @param location The asset location to read from
|
|
218
|
-
* @param
|
|
219
|
+
* @param expiresIn Expiry
|
|
219
220
|
*/
|
|
220
|
-
getBlobReadUrl?(location: string,
|
|
221
|
+
getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
|
|
221
222
|
|
|
222
223
|
/**
|
|
223
224
|
* Produces an externally usable URL for sharing allowing direct write access
|
|
224
225
|
*
|
|
225
226
|
* @param location The asset location to write to
|
|
226
|
-
* @param
|
|
227
|
-
* @param
|
|
227
|
+
* @param metadata The metadata to associate with the final asset
|
|
228
|
+
* @param expiresIn Expiry
|
|
228
229
|
*/
|
|
229
|
-
getBlobWriteUrl?(location: string,
|
|
230
|
+
getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
|
|
230
231
|
}
|
|
231
232
|
```
|
|
232
233
|
|
|
@@ -278,7 +279,7 @@ To enforce that these contracts are honored, the module provides shared test sui
|
|
|
278
279
|
**Code: Memory Service Test Configuration**
|
|
279
280
|
```typescript
|
|
280
281
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
281
|
-
import {
|
|
282
|
+
import { RuntimeError, castTo, type Class, classConstruct } from '@travetto/runtime';
|
|
282
283
|
|
|
283
284
|
import { ModelBulkUtil } from '../../src/util/bulk.ts';
|
|
284
285
|
import { ModelCrudUtil } from '../../src/util/crud.ts';
|
|
@@ -306,7 +307,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
306
307
|
}
|
|
307
308
|
return i;
|
|
308
309
|
} else {
|
|
309
|
-
throw new
|
|
310
|
+
throw new RuntimeError(`Size is not supported for this service: ${this.serviceClass.name}`);
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Datastore abstraction for core operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
"directory": "module/model"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/config": "^
|
|
31
|
-
"@travetto/di": "^
|
|
32
|
-
"@travetto/registry": "^
|
|
33
|
-
"@travetto/schema": "^
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.1",
|
|
31
|
+
"@travetto/di": "^8.0.0-alpha.1",
|
|
32
|
+
"@travetto/registry": "^8.0.0-alpha.1",
|
|
33
|
+
"@travetto/schema": "^8.0.0-alpha.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^
|
|
37
|
-
"@travetto/test": "^
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.1",
|
|
37
|
+
"@travetto/test": "^8.0.0-alpha.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@travetto/cli": {
|
package/src/error/exists.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Class,
|
|
1
|
+
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents when a data item already exists
|
|
5
5
|
*/
|
|
6
|
-
export class ExistsError extends
|
|
6
|
+
export class ExistsError extends RuntimeError {
|
|
7
7
|
constructor(cls: Class | string, id: string) {
|
|
8
8
|
super(`${typeof cls === 'string' ? cls : cls.name} with id ${id} already exists`, {
|
|
9
9
|
category: 'data',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Class,
|
|
1
|
+
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { IndexConfig } from '../registry/types.ts';
|
|
4
4
|
import type { ModelType } from '../types/model.ts';
|
|
@@ -6,7 +6,7 @@ import type { ModelType } from '../types/model.ts';
|
|
|
6
6
|
/**
|
|
7
7
|
* Represents when an index is invalid
|
|
8
8
|
*/
|
|
9
|
-
export class IndexNotSupported<T extends ModelType> extends
|
|
9
|
+
export class IndexNotSupported<T extends ModelType> extends RuntimeError {
|
|
10
10
|
constructor(cls: Class<T>, idx: IndexConfig<T>, message: string = '') {
|
|
11
11
|
super(`${typeof cls === 'string' ? cls : cls.name} and index ${idx.name} of type ${idx.type} is not supported. ${message}`.trim(), { category: 'data' });
|
|
12
12
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Class,
|
|
1
|
+
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents when a model subtype class is unable to be used directly
|
|
5
5
|
*/
|
|
6
|
-
export class SubTypeNotSupportedError extends
|
|
6
|
+
export class SubTypeNotSupportedError extends RuntimeError {
|
|
7
7
|
constructor(cls: Class | string) {
|
|
8
8
|
super(`${typeof cls === 'string' ? cls : cls.name} cannot be used for this operation`, { category: 'data' });
|
|
9
9
|
}
|
package/src/error/not-found.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Class,
|
|
1
|
+
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents when a model of cls and id cannot be found
|
|
5
5
|
*/
|
|
6
|
-
export class NotFoundError extends
|
|
6
|
+
export class NotFoundError extends RuntimeError {
|
|
7
7
|
constructor(cls: Class | string, id: string, details: Record<string, unknown> = {}) {
|
|
8
8
|
super(`${typeof cls === 'string' ? cls : cls.name} with id ${id} not found`, { category: 'notfound', details });
|
|
9
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RuntimeError, castTo, type Class, getClass } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import type { ModelType } from '../types/model.ts';
|
|
@@ -30,7 +30,7 @@ export function Model(config: Partial<ModelConfig<ModelType>> | string = {}) {
|
|
|
30
30
|
*/
|
|
31
31
|
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
32
32
|
if (indices.some(config => config.fields.some(field => field === 'id'))) {
|
|
33
|
-
throw new
|
|
33
|
+
throw new RuntimeError('Cannot create an index with the id field');
|
|
34
34
|
}
|
|
35
35
|
return function (cls: Class<T>): void {
|
|
36
36
|
ModelRegistryIndex.getForRegister(cls).register({ indices });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { RuntimeError, castTo, type Class } from '@travetto/runtime';
|
|
3
3
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
5
|
import type { IndexConfig, IndexType, ModelConfig } from './types.ts';
|
|
@@ -77,7 +77,7 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
77
77
|
|
|
78
78
|
// Don't allow two models with same class name, or same store name
|
|
79
79
|
if (classes.size > 1) {
|
|
80
|
-
throw new
|
|
80
|
+
throw new RuntimeError('Duplicate models with same store name', {
|
|
81
81
|
details: { classes: [...classes].toSorted() }
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -122,7 +122,7 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
122
122
|
getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
|
|
123
123
|
const expiry = this.getConfig(cls).expiresAt;
|
|
124
124
|
if (!expiry) {
|
|
125
|
-
throw new
|
|
125
|
+
throw new RuntimeError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`);
|
|
126
126
|
}
|
|
127
127
|
return castTo(expiry);
|
|
128
128
|
}
|
package/src/registry/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { Class,
|
|
1
|
+
import type { Class, Primitive, ValidFields } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { ModelType } from '../types/model.ts';
|
|
4
4
|
|
|
5
|
+
type RetainPrimitiveFields<T> = Pick<T, ValidFields<T, Primitive | Date>>;
|
|
6
|
+
|
|
5
7
|
export type SortClauseRaw<T> = {
|
|
6
8
|
[P in keyof T]?:
|
|
7
9
|
T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
|
package/src/types/blob.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BinaryType, BinaryMetadata, ByteRange, TimeSpan } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Support for Blobs CRUD.
|
|
@@ -11,10 +11,10 @@ export interface ModelBlobSupport {
|
|
|
11
11
|
* Upsert blob to storage
|
|
12
12
|
* @param location The location of the blob
|
|
13
13
|
* @param input The actual blob to write
|
|
14
|
-
* @param
|
|
14
|
+
* @param metadata Additional metadata to store with the blob
|
|
15
15
|
* @param overwrite Should we replace content if already found, defaults to true
|
|
16
16
|
*/
|
|
17
|
-
upsertBlob(location: string, input:
|
|
17
|
+
upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite?: boolean): Promise<void>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Get blob from storage
|
|
@@ -26,7 +26,7 @@ export interface ModelBlobSupport {
|
|
|
26
26
|
* Get metadata for blob
|
|
27
27
|
* @param location The location of the blob
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
getBlobMetadata(location: string): Promise<BinaryMetadata>;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Delete blob by location
|
|
@@ -37,23 +37,24 @@ export interface ModelBlobSupport {
|
|
|
37
37
|
/**
|
|
38
38
|
* Update blob metadata
|
|
39
39
|
* @param location The location of the blob
|
|
40
|
+
* @param metadata The metadata to update
|
|
40
41
|
*/
|
|
41
|
-
|
|
42
|
+
updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Produces an externally usable URL for sharing limited read access to a specific resource
|
|
45
46
|
*
|
|
46
47
|
* @param location The asset location to read from
|
|
47
|
-
* @param
|
|
48
|
+
* @param expiresIn Expiry
|
|
48
49
|
*/
|
|
49
|
-
getBlobReadUrl?(location: string,
|
|
50
|
+
getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Produces an externally usable URL for sharing allowing direct write access
|
|
53
54
|
*
|
|
54
55
|
* @param location The asset location to write to
|
|
55
|
-
* @param
|
|
56
|
-
* @param
|
|
56
|
+
* @param metadata The metadata to associate with the final asset
|
|
57
|
+
* @param expiresIn Expiry
|
|
57
58
|
*/
|
|
58
|
-
getBlobWriteUrl?(location: string,
|
|
59
|
+
getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
|
|
59
60
|
}
|
package/src/types/bulk.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Class,
|
|
1
|
+
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
import type { ValidationError, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import type { ModelCrudSupport } from './crud.ts';
|
|
@@ -42,7 +42,7 @@ type BulkErrorItem = { message: string, type: string, errors?: ValidationError[]
|
|
|
42
42
|
/**
|
|
43
43
|
* Bulk processing error
|
|
44
44
|
*/
|
|
45
|
-
export class BulkProcessError extends
|
|
45
|
+
export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }> {
|
|
46
46
|
constructor(errors: { idx: number, error: ValidationResultError }[]) {
|
|
47
47
|
super('Bulk processing errors have occurred', {
|
|
48
48
|
category: 'data',
|
package/src/util/blob.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AppError, type BinaryInput, BinaryUtil, type BlobMeta, type ByteRange, hasFunction } from '@travetto/runtime';
|
|
1
|
+
import { hasFunction } from '@travetto/runtime';
|
|
3
2
|
import type { ModelBlobSupport } from '../types/blob.ts';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -13,37 +12,7 @@ export class ModelBlobUtil {
|
|
|
13
12
|
static isSupported = hasFunction<ModelBlobSupport>('getBlob');
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
15
|
+
* Type guard for determining if service supports blob write urls
|
|
17
16
|
*/
|
|
18
|
-
static
|
|
19
|
-
let result: Readable;
|
|
20
|
-
if (input instanceof Blob) {
|
|
21
|
-
metadata = { ...BinaryUtil.getBlobMeta(input), ...metadata };
|
|
22
|
-
metadata.size ??= input.size;
|
|
23
|
-
result = Readable.fromWeb(input.stream());
|
|
24
|
-
} else if (typeof input === 'object' && 'pipeThrough' in input) {
|
|
25
|
-
result = Readable.fromWeb(input);
|
|
26
|
-
} else if (typeof input === 'object' && 'pipe' in input) {
|
|
27
|
-
result = input;
|
|
28
|
-
} else {
|
|
29
|
-
metadata.size = input.length;
|
|
30
|
-
result = Readable.from(input);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return [result, metadata ?? {}];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Enforce byte range for stream stream/file of a certain size
|
|
38
|
-
*/
|
|
39
|
-
static enforceRange({ start, end }: ByteRange, size: number): Required<ByteRange> {
|
|
40
|
-
// End is inclusive
|
|
41
|
-
end = Math.min(end ?? (size - 1), size - 1);
|
|
42
|
-
|
|
43
|
-
if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
|
|
44
|
-
throw new AppError('Invalid position, out of range', { category: 'data' });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { start, end };
|
|
48
|
-
}
|
|
17
|
+
static isWriteUrlSupported = hasFunction<ModelBlobSupport>('getBlobWriteUrl');
|
|
49
18
|
}
|
package/src/util/crud.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { castTo, type Class, Util,
|
|
1
|
+
import { castTo, type Class, Util, RuntimeError, hasFunction, BinaryUtil, type BinaryArray, JSONUtil } from '@travetto/runtime';
|
|
2
2
|
import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
@@ -9,6 +9,8 @@ import { SubTypeNotSupportedError } from '../error/invalid-sub-type.ts';
|
|
|
9
9
|
import type { DataHandler, PrePersistScope } from '../registry/types.ts';
|
|
10
10
|
import type { ModelCrudSupport } from '../types/crud.ts';
|
|
11
11
|
|
|
12
|
+
type ModelLoadInput = string | BinaryArray | object;
|
|
13
|
+
|
|
12
14
|
export type ModelCrudProvider = {
|
|
13
15
|
idSource: ModelIdSource;
|
|
14
16
|
};
|
|
@@ -37,13 +39,11 @@ export class ModelCrudUtil {
|
|
|
37
39
|
* @param cls Class to load model for
|
|
38
40
|
* @param input Input as string or plain object
|
|
39
41
|
*/
|
|
40
|
-
static async load<T extends ModelType>(cls: Class<T>, input:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
resolvedInput = input;
|
|
46
|
-
}
|
|
42
|
+
static async load<T extends ModelType>(cls: Class<T>, input: ModelLoadInput, onTypeMismatch: 'notfound' | 'exists' = 'notfound'): Promise<T> {
|
|
43
|
+
const resolvedInput: object =
|
|
44
|
+
typeof input === 'string' ? JSONUtil.fromUTF8(input) :
|
|
45
|
+
BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) :
|
|
46
|
+
input;
|
|
47
47
|
|
|
48
48
|
const result = SchemaRegistryIndex.getBaseClass(cls).from(resolvedInput);
|
|
49
49
|
|
|
@@ -142,11 +142,11 @@ export class ModelCrudUtil {
|
|
|
142
142
|
*/
|
|
143
143
|
static async prePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view?: string): Promise<Partial<T>> {
|
|
144
144
|
if (!DataUtil.isPlainObject(item)) {
|
|
145
|
-
throw new
|
|
145
|
+
throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, { category: 'data' });
|
|
146
146
|
}
|
|
147
147
|
const keys = Object.keys(item);
|
|
148
148
|
if ((keys.length === 1 && item.id) || keys.length === 0) {
|
|
149
|
-
throw new
|
|
149
|
+
throw new RuntimeError('No fields to update');
|
|
150
150
|
} else {
|
|
151
151
|
item = { ...item };
|
|
152
152
|
delete item.id;
|
package/src/util/expiry.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class ModelExpiryUtil {
|
|
|
34
34
|
static registerCull(service: ModelExpirySupport & { readonly config?: { cullRate?: number | TimeSpan } }): void {
|
|
35
35
|
const cullable = ModelRegistryIndex.getClasses().filter(cls => !!ModelRegistryIndex.getConfig(cls).expiresAt);
|
|
36
36
|
if (service.deleteExpired && cullable.length) {
|
|
37
|
-
const cullInterval = TimeUtil.
|
|
37
|
+
const cullInterval = TimeUtil.duration(service.config?.cullRate ?? '10m', 'ms');
|
|
38
38
|
|
|
39
39
|
(async (): Promise<void> => {
|
|
40
40
|
await Util.nonBlockingTimeout(1000);
|
package/src/util/indexed.ts
CHANGED
|
@@ -71,7 +71,6 @@ export class ModelIndexedUtil {
|
|
|
71
71
|
if (empty === undefined || empty === Error) {
|
|
72
72
|
throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
|
|
73
73
|
}
|
|
74
|
-
itemRef = castTo(empty!);
|
|
75
74
|
} else {
|
|
76
75
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
77
76
|
fields.push({ path: parts, value: castTo(itemRef) });
|
package/support/base-command.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Env } from '@travetto/runtime';
|
|
2
|
-
import { type
|
|
2
|
+
import { type CliCommandShape, cliTpl, CliModuleFlag, CliProfilesFlag } from '@travetto/cli';
|
|
3
3
|
import { Registry } from '@travetto/registry';
|
|
4
|
-
import { Schema } from '@travetto/schema';
|
|
4
|
+
import { Schema, type ValidationError } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import type { ModelStorageSupport } from '../src/types/storage.ts';
|
|
7
7
|
|
|
@@ -13,9 +13,26 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
13
13
|
@Schema()
|
|
14
14
|
export abstract class BaseModelCommand implements CliCommandShape {
|
|
15
15
|
|
|
16
|
+
static async validate(operation: keyof ModelStorageSupport, provider: string, models: string[]): Promise<ValidationError | undefined> {
|
|
17
|
+
const candidates = await ModelCandidateUtil.export(operation);
|
|
18
|
+
if (provider && !candidates.providers.includes(provider)) {
|
|
19
|
+
return { message: `provider: ${provider} is not a valid provider`, source: 'arg', kind: 'invalid', path: 'provider' };
|
|
20
|
+
}
|
|
21
|
+
const badModel = models.find(model => model !== '*' && !candidates.models.includes(model));
|
|
22
|
+
if (badModel) {
|
|
23
|
+
return { message: `model: ${badModel} is not a valid model`, source: 'arg', kind: 'invalid', path: 'models' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@CliProfilesFlag()
|
|
28
|
+
profile: string[];
|
|
29
|
+
|
|
30
|
+
@CliModuleFlag({ short: 'm' })
|
|
31
|
+
module: string;
|
|
32
|
+
|
|
16
33
|
abstract getOperation(): keyof ModelStorageSupport;
|
|
17
34
|
|
|
18
|
-
|
|
35
|
+
finalize(): void {
|
|
19
36
|
Env.DEBUG.set(false);
|
|
20
37
|
}
|
|
21
38
|
|
|
@@ -34,18 +51,5 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
34
51
|
];
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
async validate(provider: string, models: string[]): Promise<CliValidationError | undefined> {
|
|
38
|
-
await Registry.init();
|
|
39
|
-
|
|
40
|
-
const candidates = await ModelCandidateUtil.export(this.getOperation());
|
|
41
|
-
if (provider && !candidates.providers.includes(provider)) {
|
|
42
|
-
return { message: `provider: ${provider} is not a valid provider`, source: 'arg' };
|
|
43
|
-
}
|
|
44
|
-
const badModel = models.find(model => model !== '*' && !candidates.models.includes(model));
|
|
45
|
-
if (badModel) {
|
|
46
|
-
return { message: `model: ${badModel} is not a valid model`, source: 'arg' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
54
|
abstract main(...args: unknown[]): ReturnType<CliCommandShape['main']>;
|
|
51
55
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliCommand } from '@travetto/cli';
|
|
2
|
+
import { MethodValidator } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { BaseModelCommand } from './base-command.ts';
|
|
4
5
|
import { ModelExportUtil } from './bin/export.ts';
|
|
@@ -7,11 +8,12 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
7
8
|
/**
|
|
8
9
|
* Exports model schemas
|
|
9
10
|
*/
|
|
10
|
-
@CliCommand(
|
|
11
|
+
@CliCommand()
|
|
11
12
|
export class ModelExportCommand extends BaseModelCommand {
|
|
12
13
|
|
|
13
14
|
getOperation(): 'exportModel' { return 'exportModel'; }
|
|
14
15
|
|
|
16
|
+
@MethodValidator(BaseModelCommand.validate.bind(null, 'exportModel'))
|
|
15
17
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
18
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
|
17
19
|
await ModelExportUtil.run(resolved.provider, resolved.models);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliCommand, cliTpl } from '@travetto/cli';
|
|
2
|
+
import { MethodValidator } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { BaseModelCommand } from './base-command.ts';
|
|
4
5
|
import { ModelInstallUtil } from './bin/install.ts';
|
|
@@ -7,11 +8,12 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
7
8
|
/**
|
|
8
9
|
* Installing models
|
|
9
10
|
*/
|
|
10
|
-
@CliCommand(
|
|
11
|
+
@CliCommand()
|
|
11
12
|
export class ModelInstallCommand extends BaseModelCommand {
|
|
12
13
|
|
|
13
14
|
getOperation(): 'upsertModel' { return 'upsertModel'; }
|
|
14
15
|
|
|
16
|
+
@MethodValidator(BaseModelCommand.validate.bind(null, 'upsertModel'))
|
|
15
17
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
18
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
|
17
19
|
await ModelInstallUtil.run(resolved.provider, resolved.models);
|
package/support/doc.support.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export const Links = {
|
|
|
22
22
|
Blob: toLink('Blob', toConcrete<ModelBlobSupport>()),
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export const ModelTypes = (fn:
|
|
25
|
+
export const ModelTypes = (fn: Function): DocJSXElement[] => {
|
|
26
26
|
const { content } = DocFileUtil.readSource(fn);
|
|
27
27
|
const found: DocJSXElementByFn<'CodeLink'>[] = [];
|
|
28
28
|
const seen = new Set<string>();
|
package/support/test/base.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
2
|
-
import {
|
|
2
|
+
import { RuntimeError, castTo, type Class, classConstruct } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { ModelBulkUtil } from '../../src/util/bulk.ts';
|
|
5
5
|
import { ModelCrudUtil } from '../../src/util/crud.ts';
|
|
@@ -27,7 +27,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
27
27
|
}
|
|
28
28
|
return i;
|
|
29
29
|
} else {
|
|
30
|
-
throw new
|
|
30
|
+
throw new RuntimeError(`Size is not supported for this service: ${this.serviceClass.name}`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
package/support/test/blob.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Suite, Test, TestFixtures } from '@travetto/test';
|
|
4
|
-
import { BinaryUtil, Util } from '@travetto/runtime';
|
|
4
|
+
import { BinaryMetadataUtil, BinaryUtil, Util } from '@travetto/runtime';
|
|
5
5
|
|
|
6
6
|
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
7
7
|
|
|
8
8
|
import type { ModelBlobSupport } from '../../src/types/blob.ts';
|
|
9
9
|
import { ModelBlobUtil } from '../../src/util/blob.ts';
|
|
10
10
|
|
|
11
|
-
const meta = BinaryUtil.getBlobMeta;
|
|
12
|
-
|
|
13
11
|
@Suite()
|
|
14
12
|
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
15
13
|
|
|
@@ -18,51 +16,51 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
18
16
|
@Test()
|
|
19
17
|
async writeBasic(): Promise<void> {
|
|
20
18
|
const service = await this.service;
|
|
21
|
-
const buffer = await this.fixture.
|
|
19
|
+
const buffer = await this.fixture.readBinaryArray('/asset.yml');
|
|
22
20
|
|
|
23
21
|
const id = Util.uuid();
|
|
24
22
|
|
|
25
23
|
await service.upsertBlob(id, buffer);
|
|
26
|
-
const m = await service.
|
|
27
|
-
const retrieved = await service.
|
|
24
|
+
const m = await service.getBlobMetadata(id);
|
|
25
|
+
const retrieved = await service.getBlobMetadata(id);
|
|
28
26
|
assert.deepStrictEqual(m, retrieved);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
@Test()
|
|
32
30
|
async upsert(): Promise<void> {
|
|
33
31
|
const service = await this.service;
|
|
34
|
-
const buffer = await this.fixture.
|
|
32
|
+
const buffer = await this.fixture.readBinaryArray('/asset.yml');
|
|
35
33
|
|
|
36
34
|
const id = Util.uuid();
|
|
37
35
|
|
|
38
36
|
await service.upsertBlob(id, buffer, { hash: '10' });
|
|
39
|
-
assert((await service.
|
|
37
|
+
assert((await service.getBlobMetadata(id)).hash === '10');
|
|
40
38
|
|
|
41
39
|
await service.upsertBlob(id, buffer, { hash: '20' });
|
|
42
|
-
assert((await service.
|
|
40
|
+
assert((await service.getBlobMetadata(id)).hash === '20');
|
|
43
41
|
|
|
44
42
|
await service.upsertBlob(id, buffer, { hash: '30' }, false);
|
|
45
|
-
assert((await service.
|
|
43
|
+
assert((await service.getBlobMetadata(id)).hash === '20');
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
@Test()
|
|
49
47
|
async writeStream(): Promise<void> {
|
|
50
48
|
const service = await this.service;
|
|
51
|
-
const buffer = await this.fixture.
|
|
49
|
+
const buffer = await this.fixture.readBinaryArray('/asset.yml');
|
|
52
50
|
|
|
53
51
|
const id = Util.uuid();
|
|
54
52
|
await service.upsertBlob(id, buffer);
|
|
55
|
-
const { hash } = await service.
|
|
53
|
+
const { hash } = await service.getBlobMetadata(id);
|
|
56
54
|
|
|
57
55
|
const retrieved = await service.getBlob(id);
|
|
58
|
-
const { hash: received } =
|
|
56
|
+
const { hash: received } = BinaryMetadataUtil.read(retrieved)!;
|
|
59
57
|
assert(hash === received);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
@Test()
|
|
63
61
|
async writeAndDelete(): Promise<void> {
|
|
64
62
|
const service = await this.service;
|
|
65
|
-
const buffer = await this.fixture.
|
|
63
|
+
const buffer = await this.fixture.readBinaryArray('/asset.yml');
|
|
66
64
|
|
|
67
65
|
const id = Util.uuid();
|
|
68
66
|
await service.upsertBlob(id, buffer);
|
|
@@ -77,7 +75,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
77
75
|
@Test()
|
|
78
76
|
async partialStream(): Promise<void> {
|
|
79
77
|
const service = await this.service;
|
|
80
|
-
const buffer = await this.fixture.
|
|
78
|
+
const buffer = await this.fixture.readBinaryArray('/text.txt');
|
|
81
79
|
|
|
82
80
|
const id = Util.uuid();
|
|
83
81
|
await service.upsertBlob(id, buffer);
|
|
@@ -89,19 +87,19 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
89
87
|
|
|
90
88
|
const partial = await service.getBlob(id, { start: 10, end: 20 });
|
|
91
89
|
assert(partial.size === 11);
|
|
92
|
-
const partialMeta =
|
|
90
|
+
const partialMeta = BinaryMetadataUtil.read(partial)!;
|
|
93
91
|
const subContent = await partial.text();
|
|
94
|
-
const range =
|
|
92
|
+
const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
|
|
95
93
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
96
94
|
|
|
97
|
-
const og = await this.fixture.
|
|
95
|
+
const og = await this.fixture.readText('/text.txt');
|
|
98
96
|
|
|
99
97
|
assert(subContent === og.substring(10, 21));
|
|
100
98
|
|
|
101
99
|
const partialUnbounded = await service.getBlob(id, { start: 10 });
|
|
102
|
-
const partialUnboundedMeta =
|
|
100
|
+
const partialUnboundedMeta = BinaryMetadataUtil.read(partialUnbounded)!;
|
|
103
101
|
const subContent2 = await partialUnbounded.text();
|
|
104
|
-
const range2 =
|
|
102
|
+
const range2 = BinaryMetadataUtil.enforceRange({ start: 10 }, partialUnboundedMeta);
|
|
105
103
|
assert(subContent2.length === (range2.end - range2.start) + 1);
|
|
106
104
|
assert(subContent2.startsWith('klm'));
|
|
107
105
|
assert(subContent2.endsWith('xyz'));
|
|
@@ -123,15 +121,16 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
123
121
|
@Test()
|
|
124
122
|
async writeAndGet() {
|
|
125
123
|
const service = await this.service;
|
|
126
|
-
const buffer = await this.fixture.
|
|
124
|
+
const buffer = await this.fixture.readBinaryArray('/asset.yml');
|
|
127
125
|
await service.upsertBlob('orange', buffer, { contentType: 'text/yaml', filename: 'asset.yml' });
|
|
128
126
|
const saved = await service.getBlob('orange');
|
|
129
|
-
const savedMeta =
|
|
127
|
+
const savedMeta = BinaryMetadataUtil.read(saved)!;
|
|
128
|
+
console.error(savedMeta);
|
|
130
129
|
|
|
131
130
|
assert('text/yaml' === savedMeta.contentType);
|
|
132
|
-
assert(buffer.
|
|
131
|
+
assert(buffer.byteLength === savedMeta.size);
|
|
133
132
|
assert('asset.yml' === savedMeta.filename);
|
|
134
|
-
assert(
|
|
133
|
+
assert(!!savedMeta.hash);
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
@Test()
|
|
@@ -140,55 +139,54 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
140
139
|
|
|
141
140
|
await this.writeAndGet();
|
|
142
141
|
|
|
143
|
-
await service.
|
|
142
|
+
await service.updateBlobMetadata('orange', {
|
|
144
143
|
contentType: 'text/yml',
|
|
145
144
|
filename: 'orange.yml'
|
|
146
145
|
});
|
|
147
146
|
|
|
148
|
-
const savedMeta = await service.
|
|
147
|
+
const savedMeta = await service.getBlobMetadata('orange');
|
|
149
148
|
|
|
150
149
|
assert('text/yml' === savedMeta.contentType);
|
|
151
150
|
assert('orange.yml' === savedMeta.filename);
|
|
152
|
-
assert(
|
|
151
|
+
assert(savedMeta.hash === undefined);
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
@Test({ skip: (x: unknown) => !(x as ModelBlobSuite).serviceClass.prototype.getBlobWriteUrl })
|
|
154
|
+
@Test({ skip: ModelBlobSuite.ifNot(ModelBlobUtil.isWriteUrlSupported) })
|
|
157
155
|
async signedUrl() {
|
|
158
156
|
const service = await this.service;
|
|
159
157
|
|
|
160
|
-
const
|
|
161
|
-
for (let i = 0; i <
|
|
162
|
-
|
|
158
|
+
const bytes = BinaryUtil.binaryArrayToBuffer(BinaryUtil.makeBinaryArray(1.5 * 10000));
|
|
159
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
160
|
+
bytes.writeUInt8(Math.trunc(Math.random() * 255), i);
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
const writable = await service.getBlobWriteUrl!('largeFile/one', {
|
|
166
164
|
contentType: 'image/jpeg',
|
|
167
165
|
});
|
|
168
166
|
|
|
169
|
-
console.log(writable);
|
|
170
167
|
assert(writable);
|
|
171
168
|
|
|
172
169
|
const response = await fetch(writable, {
|
|
173
170
|
method: 'PUT',
|
|
174
|
-
body: new File([
|
|
171
|
+
body: new File([bytes], 'gary', { type: 'image/jpeg' }),
|
|
175
172
|
});
|
|
176
173
|
|
|
177
174
|
console.error(await response.text());
|
|
178
175
|
|
|
179
176
|
assert(response.ok);
|
|
180
177
|
|
|
181
|
-
await service.
|
|
178
|
+
await service.updateBlobMetadata('largeFile/one', {
|
|
182
179
|
contentType: 'image/jpeg',
|
|
183
180
|
title: 'orange',
|
|
184
181
|
filename: 'gary',
|
|
185
|
-
size:
|
|
182
|
+
size: bytes.byteLength,
|
|
186
183
|
});
|
|
187
184
|
|
|
188
185
|
const found = await service.getBlob('largeFile/one');
|
|
189
|
-
|
|
186
|
+
const foundMeta = BinaryMetadataUtil.read(found);
|
|
187
|
+
assert(found.size === bytes.byteLength);
|
|
190
188
|
assert(found.type === 'image/jpeg');
|
|
191
|
-
assert(
|
|
192
|
-
assert(
|
|
189
|
+
assert(foundMeta.title === 'orange');
|
|
190
|
+
assert(foundMeta.filename === 'gary');
|
|
193
191
|
}
|
|
194
192
|
}
|
package/support/test/crud.ts
CHANGED
|
@@ -67,6 +67,13 @@ class Dated {
|
|
|
67
67
|
updatedDate: Date;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
@Model()
|
|
71
|
+
class BigIntModel {
|
|
72
|
+
id: string;
|
|
73
|
+
largeNumber: bigint;
|
|
74
|
+
optionalBigInt?: bigint;
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
@Suite()
|
|
71
78
|
export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
72
79
|
|
|
@@ -343,4 +350,39 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
343
350
|
assert(o2.simples);
|
|
344
351
|
assert(o2.simples.length === 1);
|
|
345
352
|
}
|
|
353
|
+
|
|
354
|
+
@Test('Verify bigint storage and retrieval')
|
|
355
|
+
async testBigIntReadWrite() {
|
|
356
|
+
const service = await this.service;
|
|
357
|
+
|
|
358
|
+
// Create with bigint values
|
|
359
|
+
const created = await service.create(BigIntModel, BigIntModel.from({
|
|
360
|
+
largeNumber: 9007199254740991n, // Number.MAX_SAFE_INTEGER as bigint
|
|
361
|
+
optionalBigInt: 1234567890123456789n
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
assert(created.id);
|
|
365
|
+
assert.strictEqual(created.largeNumber, 9007199254740991n);
|
|
366
|
+
assert.strictEqual(created.optionalBigInt, 1234567890123456789n);
|
|
367
|
+
|
|
368
|
+
// Retrieve and verify
|
|
369
|
+
const retrieved = await service.get(BigIntModel, created.id);
|
|
370
|
+
assert.strictEqual(retrieved.largeNumber, 9007199254740991n);
|
|
371
|
+
assert.strictEqual(retrieved.optionalBigInt, 1234567890123456789n);
|
|
372
|
+
|
|
373
|
+
// Update with new bigint value
|
|
374
|
+
const updated = await service.update(BigIntModel, BigIntModel.from({
|
|
375
|
+
id: created.id,
|
|
376
|
+
largeNumber: 18014398509481982n,
|
|
377
|
+
optionalBigInt: undefined
|
|
378
|
+
}));
|
|
379
|
+
|
|
380
|
+
assert.strictEqual(updated.largeNumber, 18014398509481982n);
|
|
381
|
+
assert.strictEqual(updated.optionalBigInt, undefined);
|
|
382
|
+
|
|
383
|
+
// Verify update persisted
|
|
384
|
+
const final = await service.get(BigIntModel, created.id);
|
|
385
|
+
assert.strictEqual(final.largeNumber, 18014398509481982n);
|
|
386
|
+
assert(!final.optionalBigInt);
|
|
387
|
+
}
|
|
346
388
|
}
|
package/support/test/expiry.ts
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert';
|
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
|
-
import { type TimeSpan,
|
|
5
|
+
import { type TimeSpan, TimeUtil } from '@travetto/runtime';
|
|
6
6
|
|
|
7
7
|
import { ExpiresAt, Model } from '../../src/registry/decorator.ts';
|
|
8
8
|
import type { ModelExpirySupport } from '../../src/types/expiry.ts';
|
|
@@ -23,12 +23,12 @@ export abstract class ModelExpirySuite extends BaseModelSuite<ModelExpirySupport
|
|
|
23
23
|
|
|
24
24
|
delayFactor: number = 1;
|
|
25
25
|
|
|
26
|
-
async wait(
|
|
27
|
-
await timers.setTimeout(TimeUtil.
|
|
26
|
+
async wait(input: TimeSpan | number | string) {
|
|
27
|
+
await timers.setTimeout(TimeUtil.duration(input, 'ms') * this.delayFactor);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
timeFromNow(
|
|
31
|
-
return TimeUtil.fromNow(TimeUtil.
|
|
30
|
+
timeFromNow(input: TimeSpan | number | string) {
|
|
31
|
+
return TimeUtil.fromNow(TimeUtil.duration(input, 'ms') * this.delayFactor);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
@Test()
|
package/support/test/suite.ts
CHANGED
|
@@ -1,14 +1,81 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Class } from '@travetto/runtime';
|
|
2
2
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
3
3
|
import { Registry } from '@travetto/registry';
|
|
4
|
-
import { SuiteRegistryIndex, TestFixtures } from '@travetto/test';
|
|
4
|
+
import { SuiteRegistryIndex, TestFixtures, type SuitePhaseHandler } from '@travetto/test';
|
|
5
5
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
6
6
|
|
|
7
7
|
import { ModelBlobUtil } from '../../src/util/blob.ts';
|
|
8
8
|
import { ModelStorageUtil } from '../../src/util/storage.ts';
|
|
9
9
|
import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type ConfigType = { autoCreate?: boolean, namespace?: string };
|
|
12
|
+
|
|
13
|
+
class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass: Class }> implements SuitePhaseHandler<T> {
|
|
14
|
+
qualifier?: symbol;
|
|
15
|
+
target: Class<T>;
|
|
16
|
+
constructor(target: Class<T>, qualifier?: symbol) {
|
|
17
|
+
this.qualifier = qualifier;
|
|
18
|
+
this.target = target;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async beforeAll(instance: T) {
|
|
22
|
+
await Registry.init();
|
|
23
|
+
|
|
24
|
+
const config = await DependencyRegistryIndex.getInstance<ConfigType>(instance.configClass);
|
|
25
|
+
if ('namespace' in config) {
|
|
26
|
+
config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// We manually create
|
|
30
|
+
config.autoCreate = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async beforeEach(instance: T) {
|
|
34
|
+
const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
|
|
35
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
36
|
+
await service.createStorage();
|
|
37
|
+
if (service.upsertModel) {
|
|
38
|
+
await Promise.all(ModelRegistryIndex.getClasses()
|
|
39
|
+
.filter(cls => cls === SchemaRegistryIndex.getBaseClass(cls))
|
|
40
|
+
.map(modelCls => service.upsertModel!(modelCls)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async afterEach(instance: T) {
|
|
46
|
+
const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
|
|
47
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
48
|
+
const models = ModelRegistryIndex.getClasses()
|
|
49
|
+
.filter(model => model === SchemaRegistryIndex.getBaseClass(model));
|
|
50
|
+
|
|
51
|
+
if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
|
|
52
|
+
await service.truncateBlob();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (service.truncateModel) {
|
|
56
|
+
await Promise.all(models.map(model => service.truncateModel!(model)));
|
|
57
|
+
} else if (service.deleteModel) {
|
|
58
|
+
await Promise.all(models.map(model => service.deleteModel!(model)));
|
|
59
|
+
} else {
|
|
60
|
+
await service.deleteStorage(); // Purge it all
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async afterAll(instance: T) {
|
|
66
|
+
const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
|
|
67
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
68
|
+
if (service.deleteModel) {
|
|
69
|
+
for (const model of ModelRegistryIndex.getClasses()) {
|
|
70
|
+
if (model === SchemaRegistryIndex.getBaseClass(model)) {
|
|
71
|
+
await service.deleteModel(model);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await service.deleteStorage();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
12
79
|
|
|
13
80
|
/**
|
|
14
81
|
* Model test suite decorator
|
|
@@ -21,69 +88,7 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
|
|
|
21
88
|
return (target: Class<T>): void => {
|
|
22
89
|
target.prototype.fixtures = fixtures;
|
|
23
90
|
SuiteRegistryIndex.getForRegister(target).register({
|
|
24
|
-
|
|
25
|
-
async function (this: T & { [Loaded]?: boolean }) {
|
|
26
|
-
await Registry.init();
|
|
27
|
-
|
|
28
|
-
if (!this[Loaded]) {
|
|
29
|
-
const config = await DependencyRegistryIndex.getInstance(this.configClass);
|
|
30
|
-
if ('namespace' in config) {
|
|
31
|
-
config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
|
|
32
|
-
}
|
|
33
|
-
// We manually create
|
|
34
|
-
config.autoCreate = false;
|
|
35
|
-
this[Loaded] = true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
beforeEach: [
|
|
40
|
-
async function (this: T) {
|
|
41
|
-
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
42
|
-
if (ModelStorageUtil.isSupported(service)) {
|
|
43
|
-
await service.createStorage();
|
|
44
|
-
if (service.upsertModel) {
|
|
45
|
-
await Promise.all(ModelRegistryIndex.getClasses()
|
|
46
|
-
.filter(x => x === SchemaRegistryIndex.getBaseClass(x))
|
|
47
|
-
.map(m => service.upsertModel!(m)));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
|
-
afterEach: [
|
|
53
|
-
async function (this: T) {
|
|
54
|
-
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
55
|
-
if (ModelStorageUtil.isSupported(service)) {
|
|
56
|
-
const models = ModelRegistryIndex.getClasses().filter(m => m === SchemaRegistryIndex.getBaseClass(m));
|
|
57
|
-
|
|
58
|
-
if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
|
|
59
|
-
await service.truncateBlob();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (service.truncateModel) {
|
|
63
|
-
await Promise.all(models.map(x => service.truncateModel!(x)));
|
|
64
|
-
} else if (service.deleteModel) {
|
|
65
|
-
await Promise.all(models.map(x => service.deleteModel!(x)));
|
|
66
|
-
} else {
|
|
67
|
-
await service.deleteStorage(); // Purge it all
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
],
|
|
72
|
-
afterAll: [
|
|
73
|
-
async function (this: T) {
|
|
74
|
-
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
75
|
-
if (ModelStorageUtil.isSupported(service)) {
|
|
76
|
-
if (service.deleteModel) {
|
|
77
|
-
for (const m of ModelRegistryIndex.getClasses()) {
|
|
78
|
-
if (m === SchemaRegistryIndex.getBaseClass(m)) {
|
|
79
|
-
await service.deleteModel(m);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
await service.deleteStorage();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
]
|
|
91
|
+
phaseHandlers: [new ModelSuiteHandler(target, qualifier)]
|
|
87
92
|
});
|
|
88
93
|
};
|
|
89
94
|
}
|