@travetto/model 7.0.0-rc.1 → 7.0.0-rc.2
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 +1 -1
- package/package.json +7 -7
- package/src/registry/decorator.ts +6 -6
- package/src/registry/registry-adapter.ts +3 -3
- package/src/registry/registry-index.ts +10 -10
- package/src/types/bulk.ts +5 -5
- package/src/types/storage.ts +5 -5
- package/src/util/blob.ts +13 -13
- package/src/util/bulk.ts +16 -16
- package/src/util/crud.ts +8 -8
- package/src/util/expiry.ts +5 -5
- package/src/util/indexed.ts +34 -34
- package/src/util/storage.ts +8 -8
- package/support/base-command.ts +6 -6
- package/support/bin/candidate.ts +11 -11
- package/support/bin/install.ts +3 -3
- package/support/cli.model_export.ts +1 -1
- package/support/cli.model_install.ts +1 -1
- package/support/doc.support.tsx +4 -4
package/README.md
CHANGED
|
@@ -236,7 +236,7 @@ Finally, there is support for [Bulk](https://github.com/travetto/travetto/tree/m
|
|
|
236
236
|
**Code: Bulk Contract**
|
|
237
237
|
```typescript
|
|
238
238
|
export interface ModelBulkSupport extends ModelCrudSupport {
|
|
239
|
-
processBulk<T extends ModelType>(cls: Class<T>, operations:
|
|
239
|
+
processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse>;
|
|
240
240
|
}
|
|
241
241
|
```
|
|
242
242
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Datastore abstraction for core operations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/model"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/di": "^7.0.0-rc.
|
|
31
|
-
"@travetto/registry": "^7.0.0-rc.
|
|
32
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/di": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/registry": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/schema": "^7.0.0-rc.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
36
|
-
"@travetto/test": "^7.0.0-rc.
|
|
35
|
+
"@travetto/cli": "^7.0.0-rc.2",
|
|
36
|
+
"@travetto/test": "^7.0.0-rc.2"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
|
@@ -11,12 +11,12 @@ import { ModelRegistryIndex } from './registry-index.ts';
|
|
|
11
11
|
* @augments `@travetto/schema:Schema`
|
|
12
12
|
* @kind decorator
|
|
13
13
|
*/
|
|
14
|
-
export function Model(
|
|
14
|
+
export function Model(config: Partial<ModelConfig<ModelType>> | string = {}) {
|
|
15
15
|
return function <T extends ModelType, U extends Class<T>>(cls: U): U {
|
|
16
|
-
if (typeof
|
|
17
|
-
|
|
16
|
+
if (typeof config === 'string') {
|
|
17
|
+
config = { store: config };
|
|
18
18
|
}
|
|
19
|
-
ModelRegistryIndex.getForRegister(cls).register(
|
|
19
|
+
ModelRegistryIndex.getForRegister(cls).register(config);
|
|
20
20
|
if (SchemaRegistryIndex.getForRegister(cls).get().fields.id) {
|
|
21
21
|
SchemaRegistryIndex.getForRegister(cls).registerField('id', { required: { active: false } });
|
|
22
22
|
}
|
|
@@ -29,7 +29,7 @@ export function Model(conf: Partial<ModelConfig<ModelType>> | string = {}) {
|
|
|
29
29
|
* @kind decorator
|
|
30
30
|
*/
|
|
31
31
|
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
32
|
-
if (indices.some(
|
|
32
|
+
if (indices.some(config => config.fields.some(field => field === 'id'))) {
|
|
33
33
|
throw new AppError('Cannot create an index with the id field');
|
|
34
34
|
}
|
|
35
35
|
return function (cls: Class<T>): void {
|
|
@@ -69,7 +69,7 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
69
69
|
* @augments `@travetto/schema:Field`
|
|
70
70
|
* @kind decorator
|
|
71
71
|
*/
|
|
72
|
-
export function PersistValue<T>(handler: (
|
|
72
|
+
export function PersistValue<T>(handler: (current: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
73
73
|
return function <K extends string, C extends Partial<Record<K, T>>>(instance: C, property: K): void {
|
|
74
74
|
ModelRegistryIndex.getForRegister(getClass(instance)).register({
|
|
75
75
|
prePersist: [{
|
|
@@ -24,7 +24,7 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
register(...data: Partial<ModelConfig>[]): ModelConfig {
|
|
27
|
-
const
|
|
27
|
+
const config = this.#config ??= {
|
|
28
28
|
class: this.#cls,
|
|
29
29
|
indices: [],
|
|
30
30
|
autoCreate: true,
|
|
@@ -32,8 +32,8 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
32
32
|
postLoad: [],
|
|
33
33
|
prePersist: []
|
|
34
34
|
};
|
|
35
|
-
combineClasses(
|
|
36
|
-
return
|
|
35
|
+
combineClasses(config, data);
|
|
36
|
+
return config;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
finalize(parent?: ModelConfig): void {
|
|
@@ -88,11 +88,11 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
88
88
|
|
|
89
89
|
process(events: ChangeEvent<Class>[]): void {
|
|
90
90
|
for (const event of events) {
|
|
91
|
-
if ('
|
|
92
|
-
this.#removeClass(event.
|
|
91
|
+
if ('previous' in event) {
|
|
92
|
+
this.#removeClass(event.previous);
|
|
93
93
|
}
|
|
94
|
-
if ('
|
|
95
|
-
this.#addClass(event.
|
|
94
|
+
if ('current' in event) {
|
|
95
|
+
this.#addClass(event.current);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -112,21 +112,21 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
112
112
|
* Get Index
|
|
113
113
|
*/
|
|
114
114
|
getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
115
|
+
const config = this.getConfig(cls).indices?.find((idx): idx is IndexConfig<T> => idx.name === name);
|
|
116
|
+
if (!config) {
|
|
117
117
|
throw new NotFoundError(`${cls.name} Index`, `${name}`);
|
|
118
118
|
}
|
|
119
|
-
if (supportedTypes && !supportedTypes.includes(
|
|
120
|
-
throw new IndexNotSupported(cls,
|
|
119
|
+
if (supportedTypes && !supportedTypes.includes(config.type)) {
|
|
120
|
+
throw new IndexNotSupported(cls, config, `${config.type} indices are not supported.`);
|
|
121
121
|
}
|
|
122
|
-
return
|
|
122
|
+
return config;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Get Indices
|
|
127
127
|
*/
|
|
128
128
|
getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
|
|
129
|
-
return (this.getConfig(cls).indices ?? []).filter((
|
|
129
|
+
return (this.getConfig(cls).indices ?? []).filter((idx): idx is IndexConfig<T> => !supportedTypes || supportedTypes.includes(idx.type));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
package/src/types/bulk.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { ModelType, OptionalId } from '../types/model.ts';
|
|
|
7
7
|
/**
|
|
8
8
|
* Bulk operation. Each operation has a single action and payload
|
|
9
9
|
*/
|
|
10
|
-
export type
|
|
10
|
+
export type BulkOperation<T extends ModelType> =
|
|
11
11
|
{ delete?: T } &
|
|
12
12
|
{ insert?: OptionalId<T> } &
|
|
13
13
|
{ update?: T } &
|
|
@@ -47,9 +47,9 @@ export class BulkProcessError extends AppError<{ errors: BulkErrorItem[] }> {
|
|
|
47
47
|
super('Bulk processing errors have occurred', {
|
|
48
48
|
category: 'data',
|
|
49
49
|
details: {
|
|
50
|
-
errors: errors.map(
|
|
51
|
-
const { message, type, details: { errors: subErrors } = {} } =
|
|
52
|
-
return { message, type, errors: subErrors, idx:
|
|
50
|
+
errors: errors.map(error => {
|
|
51
|
+
const { message, type, details: { errors: subErrors } = {} } = error.error;
|
|
52
|
+
return { message, type, errors: subErrors, idx: error.idx };
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
});
|
|
@@ -62,5 +62,5 @@ export class BulkProcessError extends AppError<{ errors: BulkErrorItem[] }> {
|
|
|
62
62
|
* @concrete
|
|
63
63
|
*/
|
|
64
64
|
export interface ModelBulkSupport extends ModelCrudSupport {
|
|
65
|
-
processBulk<T extends ModelType>(cls: Class<T>, operations:
|
|
65
|
+
processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse>;
|
|
66
66
|
}
|
package/src/types/storage.ts
CHANGED
|
@@ -32,23 +32,23 @@ export interface ModelStorageSupport {
|
|
|
32
32
|
/**
|
|
33
33
|
* Installs model
|
|
34
34
|
*/
|
|
35
|
-
createModel?<T extends ModelType>(
|
|
35
|
+
createModel?<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
36
36
|
/**
|
|
37
37
|
* Installs model
|
|
38
38
|
*/
|
|
39
|
-
exportModel?<T extends ModelType>(
|
|
39
|
+
exportModel?<T extends ModelType>(cls: Class<T>): Promise<string>;
|
|
40
40
|
/**
|
|
41
41
|
* Installs model
|
|
42
42
|
*/
|
|
43
|
-
deleteModel?<T extends ModelType>(
|
|
43
|
+
deleteModel?<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
44
44
|
/**
|
|
45
45
|
* Removes all data from a model, but leaving the structure in place
|
|
46
46
|
*/
|
|
47
|
-
truncateModel?<T extends ModelType>(
|
|
47
|
+
truncateModel?<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
48
48
|
/**
|
|
49
49
|
* Deals with model internals changing
|
|
50
50
|
*/
|
|
51
|
-
changeModel?<T extends ModelType>(
|
|
51
|
+
changeModel?<T extends ModelType>(cls: Class<T>): Promise<void>;
|
|
52
52
|
/**
|
|
53
53
|
* An event listener for whenever a model schema is changed
|
|
54
54
|
*/
|
package/src/util/blob.ts
CHANGED
|
@@ -15,22 +15,22 @@ export class ModelBlobUtil {
|
|
|
15
15
|
/**
|
|
16
16
|
* Convert input to a Readable, and get what metadata is available
|
|
17
17
|
*/
|
|
18
|
-
static async getInput(
|
|
19
|
-
let
|
|
20
|
-
if (
|
|
21
|
-
metadata = { ...BinaryUtil.getBlobMeta(
|
|
22
|
-
metadata.size ??=
|
|
23
|
-
|
|
24
|
-
} else if (typeof
|
|
25
|
-
|
|
26
|
-
} else if (typeof
|
|
27
|
-
|
|
18
|
+
static async getInput(input: BinaryInput, metadata: BlobMeta = {}): Promise<[Readable, BlobMeta]> {
|
|
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
28
|
} else {
|
|
29
|
-
metadata.size =
|
|
30
|
-
|
|
29
|
+
metadata.size = input.length;
|
|
30
|
+
result = Readable.from(input);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
return [
|
|
33
|
+
return [result, metadata ?? {}];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
package/src/util/bulk.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Class, hasFunction } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { BulkOperation, ModelBulkSupport } from '../types/bulk.ts';
|
|
4
4
|
import { ModelType } from '../types/model.ts';
|
|
5
5
|
import { ModelCrudProvider, ModelCrudUtil } from './crud.ts';
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ export type BulkPreStore<T extends ModelType> = {
|
|
|
9
9
|
upsertedIds: Map<number, string>;
|
|
10
10
|
updatedIds: Map<number, string>;
|
|
11
11
|
existingUpsertedIds: Map<number, string>;
|
|
12
|
-
operations:
|
|
12
|
+
operations: BulkOperation<T>[];
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export class ModelBulkUtil {
|
|
@@ -20,12 +20,12 @@ export class ModelBulkUtil {
|
|
|
20
20
|
static isSupported = hasFunction<ModelBulkSupport>('processBulk');
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Prepares bulk
|
|
23
|
+
* Prepares bulk operations for storage
|
|
24
24
|
* @param cls
|
|
25
25
|
* @param operations
|
|
26
26
|
* @param provider
|
|
27
27
|
*/
|
|
28
|
-
static async preStore<T extends ModelType>(cls: Class<T>, operations:
|
|
28
|
+
static async preStore<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[], provider: ModelCrudProvider): Promise<BulkPreStore<T>> {
|
|
29
29
|
const insertedIds = new Map<number, string>();
|
|
30
30
|
const upsertedIds = new Map<number, string>();
|
|
31
31
|
const updatedIds = new Map<number, string>();
|
|
@@ -33,20 +33,20 @@ export class ModelBulkUtil {
|
|
|
33
33
|
|
|
34
34
|
// Pre store
|
|
35
35
|
let i = 0;
|
|
36
|
-
for (const
|
|
37
|
-
if ('insert' in
|
|
38
|
-
|
|
39
|
-
insertedIds.set(i,
|
|
40
|
-
} else if ('update' in
|
|
41
|
-
|
|
42
|
-
updatedIds.set(i,
|
|
43
|
-
} else if ('upsert' in
|
|
44
|
-
const isNew = !
|
|
45
|
-
|
|
36
|
+
for (const operation of operations) {
|
|
37
|
+
if ('insert' in operation && operation.insert) {
|
|
38
|
+
operation.insert = await ModelCrudUtil.preStore(cls, operation.insert, provider);
|
|
39
|
+
insertedIds.set(i, operation.insert.id!);
|
|
40
|
+
} else if ('update' in operation && operation.update) {
|
|
41
|
+
operation.update = await ModelCrudUtil.preStore(cls, operation.update, provider);
|
|
42
|
+
updatedIds.set(i, operation.update.id);
|
|
43
|
+
} else if ('upsert' in operation && operation.upsert) {
|
|
44
|
+
const isNew = !operation.upsert.id;
|
|
45
|
+
operation.upsert = await ModelCrudUtil.preStore(cls, operation.upsert, provider);
|
|
46
46
|
if (isNew) {
|
|
47
|
-
upsertedIds.set(i,
|
|
47
|
+
upsertedIds.set(i, operation.upsert.id!);
|
|
48
48
|
} else {
|
|
49
|
-
existingUpsertedIds.set(i,
|
|
49
|
+
existingUpsertedIds.set(i, operation.upsert.id!);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
i += 1;
|
package/src/util/crud.ts
CHANGED
|
@@ -26,9 +26,9 @@ export class ModelCrudUtil {
|
|
|
26
26
|
/**
|
|
27
27
|
* Build a uuid generator
|
|
28
28
|
*/
|
|
29
|
-
static uuidSource(
|
|
30
|
-
const create = (): string => Util.uuid(
|
|
31
|
-
const valid = (id: string): boolean => id.length ===
|
|
29
|
+
static uuidSource(length: number = 32): ModelIdSource {
|
|
30
|
+
const create = (): string => Util.uuid(length);
|
|
31
|
+
const valid = (id: string): boolean => id.length === length && /^[0-9a-f]+$/i.test(id);
|
|
32
32
|
return { create, valid };
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -82,9 +82,9 @@ export class ModelCrudUtil {
|
|
|
82
82
|
let errors: ValidationError[] = [];
|
|
83
83
|
try {
|
|
84
84
|
await SchemaValidator.validate(cls, item);
|
|
85
|
-
} catch (
|
|
86
|
-
if (
|
|
87
|
-
errors =
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof ValidationResultError) {
|
|
87
|
+
errors = error.details.errors;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -153,9 +153,9 @@ export class ModelCrudUtil {
|
|
|
153
153
|
item = { ...item };
|
|
154
154
|
delete item.id;
|
|
155
155
|
}
|
|
156
|
-
const
|
|
156
|
+
const result = await this.prePersist(cls, castTo(item), 'partial');
|
|
157
157
|
await SchemaValidator.validatePartial(cls, item, view);
|
|
158
|
-
return
|
|
158
|
+
return result;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
package/src/util/expiry.ts
CHANGED
|
@@ -29,13 +29,13 @@ export class ModelExpiryUtil {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Delete all expired on a fixed interval, if supported and needed
|
|
32
|
-
* @param
|
|
32
|
+
* @param service
|
|
33
33
|
*/
|
|
34
|
-
static registerCull(
|
|
34
|
+
static registerCull(service: ModelExpirySupport & { readonly config?: { cullRate?: number | TimeSpan } }): void {
|
|
35
35
|
const cullable = ModelRegistryIndex.getClasses().filter(cls => !!ModelRegistryIndex.getConfig(cls).expiresAt);
|
|
36
|
-
if (
|
|
36
|
+
if (service.deleteExpired && cullable.length) {
|
|
37
37
|
const running = new AbortController();
|
|
38
|
-
const cullInterval = TimeUtil.asMillis(
|
|
38
|
+
const cullInterval = TimeUtil.asMillis(service.config?.cullRate ?? '10m');
|
|
39
39
|
|
|
40
40
|
ShutdownManager.onGracefulShutdown(async () => running.abort());
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ export class ModelExpiryUtil {
|
|
|
43
43
|
await Util.nonBlockingTimeout(1000);
|
|
44
44
|
while (!running.signal.aborted) {
|
|
45
45
|
await Util.nonBlockingTimeout(cullInterval);
|
|
46
|
-
await Promise.all(cullable.map(cls =>
|
|
46
|
+
await Promise.all(cullable.map(cls => service.deleteExpired(cls)));
|
|
47
47
|
}
|
|
48
48
|
})();
|
|
49
49
|
}
|
package/src/util/indexed.ts
CHANGED
|
@@ -38,43 +38,43 @@ export class ModelIndexedUtil {
|
|
|
38
38
|
static computeIndexParts<T extends ModelType>(
|
|
39
39
|
cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
|
|
40
40
|
): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
|
|
41
|
-
const
|
|
42
|
-
const sortField =
|
|
41
|
+
const config = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
42
|
+
const sortField = config.type === 'sorted' ? config.fields.at(-1) : undefined;
|
|
43
43
|
|
|
44
44
|
const fields: IndexFieldPart[] = [];
|
|
45
|
-
let
|
|
45
|
+
let sortDirection: number = 0;
|
|
46
46
|
let sorted: IndexSortPart | undefined;
|
|
47
47
|
|
|
48
|
-
for (const field of
|
|
49
|
-
let
|
|
50
|
-
let
|
|
48
|
+
for (const field of config.fields) {
|
|
49
|
+
let fieldRef: Record<string, unknown> = field;
|
|
50
|
+
let itemRef: Record<string, unknown> = item;
|
|
51
51
|
const parts = [];
|
|
52
52
|
|
|
53
|
-
while (
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
parts.push(
|
|
57
|
-
if (typeof
|
|
58
|
-
if (
|
|
59
|
-
|
|
53
|
+
while (itemRef !== undefined && itemRef !== null) {
|
|
54
|
+
const key = TypedObject.keys(fieldRef)[0];
|
|
55
|
+
itemRef = castTo(itemRef[key]);
|
|
56
|
+
parts.push(key);
|
|
57
|
+
if (typeof fieldRef[key] === 'boolean' || typeof fieldRef[key] === 'number') {
|
|
58
|
+
if (config.type === 'sorted') {
|
|
59
|
+
sortDirection = fieldRef[key] === true ? 1 : fieldRef[key] === false ? 0 : fieldRef[key];
|
|
60
60
|
}
|
|
61
61
|
break; // At the bottom
|
|
62
62
|
} else {
|
|
63
|
-
|
|
63
|
+
fieldRef = castTo(fieldRef[key]);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
if (field === sortField) {
|
|
67
|
-
sorted = { path: parts, dir:
|
|
67
|
+
sorted = { path: parts, dir: sortDirection, value: castTo(itemRef) };
|
|
68
68
|
}
|
|
69
|
-
if (
|
|
69
|
+
if (itemRef === undefined || itemRef === null) {
|
|
70
70
|
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
71
71
|
if (empty === undefined || empty === Error) {
|
|
72
|
-
throw new IndexNotSupported(cls,
|
|
72
|
+
throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
itemRef = castTo(empty!);
|
|
75
75
|
} else {
|
|
76
76
|
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
77
|
-
fields.push({ path: parts, value: castTo(
|
|
77
|
+
fields.push({ path: parts, value: castTo(itemRef) });
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -87,18 +87,18 @@ export class ModelIndexedUtil {
|
|
|
87
87
|
* @param cls Type to get index for
|
|
88
88
|
* @param idx Index config
|
|
89
89
|
*/
|
|
90
|
-
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>,
|
|
91
|
-
const
|
|
92
|
-
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {},
|
|
93
|
-
let sub: Record<string, unknown> =
|
|
90
|
+
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, config?: ComputeConfig): Record<string, unknown> {
|
|
91
|
+
const response: Record<string, unknown> = {};
|
|
92
|
+
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, config).fields) {
|
|
93
|
+
let sub: Record<string, unknown> = response;
|
|
94
94
|
const all = path.slice(0);
|
|
95
95
|
const last = all.pop()!;
|
|
96
|
-
for (const
|
|
97
|
-
sub = castTo(sub[
|
|
96
|
+
for (const part of all) {
|
|
97
|
+
sub = castTo(sub[part] ??= {});
|
|
98
98
|
}
|
|
99
99
|
sub[last] = value;
|
|
100
100
|
}
|
|
101
|
-
return
|
|
101
|
+
return response;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -111,12 +111,12 @@ export class ModelIndexedUtil {
|
|
|
111
111
|
cls: Class<T>,
|
|
112
112
|
idx: IndexConfig<T> | string,
|
|
113
113
|
item: DeepPartial<T> = {},
|
|
114
|
-
|
|
114
|
+
config?: ComputeConfig & { separator?: string }
|
|
115
115
|
): { type: string, key: string, sort?: number | Date } {
|
|
116
|
-
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(
|
|
117
|
-
const key = fields.map(({ value }) => value).map(
|
|
118
|
-
const
|
|
119
|
-
return !sorted ? { type:
|
|
116
|
+
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(config ?? {}), includeSortInFields: false });
|
|
117
|
+
const key = fields.map(({ value }) => value).map(value => `${value}`).join(config?.separator ?? DEFAULT_SEP);
|
|
118
|
+
const indexConfig = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
119
|
+
return !sorted ? { type: indexConfig.type, key } : { type: indexConfig.type, key, sort: sorted.value };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
@@ -134,11 +134,11 @@ export class ModelIndexedUtil {
|
|
|
134
134
|
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
135
135
|
body.id = id;
|
|
136
136
|
return await service.update(cls, castTo(body));
|
|
137
|
-
} catch (
|
|
138
|
-
if (
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof NotFoundError) {
|
|
139
139
|
return await service.create(cls, body);
|
|
140
140
|
} else {
|
|
141
|
-
throw
|
|
141
|
+
throw error;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
package/src/util/storage.ts
CHANGED
|
@@ -33,11 +33,11 @@ export class ModelStorageUtil {
|
|
|
33
33
|
|
|
34
34
|
// If listening for model add/removes/updates
|
|
35
35
|
if (storage.createModel || storage.deleteModel || storage.changeModel) {
|
|
36
|
-
Registry.onClassChange(
|
|
37
|
-
switch (
|
|
38
|
-
case 'added': checkType(
|
|
39
|
-
case 'changed': checkType(
|
|
40
|
-
case 'removing': checkType(
|
|
36
|
+
Registry.onClassChange(event => {
|
|
37
|
+
switch (event.type) {
|
|
38
|
+
case 'added': checkType(event.current) ? storage.createModel?.(event.current) : undefined; break;
|
|
39
|
+
case 'changed': checkType(event.current, false) ? storage.changeModel?.(event.current) : undefined; break;
|
|
40
|
+
case 'removing': checkType(event.previous) ? storage.deleteModel?.(event.previous) : undefined; break;
|
|
41
41
|
}
|
|
42
42
|
}, ModelRegistryIndex);
|
|
43
43
|
}
|
|
@@ -55,9 +55,9 @@ export class ModelStorageUtil {
|
|
|
55
55
|
|
|
56
56
|
// If listening for model add/removes/updates
|
|
57
57
|
if (storage.changeSchema) {
|
|
58
|
-
SchemaChangeListener.onSchemaChange(
|
|
59
|
-
if (checkType(
|
|
60
|
-
storage.changeSchema!(
|
|
58
|
+
SchemaChangeListener.onSchemaChange(event => {
|
|
59
|
+
if (checkType(event.cls)) {
|
|
60
|
+
storage.changeSchema!(event.cls, event.change);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
}
|
package/support/base-command.ts
CHANGED
|
@@ -16,7 +16,7 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
16
16
|
/** Application Environment */
|
|
17
17
|
env?: string;
|
|
18
18
|
|
|
19
|
-
abstract
|
|
19
|
+
abstract getOperation(): keyof ModelStorageSupport;
|
|
20
20
|
|
|
21
21
|
preMain(): void {
|
|
22
22
|
Env.DEBUG.set(false);
|
|
@@ -25,26 +25,26 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
25
25
|
async help(): Promise<string[]> {
|
|
26
26
|
await Registry.init();
|
|
27
27
|
|
|
28
|
-
const candidates = await ModelCandidateUtil.export(this.
|
|
28
|
+
const candidates = await ModelCandidateUtil.export(this.getOperation());
|
|
29
29
|
return [
|
|
30
30
|
cliTpl`${{ title: 'Providers' }}`,
|
|
31
31
|
'-'.repeat(20),
|
|
32
|
-
...candidates.providers.map(
|
|
32
|
+
...candidates.providers.map(type => cliTpl` * ${{ type }}`),
|
|
33
33
|
'',
|
|
34
34
|
cliTpl`${{ title: 'Models' }}`,
|
|
35
35
|
'-'.repeat(20),
|
|
36
|
-
...candidates.models.map(
|
|
36
|
+
...candidates.models.map(param => cliTpl` * ${{ param }}`)
|
|
37
37
|
];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async validate(provider: string, models: string[]): Promise<CliValidationError | undefined> {
|
|
41
41
|
await Registry.init();
|
|
42
42
|
|
|
43
|
-
const candidates = await ModelCandidateUtil.export(this.
|
|
43
|
+
const candidates = await ModelCandidateUtil.export(this.getOperation());
|
|
44
44
|
if (provider && !candidates.providers.includes(provider)) {
|
|
45
45
|
return { message: `provider: ${provider} is not a valid provider`, source: 'arg' };
|
|
46
46
|
}
|
|
47
|
-
const badModel = models.find(
|
|
47
|
+
const badModel = models.find(model => model !== '*' && !candidates.models.includes(model));
|
|
48
48
|
if (badModel) {
|
|
49
49
|
return { message: `model: ${badModel} is not a valid model`, source: 'arg' };
|
|
50
50
|
}
|
package/support/bin/candidate.ts
CHANGED
|
@@ -11,10 +11,10 @@ import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
export class ModelCandidateUtil {
|
|
13
13
|
|
|
14
|
-
static async export(
|
|
14
|
+
static async export(operation: keyof ModelStorageSupport): Promise<{ models: string[], providers: string[] }> {
|
|
15
15
|
return {
|
|
16
16
|
models: await this.getModelNames(),
|
|
17
|
-
providers: await this.getProviderNames(
|
|
17
|
+
providers: await this.getProviderNames(operation)
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -25,30 +25,30 @@ export class ModelCandidateUtil {
|
|
|
25
25
|
const names = new Set(models ?? []);
|
|
26
26
|
const all = names.has('*');
|
|
27
27
|
return ModelRegistryIndex.getClasses()
|
|
28
|
-
.map(
|
|
29
|
-
.filter(
|
|
28
|
+
.map(cls => SchemaRegistryIndex.getBaseClass(cls))
|
|
29
|
+
.filter(cls => !models || all || names.has(ModelRegistryIndex.getStoreName(cls)));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Get model names
|
|
34
34
|
*/
|
|
35
35
|
static async getModelNames(): Promise<string[]> {
|
|
36
|
-
return (await this.#getModels()).map(
|
|
36
|
+
return (await this.#getModels()).map(cls => ModelRegistryIndex.getStoreName(cls)).toSorted();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Get all providers that are viable candidates
|
|
41
41
|
*/
|
|
42
|
-
static async getProviders(
|
|
43
|
-
const
|
|
44
|
-
return
|
|
42
|
+
static async getProviders(operation?: keyof ModelStorageSupport): Promise<InjectableCandidate[]> {
|
|
43
|
+
const candidates = DependencyRegistryIndex.getCandidates(toConcrete<ModelStorageSupport>());
|
|
44
|
+
return candidates.filter(type => !operation || type.class.prototype?.[operation]);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Get list of names of all viable providers
|
|
49
49
|
*/
|
|
50
|
-
static async getProviderNames(
|
|
51
|
-
return (await this.getProviders(
|
|
50
|
+
static async getProviderNames(operation?: keyof ModelStorageSupport): Promise<string[]> {
|
|
51
|
+
return (await this.getProviders(operation))
|
|
52
52
|
.map(x => x.class.name.replace(/ModelService/, ''))
|
|
53
53
|
.toSorted();
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@ export class ModelCandidateUtil {
|
|
|
57
57
|
* Get a single provider
|
|
58
58
|
*/
|
|
59
59
|
static async getProvider(provider: string): Promise<ModelStorageSupport> {
|
|
60
|
-
const config = (await this.getProviders()).find(
|
|
60
|
+
const config = (await this.getProviders()).find(candidates => candidates.class.name === `${provider}ModelService`)!;
|
|
61
61
|
return DependencyRegistryIndex.getInstance<ModelStorageSupport>(config.candidateType, config.qualifier);
|
|
62
62
|
}
|
|
63
63
|
|
package/support/bin/install.ts
CHANGED
|
@@ -6,9 +6,9 @@ export class ModelInstallUtil {
|
|
|
6
6
|
if (!provider.createModel) {
|
|
7
7
|
throw new Error(`${provider} does not support model installation`);
|
|
8
8
|
}
|
|
9
|
-
for (const
|
|
10
|
-
console.log('Installing', { name:
|
|
11
|
-
await provider.createModel(
|
|
9
|
+
for (const cls of models) {
|
|
10
|
+
console.log('Installing', { name: cls.Ⲑid });
|
|
11
|
+
await provider.createModel(cls);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -10,7 +10,7 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
10
10
|
@CliCommand({ with: { env: true, module: true } })
|
|
11
11
|
export class ModelExportCommand extends BaseModelCommand {
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
getOperation(): 'exportModel' { return 'exportModel'; }
|
|
14
14
|
|
|
15
15
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
16
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
|
@@ -10,7 +10,7 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
10
10
|
@CliCommand({ with: { env: true, module: true } })
|
|
11
11
|
export class ModelInstallCommand extends BaseModelCommand {
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
getOperation(): 'createModel' { return 'createModel'; }
|
|
14
14
|
|
|
15
15
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
16
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
package/support/doc.support.tsx
CHANGED
|
@@ -34,18 +34,18 @@ export const ModelTypes = (fn: | Function): DocJSXElement[] => {
|
|
|
34
34
|
found.push(link);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
return found.map(
|
|
37
|
+
return found.map(type => <li>{type}</li>);
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export const ModelCustomConfig = ({
|
|
40
|
+
export const ModelCustomConfig = ({ config }: { config: Function }): DocJSXElement => <>
|
|
41
41
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source
|
|
42
42
|
or config, you can override and register it with the {d.mod('Di')} module.
|
|
43
43
|
|
|
44
44
|
<c.Code title='Wiring up a custom Model Source' src='doc/custom-service.ts' />
|
|
45
45
|
|
|
46
|
-
where the {
|
|
46
|
+
where the {config} is defined by:
|
|
47
47
|
|
|
48
|
-
<c.Code title={`Structure of ${
|
|
48
|
+
<c.Code title={`Structure of ${config.name}`} src={config} startRe={/@Config/} />
|
|
49
49
|
|
|
50
50
|
Additionally, you can see that the class is registered with the {Config} annotation, and so these values can be overridden using the
|
|
51
51
|
standard {d.mod('Config')}resolution paths.
|