@travetto/model 8.0.0-alpha.8 → 8.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 +91 -102
- package/__index__.ts +7 -11
- package/package.json +18 -18
- package/src/error/exists.ts +1 -1
- package/src/error/invalid-index.ts +5 -3
- package/src/error/invalid-sub-type.ts +1 -1
- package/src/error/not-found.ts +1 -1
- package/src/error/unique.ts +13 -0
- package/src/registry/decorator.ts +38 -32
- package/src/registry/registry-adapter.ts +11 -8
- package/src/registry/registry-index.ts +12 -33
- package/src/registry/types.ts +24 -39
- package/src/types/basic.ts +3 -3
- package/src/types/blob.ts +6 -6
- package/src/types/bulk.ts +5 -9
- package/src/types/crud.ts +9 -6
- package/src/types/expiry.ts +2 -2
- package/src/types/model.ts +11 -2
- package/src/types/storage.ts +2 -3
- package/src/util/blob.ts +2 -2
- package/src/util/bulk.ts +6 -3
- package/src/util/crud.ts +105 -22
- package/src/util/expiry.ts +4 -5
- package/src/util/storage.ts +3 -4
- package/support/base-command.ts +3 -5
- package/support/bin/candidate.ts +8 -11
- package/support/bin/export.ts +2 -2
- package/support/bin/install.ts +2 -2
- package/support/cli.model_export.ts +9 -5
- package/support/cli.model_install.ts +9 -5
- package/support/doc.support.tsx +16 -20
- package/support/test/base.ts +14 -12
- package/support/test/basic.ts +44 -3
- package/support/test/blob.ts +8 -9
- package/support/test/bulk.ts +16 -5
- package/support/test/crud.ts +134 -82
- package/support/test/expiry.ts +39 -21
- package/support/test/polymorphism.ts +43 -143
- package/support/test/suite.ts +15 -12
- package/src/types/indexed.ts +0 -43
- package/src/util/indexed.ts +0 -144
- package/support/test/indexed.ts +0 -190
|
@@ -7,9 +7,10 @@ import type { ModelConfig } from './types.ts';
|
|
|
7
7
|
function combineClasses(target: ModelConfig, sources: Partial<ModelConfig>[]): ModelConfig {
|
|
8
8
|
for (const source of sources) {
|
|
9
9
|
Object.assign(target, source, {
|
|
10
|
-
indices:
|
|
10
|
+
indices: { ...(target.indices || {}), ...(source.indices || {}) },
|
|
11
11
|
postLoad: [...(target.postLoad || []), ...(source.postLoad || [])],
|
|
12
12
|
prePersist: [...(target.prePersist || []), ...(source.prePersist || [])],
|
|
13
|
+
transientFields: [...(target.transientFields || []), ...(source.transientFields || [])]
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
if (target.store) {
|
|
@@ -27,14 +28,15 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
register(...data: Partial<ModelConfig>[]): ModelConfig {
|
|
30
|
-
const config = this.#config ??= {
|
|
31
|
+
const config = (this.#config ??= {
|
|
31
32
|
class: this.#cls,
|
|
32
|
-
indices:
|
|
33
|
+
indices: {},
|
|
33
34
|
autoCreate: 'development',
|
|
34
35
|
store: this.#cls.name,
|
|
35
36
|
postLoad: [],
|
|
36
|
-
prePersist: []
|
|
37
|
-
|
|
37
|
+
prePersist: [],
|
|
38
|
+
transientFields: []
|
|
39
|
+
});
|
|
38
40
|
combineClasses(config, data);
|
|
39
41
|
return config;
|
|
40
42
|
}
|
|
@@ -48,12 +50,13 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
48
50
|
config.store = parent.store;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
config.postLoad = [...parent.postLoad ?? [], ...config.postLoad ?? []];
|
|
52
|
-
config.prePersist = [...parent.prePersist ?? [], ...config.prePersist ?? []];
|
|
53
|
+
config.postLoad = [...(parent.postLoad ?? []), ...(config.postLoad ?? [])];
|
|
54
|
+
config.prePersist = [...(parent.prePersist ?? []), ...(config.prePersist ?? [])];
|
|
55
|
+
config.transientFields = [...(parent.transientFields ?? []), ...(config.transientFields ?? [])];
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
get(): ModelConfig {
|
|
57
60
|
return this.#config;
|
|
58
61
|
}
|
|
59
|
-
}
|
|
62
|
+
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import { type RegistryIndex, RegistryIndexStore
|
|
2
|
-
import {
|
|
1
|
+
import { Registry, type RegistryIndex, RegistryIndexStore } from '@travetto/registry';
|
|
2
|
+
import { type Class, castTo, RuntimeError } from '@travetto/runtime';
|
|
3
3
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import type { IndexConfig, IndexType, ModelConfig } from './types.ts';
|
|
6
5
|
import type { ModelType } from '../types/model.ts';
|
|
7
6
|
import { ModelRegistryAdapter } from './registry-adapter.ts';
|
|
8
|
-
import {
|
|
9
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
10
|
-
|
|
11
|
-
type IndexResult<T extends ModelType, K extends IndexType[]> = IndexConfig<T> & { type: K[number] };
|
|
7
|
+
import type { IndexConfig, ModelConfig } from './types.ts';
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
10
|
* Model registry index for managing model configurations across classes
|
|
15
11
|
*/
|
|
16
12
|
export class ModelRegistryIndex implements RegistryIndex {
|
|
17
|
-
|
|
18
13
|
static #instance = Registry.registerIndex(this);
|
|
19
14
|
|
|
20
15
|
static getForRegister(cls: Class): ModelRegistryAdapter {
|
|
@@ -33,12 +28,8 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
33
28
|
return this.#instance.getStoreName(cls);
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
static getIndices<T extends ModelType
|
|
37
|
-
return this.#instance.getIndices(cls
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
|
|
41
|
-
return this.#instance.getIndex(cls, name, supportedTypes);
|
|
31
|
+
static getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
|
|
32
|
+
return this.#instance.getIndices(cls);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
static getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
|
|
@@ -58,7 +49,9 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
58
49
|
|
|
59
50
|
store = new RegistryIndexStore(ModelRegistryAdapter);
|
|
60
51
|
|
|
61
|
-
/** @private */ constructor(source: unknown) {
|
|
52
|
+
/** @private */ constructor(source: unknown) {
|
|
53
|
+
Registry.validateConstructor(source);
|
|
54
|
+
}
|
|
62
55
|
|
|
63
56
|
onCreate(cls: Class): void {
|
|
64
57
|
const schema = SchemaRegistryIndex.getConfig(cls);
|
|
@@ -71,7 +64,7 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
71
64
|
const { store } = this.getConfig(cls);
|
|
72
65
|
let classes = this.#modelNameMapping.get(store);
|
|
73
66
|
if (!classes) {
|
|
74
|
-
this.#modelNameMapping.set(store, classes = new Set());
|
|
67
|
+
this.#modelNameMapping.set(store, (classes = new Set()));
|
|
75
68
|
}
|
|
76
69
|
classes.add(cls.Ⲑid);
|
|
77
70
|
|
|
@@ -94,25 +87,11 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
94
87
|
return this.store.get(cls).get().store;
|
|
95
88
|
}
|
|
96
89
|
|
|
97
|
-
/**
|
|
98
|
-
* Get Index
|
|
99
|
-
*/
|
|
100
|
-
getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
|
|
101
|
-
const config = this.getConfig(cls).indices?.find((idx): idx is IndexConfig<T> => idx.name === name);
|
|
102
|
-
if (!config) {
|
|
103
|
-
throw new NotFoundError(`${cls.name} Index`, `${name}`);
|
|
104
|
-
}
|
|
105
|
-
if (supportedTypes && !supportedTypes.includes(config.type)) {
|
|
106
|
-
throw new IndexNotSupported(cls, config, `${config.type} indices are not supported.`);
|
|
107
|
-
}
|
|
108
|
-
return config;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
90
|
/**
|
|
112
91
|
* Get Indices
|
|
113
92
|
*/
|
|
114
|
-
getIndices<T extends ModelType
|
|
115
|
-
return (this.getConfig(cls).indices ?? [])
|
|
93
|
+
getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
|
|
94
|
+
return Object.values(this.getConfig(cls).indices ?? []);
|
|
116
95
|
}
|
|
117
96
|
|
|
118
97
|
/**
|
|
@@ -126,4 +105,4 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
126
105
|
}
|
|
127
106
|
return castTo(expiry);
|
|
128
107
|
}
|
|
129
|
-
}
|
|
108
|
+
}
|
package/src/registry/types.ts
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import type { Class
|
|
1
|
+
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { ModelType } from '../types/model.ts';
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
export type DataHandler<T = unknown> = (inst: T) => Promise<T | void> | T | void;
|
|
6
6
|
|
|
7
|
-
export type
|
|
8
|
-
[P in keyof T]?:
|
|
9
|
-
T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
|
|
10
|
-
};
|
|
7
|
+
export type PrePersistScope = 'full' | 'partial' | 'all';
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Index options
|
|
11
|
+
*/
|
|
12
|
+
export type IndexConfig<V extends string = string, T extends ModelType = ModelType> = {
|
|
13
|
+
/**
|
|
14
|
+
* Index name
|
|
15
|
+
*/
|
|
16
|
+
name: string;
|
|
17
|
+
/**
|
|
18
|
+
* Type
|
|
19
|
+
*/
|
|
20
|
+
type: V;
|
|
21
|
+
/**
|
|
22
|
+
* Class the index belongs to
|
|
23
|
+
*/
|
|
24
|
+
class: Class<T>;
|
|
15
25
|
};
|
|
16
26
|
|
|
17
|
-
export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
|
|
18
|
-
|
|
19
|
-
export type PrePersistScope = 'full' | 'partial' | 'all';
|
|
20
|
-
|
|
21
27
|
/**
|
|
22
28
|
* Model config
|
|
23
29
|
*/
|
|
@@ -33,7 +39,7 @@ export class ModelConfig<T extends ModelType = ModelType> {
|
|
|
33
39
|
/**
|
|
34
40
|
* Indices
|
|
35
41
|
*/
|
|
36
|
-
indices?: IndexConfig
|
|
42
|
+
indices?: Record<string, IndexConfig>;
|
|
37
43
|
/**
|
|
38
44
|
* Vendor specific extras
|
|
39
45
|
*/
|
|
@@ -49,34 +55,13 @@ export class ModelConfig<T extends ModelType = ModelType> {
|
|
|
49
55
|
/**
|
|
50
56
|
* Pre-persist handlers
|
|
51
57
|
*/
|
|
52
|
-
prePersist?: { scope: PrePersistScope
|
|
58
|
+
prePersist?: { scope: PrePersistScope; handler: DataHandler<unknown> }[];
|
|
53
59
|
/**
|
|
54
60
|
* Post-load handlers
|
|
55
61
|
*/
|
|
56
62
|
postLoad?: DataHandler<unknown>[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Supported index types
|
|
61
|
-
*/
|
|
62
|
-
export type IndexType = 'unique' | 'unsorted' | 'sorted';
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Index options
|
|
66
|
-
*/
|
|
67
|
-
export type IndexConfig<T extends ModelType> = {
|
|
68
|
-
/**
|
|
69
|
-
* Index name
|
|
70
|
-
*/
|
|
71
|
-
name: string;
|
|
72
63
|
/**
|
|
73
|
-
* Fields
|
|
64
|
+
* Fields to ignore during persistence
|
|
74
65
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* Type
|
|
78
|
-
*/
|
|
79
|
-
type: IndexType;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export type IndexField<T extends ModelType> = IndexClauseRaw<RetainPrimitiveFields<T>>;
|
|
66
|
+
transientFields?: string[];
|
|
67
|
+
}
|
package/src/types/basic.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import type { ModelIdSource, ModelType, OptionalId } from './model.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Interface for basic data interface
|
|
6
7
|
* @concrete
|
|
7
8
|
*/
|
|
8
9
|
export interface ModelBasicSupport<C = unknown> {
|
|
9
|
-
|
|
10
10
|
/**
|
|
11
11
|
* Id Source
|
|
12
12
|
*/
|
|
@@ -37,4 +37,4 @@ export interface ModelBasicSupport<C = unknown> {
|
|
|
37
37
|
* @throws {NotFoundError} When an item is not found
|
|
38
38
|
*/
|
|
39
39
|
delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void>;
|
|
40
|
-
}
|
|
40
|
+
}
|
package/src/types/blob.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BinaryMetadata, BinaryType, ByteRange, TimeSpan } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Support for Blobs CRUD.
|
|
@@ -6,7 +6,6 @@ import type { BinaryType, BinaryMetadata, ByteRange, TimeSpan } from '@travetto/
|
|
|
6
6
|
* @concrete
|
|
7
7
|
*/
|
|
8
8
|
export interface ModelBlobSupport {
|
|
9
|
-
|
|
10
9
|
/**
|
|
11
10
|
* Upsert blob to storage
|
|
12
11
|
* @param location The location of the blob
|
|
@@ -42,12 +41,13 @@ export interface ModelBlobSupport {
|
|
|
42
41
|
updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* Produces an externally usable URL for sharing limited read access to a specific resource
|
|
44
|
+
* Produces an externally usable URL for sharing limited read access to a specific resource.
|
|
45
|
+
* If expiresIn is explicitly set to false, returns a direct/public URL.
|
|
46
46
|
*
|
|
47
47
|
* @param location The asset location to read from
|
|
48
|
-
* @param expiresIn Expiry
|
|
48
|
+
* @param expiresIn Expiry or false for public/direct URL
|
|
49
49
|
*/
|
|
50
|
-
getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
|
|
50
|
+
getBlobReadUrl?(location: string, expiresIn?: TimeSpan | false): Promise<string>;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Produces an externally usable URL for sharing allowing direct write access
|
|
@@ -57,4 +57,4 @@ export interface ModelBlobSupport {
|
|
|
57
57
|
* @param expiresIn Expiry
|
|
58
58
|
*/
|
|
59
59
|
getBlobWriteUrl?(location: string, metadata: BinaryMetadata, expiresIn?: TimeSpan): Promise<string>;
|
|
60
|
-
}
|
|
60
|
+
}
|
package/src/types/bulk.ts
CHANGED
|
@@ -2,16 +2,12 @@ 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';
|
|
5
|
-
import type { ModelType, OptionalId } from '
|
|
5
|
+
import type { ModelType, OptionalId } from './model.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Bulk operation. Each operation has a single action and payload
|
|
9
9
|
*/
|
|
10
|
-
export type BulkOperation<T extends ModelType> =
|
|
11
|
-
{ delete?: T } &
|
|
12
|
-
{ insert?: OptionalId<T> } &
|
|
13
|
-
{ update?: T } &
|
|
14
|
-
{ upsert?: OptionalId<T> };
|
|
10
|
+
export type BulkOperation<T extends ModelType> = { delete?: T } & { insert?: OptionalId<T> } & { update?: T } & { upsert?: OptionalId<T> };
|
|
15
11
|
|
|
16
12
|
/**
|
|
17
13
|
* Bulk response provides a summary of all the operations
|
|
@@ -37,13 +33,13 @@ export interface BulkResponse<E = unknown> {
|
|
|
37
33
|
};
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
type BulkErrorItem = { message: string
|
|
36
|
+
type BulkErrorItem = { message: string; type: string; errors?: ValidationError[]; idx: number };
|
|
41
37
|
|
|
42
38
|
/**
|
|
43
39
|
* Bulk processing error
|
|
44
40
|
*/
|
|
45
41
|
export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }> {
|
|
46
|
-
constructor(errors: { idx: number
|
|
42
|
+
constructor(errors: { idx: number; error: ValidationResultError }[]) {
|
|
47
43
|
super('Bulk processing errors have occurred', {
|
|
48
44
|
category: 'data',
|
|
49
45
|
details: {
|
|
@@ -63,4 +59,4 @@ export class BulkProcessError extends RuntimeError<{ errors: BulkErrorItem[] }>
|
|
|
63
59
|
*/
|
|
64
60
|
export interface ModelBulkSupport extends ModelCrudSupport {
|
|
65
61
|
processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse>;
|
|
66
|
-
}
|
|
62
|
+
}
|
package/src/types/crud.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
4
|
-
|
|
5
3
|
import type { ModelBasicSupport } from './basic.ts';
|
|
4
|
+
import type { ModelListOptions, ModelType, OptionalId } from './model.ts';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Interface for simple CRUD
|
|
9
8
|
* @concrete
|
|
10
9
|
*/
|
|
11
10
|
export interface ModelCrudSupport extends ModelBasicSupport {
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
12
|
* Update an item
|
|
15
13
|
* @param item The document to update.
|
|
@@ -38,7 +36,12 @@ export interface ModelCrudSupport extends ModelBasicSupport {
|
|
|
38
36
|
updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T>;
|
|
39
37
|
|
|
40
38
|
/**
|
|
41
|
-
* List all items
|
|
39
|
+
* List all items of a collection, results returned in batches of items.
|
|
40
|
+
*
|
|
41
|
+
* Note: Batch size hint can be used to optimize batch size, but is not guaranteed.
|
|
42
|
+
*
|
|
43
|
+
* @param cls The class to list
|
|
44
|
+
* @param options Options for listing
|
|
42
45
|
*/
|
|
43
|
-
list<T extends ModelType>(cls: Class<T
|
|
44
|
-
}
|
|
46
|
+
list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
|
|
47
|
+
}
|
package/src/types/expiry.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { ModelType } from '../types/model.ts';
|
|
4
3
|
import type { ModelCrudSupport } from './crud.ts';
|
|
4
|
+
import type { ModelType } from './model.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Support for managing expiration of data
|
|
@@ -14,4 +14,4 @@ export interface ModelExpirySupport extends ModelCrudSupport {
|
|
|
14
14
|
* @returns Returns the number of documents expired
|
|
15
15
|
*/
|
|
16
16
|
deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number>;
|
|
17
|
-
}
|
|
17
|
+
}
|
package/src/types/model.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model Id Source
|
|
3
3
|
*/
|
|
4
|
-
export type ModelIdSource = { create: () => string
|
|
4
|
+
export type ModelIdSource = { create: () => string; valid: (id: string) => boolean };
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Simple model interface
|
|
@@ -16,4 +16,13 @@ export interface ModelType {
|
|
|
16
16
|
id: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
|
|
19
|
+
export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for listing items
|
|
23
|
+
*/
|
|
24
|
+
export interface ModelListOptions {
|
|
25
|
+
abort?: AbortSignal;
|
|
26
|
+
limit?: number;
|
|
27
|
+
batchSizeHint?: number;
|
|
28
|
+
}
|
package/src/types/storage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { ModelType } from '
|
|
3
|
+
import type { ModelType } from './model.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* This interface defines the behavior for dealing with the
|
|
@@ -12,7 +12,6 @@ import type { ModelType } from '../types/model.ts';
|
|
|
12
12
|
* @concrete
|
|
13
13
|
*/
|
|
14
14
|
export interface ModelStorageSupport {
|
|
15
|
-
|
|
16
15
|
/**
|
|
17
16
|
* Should storage modification be allowed
|
|
18
17
|
*/
|
|
@@ -48,4 +47,4 @@ export interface ModelStorageSupport {
|
|
|
48
47
|
* Truncate blob storage data
|
|
49
48
|
*/
|
|
50
49
|
truncateBlob?(): Promise<void>;
|
|
51
|
-
}
|
|
50
|
+
}
|
package/src/util/blob.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { hasFunction } from '@travetto/runtime';
|
|
2
|
+
|
|
2
3
|
import type { ModelBlobSupport } from '../types/blob.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Utilities for processing blobs
|
|
6
7
|
*/
|
|
7
8
|
export class ModelBlobUtil {
|
|
8
|
-
|
|
9
9
|
/**
|
|
10
10
|
* Type guard for determining if service supports blob operations
|
|
11
11
|
*/
|
|
@@ -15,4 +15,4 @@ export class ModelBlobUtil {
|
|
|
15
15
|
* Type guard for determining if service supports blob write urls
|
|
16
16
|
*/
|
|
17
17
|
static isWriteUrlSupported = hasFunction<ModelBlobSupport>('getBlobWriteUrl');
|
|
18
|
-
}
|
|
18
|
+
}
|
package/src/util/bulk.ts
CHANGED
|
@@ -13,7 +13,6 @@ export type BulkPreStore<T extends ModelType> = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export class ModelBulkUtil {
|
|
16
|
-
|
|
17
16
|
/**
|
|
18
17
|
* Type guard for determining if service supports bulk operation
|
|
19
18
|
*/
|
|
@@ -25,7 +24,11 @@ export class ModelBulkUtil {
|
|
|
25
24
|
* @param operations
|
|
26
25
|
* @param provider
|
|
27
26
|
*/
|
|
28
|
-
static async preStore<T extends ModelType>(
|
|
27
|
+
static async preStore<T extends ModelType>(
|
|
28
|
+
cls: Class<T>,
|
|
29
|
+
operations: BulkOperation<T>[],
|
|
30
|
+
provider: ModelCrudProvider
|
|
31
|
+
): Promise<BulkPreStore<T>> {
|
|
29
32
|
const insertedIds = new Map<number, string>();
|
|
30
33
|
const upsertedIds = new Map<number, string>();
|
|
31
34
|
const updatedIds = new Map<number, string>();
|
|
@@ -53,4 +56,4 @@ export class ModelBulkUtil {
|
|
|
53
56
|
}
|
|
54
57
|
return { insertedIds, upsertedIds, updatedIds, existingUpsertedIds, operations };
|
|
55
58
|
}
|
|
56
|
-
}
|
|
59
|
+
}
|
package/src/util/crud.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type BinaryArray, BinaryUtil, type Class, castKey, castTo, hasFunction, JSONUtil, RuntimeError, Util } from '@travetto/runtime';
|
|
2
2
|
import { DataUtil, SchemaRegistryIndex, SchemaValidator, type ValidationError, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
|
-
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
5
|
-
import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
|
|
6
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
7
4
|
import { ExistsError } from '../error/exists.ts';
|
|
8
5
|
import { SubTypeNotSupportedError } from '../error/invalid-sub-type.ts';
|
|
6
|
+
import { NotFoundError } from '../error/not-found.ts';
|
|
7
|
+
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
9
8
|
import type { DataHandler, PrePersistScope } from '../registry/types.ts';
|
|
10
9
|
import type { ModelCrudSupport } from '../types/crud.ts';
|
|
10
|
+
import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
|
|
11
11
|
|
|
12
12
|
type ModelLoadInput = string | BinaryArray | object;
|
|
13
13
|
|
|
@@ -19,7 +19,6 @@ export type ModelCrudProvider = {
|
|
|
19
19
|
* Crud utilities
|
|
20
20
|
*/
|
|
21
21
|
export class ModelCrudUtil {
|
|
22
|
-
|
|
23
22
|
/**
|
|
24
23
|
* Type guard for determining if service supports crud operations
|
|
25
24
|
*/
|
|
@@ -34,16 +33,35 @@ export class ModelCrudUtil {
|
|
|
34
33
|
return { create, valid };
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
static async filterOutNotFound<T extends ModelType>(actions: Promise<T>[] | undefined): Promise<T[]> {
|
|
37
|
+
if (!actions) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return (await Promise.allSettled(actions))
|
|
41
|
+
.map(p => {
|
|
42
|
+
if (p.status === 'fulfilled') {
|
|
43
|
+
return p.value;
|
|
44
|
+
} else if (p.reason instanceof NotFoundError) {
|
|
45
|
+
return undefined!;
|
|
46
|
+
} else {
|
|
47
|
+
throw p.reason;
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.filter(item => !!item);
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
/**
|
|
38
54
|
* Load model
|
|
39
55
|
* @param cls Class to load model for
|
|
40
56
|
* @param input Input as string or plain object
|
|
41
57
|
*/
|
|
42
|
-
static async load<T extends ModelType>(
|
|
58
|
+
static async load<T extends ModelType>(
|
|
59
|
+
cls: Class<T>,
|
|
60
|
+
input: ModelLoadInput,
|
|
61
|
+
onTypeMismatch: 'notfound' | 'exists' = 'notfound'
|
|
62
|
+
): Promise<T> {
|
|
43
63
|
const resolvedInput: object =
|
|
44
|
-
typeof input === 'string' ? JSONUtil.fromUTF8(input) :
|
|
45
|
-
BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) :
|
|
46
|
-
input;
|
|
64
|
+
typeof input === 'string' ? JSONUtil.fromUTF8(input) : BinaryUtil.isBinaryArray(input) ? JSONUtil.fromBinaryArray(input) : input;
|
|
47
65
|
|
|
48
66
|
const result = SchemaRegistryIndex.getBaseClass(cls).from(resolvedInput);
|
|
49
67
|
|
|
@@ -64,7 +82,12 @@ export class ModelCrudUtil {
|
|
|
64
82
|
* @param cls Type to store for
|
|
65
83
|
* @param item Item to store
|
|
66
84
|
*/
|
|
67
|
-
static async preStore<T extends ModelType>(
|
|
85
|
+
static async preStore<T extends ModelType>(
|
|
86
|
+
cls: Class<T>,
|
|
87
|
+
item: Partial<OptionalId<T>>,
|
|
88
|
+
provider: ModelCrudProvider,
|
|
89
|
+
scope: PrePersistScope = 'all'
|
|
90
|
+
): Promise<T> {
|
|
68
91
|
if (!item.id) {
|
|
69
92
|
item.id = provider.idSource.create();
|
|
70
93
|
}
|
|
@@ -96,6 +119,70 @@ export class ModelCrudUtil {
|
|
|
96
119
|
return castTo(item);
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Recursively deletes or masks transient fields from an object based on its Model config.
|
|
124
|
+
* If transient fields are present, returns a copy with those fields set to undefined.
|
|
125
|
+
*/
|
|
126
|
+
static cleanTransientFields<T>(cls: Class<T>, item: T): T {
|
|
127
|
+
if (!item || typeof item !== 'object') {
|
|
128
|
+
return item;
|
|
129
|
+
}
|
|
130
|
+
if (SchemaRegistryIndex.has(cls)) {
|
|
131
|
+
const schema = SchemaRegistryIndex.get(cls).get();
|
|
132
|
+
const transientFields = ModelRegistryIndex.has(cls) ? ModelRegistryIndex.getConfig(cls).transientFields : undefined;
|
|
133
|
+
|
|
134
|
+
const isCleanTarget = (key: string): boolean => {
|
|
135
|
+
return transientFields?.includes(key) ?? false;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const hasTargets = Object.keys(schema.fields).some(key => isCleanTarget(key));
|
|
139
|
+
|
|
140
|
+
let res = item;
|
|
141
|
+
if (hasTargets) {
|
|
142
|
+
res = { ...item };
|
|
143
|
+
for (const key of Object.keys(schema.fields)) {
|
|
144
|
+
if (isCleanTarget(key)) {
|
|
145
|
+
res[castKey<T>(key)] = undefined!;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
151
|
+
const fieldKey = castKey<typeof res>(key);
|
|
152
|
+
if (!isCleanTarget(key) && res[fieldKey] !== undefined && res[fieldKey] !== null) {
|
|
153
|
+
if (field.array && Array.isArray(res[fieldKey])) {
|
|
154
|
+
const arr = res[fieldKey];
|
|
155
|
+
let changed = false;
|
|
156
|
+
const newArray = arr.map(subItem => {
|
|
157
|
+
const cleaned = this.cleanTransientFields(field.type, subItem);
|
|
158
|
+
if (cleaned !== subItem) {
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
return cleaned;
|
|
162
|
+
});
|
|
163
|
+
if (changed) {
|
|
164
|
+
if (res === item) {
|
|
165
|
+
res = { ...item };
|
|
166
|
+
}
|
|
167
|
+
res[fieldKey] = castTo(newArray);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const val = res[fieldKey];
|
|
171
|
+
const cleaned = this.cleanTransientFields(field.type, val);
|
|
172
|
+
if (cleaned !== val) {
|
|
173
|
+
if (res === item) {
|
|
174
|
+
res = { ...item };
|
|
175
|
+
}
|
|
176
|
+
res[fieldKey] = cleaned;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return res;
|
|
182
|
+
}
|
|
183
|
+
return item;
|
|
184
|
+
}
|
|
185
|
+
|
|
99
186
|
/**
|
|
100
187
|
* Ensure subtype is not supported
|
|
101
188
|
*/
|
|
@@ -111,16 +198,13 @@ export class ModelCrudUtil {
|
|
|
111
198
|
*/
|
|
112
199
|
static async prePersist<T>(cls: Class<T>, item: T, scope: PrePersistScope): Promise<T> {
|
|
113
200
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
114
|
-
for (const state of
|
|
201
|
+
for (const state of config.prePersist ?? []) {
|
|
115
202
|
if (state.scope === scope || scope === 'all' || state.scope === 'all') {
|
|
116
203
|
const handler: DataHandler<T> = castTo(state.handler);
|
|
117
|
-
item = await handler(item) ?? item;
|
|
204
|
+
item = (await handler(item)) ?? item;
|
|
118
205
|
}
|
|
119
206
|
}
|
|
120
|
-
|
|
121
|
-
item = await item.prePersist() ?? item;
|
|
122
|
-
}
|
|
123
|
-
return item;
|
|
207
|
+
return this.cleanTransientFields(cls, item);
|
|
124
208
|
}
|
|
125
209
|
|
|
126
210
|
/**
|
|
@@ -129,10 +213,7 @@ export class ModelCrudUtil {
|
|
|
129
213
|
static async postLoad<T>(cls: Class<T>, item: T): Promise<T> {
|
|
130
214
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
131
215
|
for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
|
|
132
|
-
item = await handler(item) ?? item;
|
|
133
|
-
}
|
|
134
|
-
if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
|
|
135
|
-
item = await item.postLoad() ?? item;
|
|
216
|
+
item = (await handler(item)) ?? item;
|
|
136
217
|
}
|
|
137
218
|
return item;
|
|
138
219
|
}
|
|
@@ -142,7 +223,9 @@ export class ModelCrudUtil {
|
|
|
142
223
|
*/
|
|
143
224
|
static async prePartialUpdate<T extends ModelType>(cls: Class<T>, item: Partial<T>, view?: string): Promise<Partial<T>> {
|
|
144
225
|
if (!DataUtil.isPlainObject(item)) {
|
|
145
|
-
throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, {
|
|
226
|
+
throw new RuntimeError(`A partial update requires a plain object, not an instance of ${castTo<Function>(item).constructor.name}`, {
|
|
227
|
+
category: 'data'
|
|
228
|
+
});
|
|
146
229
|
}
|
|
147
230
|
const keys = Object.keys(item);
|
|
148
231
|
if ((keys.length === 1 && item.id) || keys.length === 0) {
|
|
@@ -164,4 +247,4 @@ export class ModelCrudUtil {
|
|
|
164
247
|
const full = await get();
|
|
165
248
|
return cls.from(castTo({ ...full, ...prepared }));
|
|
166
249
|
}
|
|
167
|
-
}
|
|
250
|
+
}
|