@travetto/model 6.0.1 → 7.0.0-rc.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 +6 -6
- package/__index__.ts +2 -1
- package/package.json +7 -7
- package/src/registry/decorator.ts +36 -23
- package/src/registry/registry-adapter.ts +56 -0
- package/src/registry/registry-index.ts +143 -0
- package/src/registry/types.ts +11 -28
- package/src/util/crud.ts +9 -11
- package/src/util/expiry.ts +3 -3
- package/src/util/indexed.ts +3 -3
- package/src/util/storage.ts +12 -12
- package/support/base-command.ts +6 -4
- package/support/bin/candidate.ts +10 -9
- package/support/test/base.ts +2 -2
- package/support/test/polymorphism.ts +7 -6
- package/support/test/suite.ts +61 -64
- package/src/registry/model.ts +0 -218
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ yarn add @travetto/model
|
|
|
16
16
|
This module provides a set of contracts/interfaces to data model persistence, modification and retrieval. This module builds heavily upon the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding."), which is used for data model validation.
|
|
17
17
|
|
|
18
18
|
## A Simple Model
|
|
19
|
-
A model can be simply defined by usage of the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
19
|
+
A model can be simply defined by usage of the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14) decorator, which opts it into the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") contracts, as well as making it available to the [ModelRegistryIndex](https://github.com/travetto/travetto/tree/main/module/model/src/registry/registry-index.ts#L16).
|
|
20
20
|
|
|
21
21
|
**Code: Basic Structure**
|
|
22
22
|
```typescript
|
|
@@ -241,7 +241,7 @@ export interface ModelBulkSupport extends ModelCrudSupport {
|
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
## Declaration
|
|
244
|
-
Models are declared via the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
244
|
+
Models are declared via the [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14) decorator, which allows the system to know that this is a class that is compatible with the module. The only requirement for a model is the [ModelType](https://github.com/travetto/travetto/tree/main/module/model/src/types/model.ts#L10)
|
|
245
245
|
|
|
246
246
|
**Code: ModelType**
|
|
247
247
|
```typescript
|
|
@@ -277,7 +277,7 @@ To enforce that these contracts are honored, the module provides shared test sui
|
|
|
277
277
|
|
|
278
278
|
**Code: Memory Service Test Configuration**
|
|
279
279
|
```typescript
|
|
280
|
-
import {
|
|
280
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
281
281
|
import { AppError, castTo, Class, classConstruct } from '@travetto/runtime';
|
|
282
282
|
|
|
283
283
|
import { ModelBulkUtil } from '../../src/util/bulk.ts';
|
|
@@ -328,7 +328,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
get service(): Promise<T> {
|
|
331
|
-
return
|
|
331
|
+
return DependencyRegistryIndex.getInstance(this.serviceClass);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
@@ -342,7 +342,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
342
342
|
```
|
|
343
343
|
|
|
344
344
|
## CLI - model:export
|
|
345
|
-
The module provides the ability to generate an export of the model structure from all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
345
|
+
The module provides the ability to generate an export of the model structure from all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14)s within the application. This is useful for being able to generate the appropriate files to manually create the data schemas in production.
|
|
346
346
|
|
|
347
347
|
**Terminal: Running model export**
|
|
348
348
|
```bash
|
|
@@ -365,7 +365,7 @@ Models
|
|
|
365
365
|
```
|
|
366
366
|
|
|
367
367
|
## CLI - model:install
|
|
368
|
-
The module provides the ability to install all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#
|
|
368
|
+
The module provides the ability to install all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14)s within the application given the current configuration being targeted. This is useful for being able to prepare the datastore manually.
|
|
369
369
|
|
|
370
370
|
**Terminal: Running model install**
|
|
371
371
|
```bash
|
package/__index__.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './src/registry/decorator.ts';
|
|
2
|
-
export * from './src/registry/
|
|
2
|
+
export * from './src/registry/registry-index.ts';
|
|
3
|
+
export * from './src/registry/registry-adapter.ts';
|
|
3
4
|
export * from './src/registry/types.ts';
|
|
4
5
|
export * from './src/types/model.ts';
|
|
5
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-rc.0",
|
|
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": "^
|
|
30
|
-
"@travetto/di": "^
|
|
31
|
-
"@travetto/registry": "^
|
|
32
|
-
"@travetto/schema": "^
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.0",
|
|
30
|
+
"@travetto/di": "^7.0.0-rc.0",
|
|
31
|
+
"@travetto/registry": "^7.0.0-rc.0",
|
|
32
|
+
"@travetto/schema": "^7.0.0-rc.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^
|
|
36
|
-
"@travetto/test": "^
|
|
35
|
+
"@travetto/cli": "^7.0.0-rc.0",
|
|
36
|
+
"@travetto/test": "^7.0.0-rc.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
|
@@ -1,54 +1,61 @@
|
|
|
1
|
-
import { AppError,
|
|
2
|
-
import {
|
|
1
|
+
import { AppError, castTo, Class, getClass } from '@travetto/runtime';
|
|
2
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { ModelType } from '../types/model.ts';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { DataHandler, IndexConfig, ModelConfig, PrePersistScope } from './types.ts';
|
|
6
|
+
import { ModelRegistryIndex } from './registry-index.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Model decorator, extends `@Schema`
|
|
10
10
|
*
|
|
11
11
|
* @augments `@travetto/schema:Schema`
|
|
12
|
+
* @kind decorator
|
|
12
13
|
*/
|
|
13
|
-
export function Model(conf: Partial<
|
|
14
|
-
return function <T extends ModelType, U extends Class<T>>(
|
|
14
|
+
export function Model(conf: Partial<ModelConfig<ModelType>> | string = {}) {
|
|
15
|
+
return function <T extends ModelType, U extends Class<T>>(cls: U): U {
|
|
15
16
|
if (typeof conf === 'string') {
|
|
16
17
|
conf = { store: conf };
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
ModelRegistryIndex.getForRegister(cls).register(conf);
|
|
20
|
+
if (SchemaRegistryIndex.getForRegister(cls).get().fields.id) {
|
|
21
|
+
SchemaRegistryIndex.getForRegister(cls).registerField('id', { required: { active: false } });
|
|
22
|
+
}
|
|
23
|
+
return cls;
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Defines an index on a model
|
|
29
|
+
* @kind decorator
|
|
26
30
|
*/
|
|
27
31
|
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
28
32
|
if (indices.some(x => x.fields.some(f => f === 'id'))) {
|
|
29
33
|
throw new AppError('Cannot create an index with the id field');
|
|
30
34
|
}
|
|
31
|
-
return function (
|
|
32
|
-
|
|
35
|
+
return function (cls: Class<T>): void {
|
|
36
|
+
ModelRegistryIndex.getForRegister(cls).register({ indices });
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Model field decorator for denoting expiry date/time
|
|
38
42
|
* @augments `@travetto/schema:Field`
|
|
43
|
+
* @kind decorator
|
|
39
44
|
*/
|
|
40
45
|
export function ExpiresAt() {
|
|
41
|
-
return <K extends string, T extends Partial<Record<K, Date>>>(
|
|
42
|
-
|
|
46
|
+
return <K extends string, T extends Partial<Record<K, Date>>>(instance: T, property: K): void => {
|
|
47
|
+
ModelRegistryIndex.getForRegister(getClass(instance)).register({ expiresAt: property });
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
52
|
* Model class decorator for pre-persist behavior
|
|
53
|
+
* @augments `@travetto/schema:Schema`
|
|
54
|
+
* @kind decorator
|
|
48
55
|
*/
|
|
49
56
|
export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope = 'all') {
|
|
50
|
-
return function (
|
|
51
|
-
|
|
57
|
+
return function (cls: Class<T>): void {
|
|
58
|
+
ModelRegistryIndex.getForRegister(cls).register({
|
|
52
59
|
prePersist: [{
|
|
53
60
|
scope,
|
|
54
61
|
handler: castTo(handler)
|
|
@@ -59,15 +66,17 @@ export function PrePersist<T>(handler: DataHandler<T>, scope: PrePersistScope =
|
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* Model field decorator for pre-persist value setting
|
|
69
|
+
* @augments `@travetto/schema:Field`
|
|
70
|
+
* @kind decorator
|
|
62
71
|
*/
|
|
63
72
|
export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PrePersistScope = 'all') {
|
|
64
|
-
return function <K extends string, C extends Partial<Record<K, T>>>(
|
|
65
|
-
|
|
73
|
+
return function <K extends string, C extends Partial<Record<K, T>>>(instance: C, property: K): void {
|
|
74
|
+
ModelRegistryIndex.getForRegister(getClass(instance)).register({
|
|
66
75
|
prePersist: [{
|
|
67
76
|
scope,
|
|
68
77
|
handler: (inst): void => {
|
|
69
78
|
const cInst: Record<K, T> = castTo(inst);
|
|
70
|
-
cInst[
|
|
79
|
+
cInst[property] = handler(cInst[property]);
|
|
71
80
|
}
|
|
72
81
|
}]
|
|
73
82
|
});
|
|
@@ -76,15 +85,17 @@ export function PersistValue<T>(handler: (curr: T | undefined) => T, scope: PreP
|
|
|
76
85
|
|
|
77
86
|
/**
|
|
78
87
|
* Prevent a field from being persisted
|
|
88
|
+
* @augments `@travetto/schema:Field`
|
|
89
|
+
* @kind decorator
|
|
79
90
|
*/
|
|
80
91
|
export function Transient<T>() {
|
|
81
|
-
return function <K extends string, C extends Partial<Record<K, T>>>(
|
|
82
|
-
|
|
92
|
+
return function <K extends string, C extends Partial<Record<K, T>>>(instance: C, property: K): void {
|
|
93
|
+
ModelRegistryIndex.getForRegister(getClass(instance)).register({
|
|
83
94
|
prePersist: [{
|
|
84
95
|
scope: 'all',
|
|
85
96
|
handler: (inst): void => {
|
|
86
97
|
const cInst: Record<K, T> = castTo(inst);
|
|
87
|
-
delete cInst[
|
|
98
|
+
delete cInst[property];
|
|
88
99
|
}
|
|
89
100
|
}]
|
|
90
101
|
});
|
|
@@ -93,9 +104,11 @@ export function Transient<T>() {
|
|
|
93
104
|
|
|
94
105
|
/**
|
|
95
106
|
* Model class decorator for post-load behavior
|
|
107
|
+
* @augments `@travetto/schema:Schema`
|
|
108
|
+
* @kind decorator
|
|
96
109
|
*/
|
|
97
110
|
export function PostLoad<T>(handler: DataHandler<T>) {
|
|
98
|
-
return function (
|
|
99
|
-
|
|
111
|
+
return function (cls: Class<T>): void {
|
|
112
|
+
ModelRegistryIndex.getForRegister(cls).register({ postLoad: [castTo(handler)] });
|
|
100
113
|
};
|
|
101
114
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RegistryAdapter } from '@travetto/registry';
|
|
2
|
+
import { Class } from '@travetto/runtime';
|
|
3
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { ModelConfig } from './types';
|
|
6
|
+
|
|
7
|
+
function combineClasses(target: ModelConfig, sources: Partial<ModelConfig>[]): ModelConfig {
|
|
8
|
+
for (const source of sources) {
|
|
9
|
+
Object.assign(target, source, {
|
|
10
|
+
indices: [...(target.indices || []), ...(source.indices || [])],
|
|
11
|
+
postLoad: [...(target.postLoad || []), ...(source.postLoad || [])],
|
|
12
|
+
prePersist: [...(target.prePersist || []), ...(source.prePersist || [])],
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
19
|
+
#cls: Class;
|
|
20
|
+
#config: ModelConfig;
|
|
21
|
+
|
|
22
|
+
constructor(cls: Class) {
|
|
23
|
+
this.#cls = cls;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
register(...data: Partial<ModelConfig>[]): ModelConfig {
|
|
27
|
+
const cfg = this.#config ??= {
|
|
28
|
+
class: this.#cls,
|
|
29
|
+
indices: [],
|
|
30
|
+
autoCreate: true,
|
|
31
|
+
store: this.#cls.name.toLowerCase(),
|
|
32
|
+
postLoad: [],
|
|
33
|
+
prePersist: []
|
|
34
|
+
};
|
|
35
|
+
combineClasses(cfg, data);
|
|
36
|
+
return cfg;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
finalize(parent?: ModelConfig): void {
|
|
40
|
+
const config = this.#config;
|
|
41
|
+
if (parent) {
|
|
42
|
+
const parentSchema = parent ? SchemaRegistryIndex.getConfig(parent.class) : undefined; // Ensure schema is finalized first
|
|
43
|
+
|
|
44
|
+
if (parentSchema?.discriminatedField && parent.store) {
|
|
45
|
+
config.store = parent.store;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
config.postLoad = [...parent.postLoad ?? [], ...config.postLoad ?? []];
|
|
49
|
+
config.prePersist = [...parent.prePersist ?? [], ...config.prePersist ?? []];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(): ModelConfig {
|
|
54
|
+
return this.#config;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ChangeEvent, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
+
import { AppError, castTo, Class } from '@travetto/runtime';
|
|
3
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { IndexConfig, IndexType, ModelConfig } from './types';
|
|
6
|
+
import { ModelType } from '../types/model';
|
|
7
|
+
import { ModelRegistryAdapter } from './registry-adapter';
|
|
8
|
+
import { IndexNotSupported } from '../error/invalid-index';
|
|
9
|
+
import { NotFoundError } from '../error/not-found';
|
|
10
|
+
|
|
11
|
+
type IndexResult<T extends ModelType, K extends IndexType[]> = IndexConfig<T> & { type: K[number] };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Model registry index for managing model configurations across classes
|
|
15
|
+
*/
|
|
16
|
+
export class ModelRegistryIndex implements RegistryIndex {
|
|
17
|
+
|
|
18
|
+
static #instance = Registry.registerIndex(this);
|
|
19
|
+
|
|
20
|
+
static getForRegister(cls: Class): ModelRegistryAdapter {
|
|
21
|
+
return this.#instance.store.getForRegister(cls);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getConfig(cls: Class): ModelConfig {
|
|
25
|
+
return this.#instance.getConfig(cls);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static has(cls: Class): boolean {
|
|
29
|
+
return this.#instance.store.has(cls);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static getStoreName<T extends ModelType>(cls: Class<T>): string {
|
|
33
|
+
return this.#instance.getStoreName(cls);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
|
|
37
|
+
return this.#instance.getIndices(cls, supportedTypes);
|
|
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);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
|
|
45
|
+
return this.#instance.getExpiryFieldName(cls);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static getClasses(): Class[] {
|
|
49
|
+
return this.#instance.store.getClasses();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default mapping of classes by class name or
|
|
54
|
+
* by requested store name. This is the state at the
|
|
55
|
+
* start of the application.
|
|
56
|
+
*/
|
|
57
|
+
#modelNameMapping = new Map<string, Set<string>>();
|
|
58
|
+
|
|
59
|
+
store = new RegistryIndexStore(ModelRegistryAdapter);
|
|
60
|
+
|
|
61
|
+
#addClass(cls: Class): void {
|
|
62
|
+
const schema = SchemaRegistryIndex.getConfig(cls);
|
|
63
|
+
|
|
64
|
+
// Don't index on discriminated schemas
|
|
65
|
+
if (schema.discriminatedType && !schema.discriminatedBase) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { store } = this.getConfig(cls);
|
|
70
|
+
let classes = this.#modelNameMapping.get(store);
|
|
71
|
+
if (!classes) {
|
|
72
|
+
this.#modelNameMapping.set(store, classes = new Set());
|
|
73
|
+
}
|
|
74
|
+
classes.add(cls.Ⲑid);
|
|
75
|
+
|
|
76
|
+
// Don't allow two models with same class name, or same store name
|
|
77
|
+
if (classes.size > 1) {
|
|
78
|
+
throw new AppError('Duplicate models with same store name', {
|
|
79
|
+
details: { classes: [...classes].toSorted() }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#removeClass(cls: Class): void {
|
|
85
|
+
const { store } = this.store.get(cls).get();
|
|
86
|
+
this.#modelNameMapping.get(store)?.delete(cls.Ⲑid);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process(events: ChangeEvent<Class>[]): void {
|
|
90
|
+
for (const event of events) {
|
|
91
|
+
if ('prev' in event) {
|
|
92
|
+
this.#removeClass(event.prev);
|
|
93
|
+
}
|
|
94
|
+
if ('curr' in event) {
|
|
95
|
+
this.#addClass(event.curr);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getConfig(cls: Class): ModelConfig<ModelType> {
|
|
101
|
+
return this.store.get(cls).get();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the apparent store for a type, handling polymorphism when appropriate
|
|
106
|
+
*/
|
|
107
|
+
getStoreName(cls: Class): string {
|
|
108
|
+
return this.store.get(cls).get().store;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get Index
|
|
113
|
+
*/
|
|
114
|
+
getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
|
|
115
|
+
const cfg = this.getConfig(cls).indices?.find((x): x is IndexConfig<T> => x.name === name);
|
|
116
|
+
if (!cfg) {
|
|
117
|
+
throw new NotFoundError(`${cls.name} Index`, `${name}`);
|
|
118
|
+
}
|
|
119
|
+
if (supportedTypes && !supportedTypes.includes(cfg.type)) {
|
|
120
|
+
throw new IndexNotSupported(cls, cfg, `${cfg.type} indices are not supported.`);
|
|
121
|
+
}
|
|
122
|
+
return cfg;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get Indices
|
|
127
|
+
*/
|
|
128
|
+
getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
|
|
129
|
+
return (this.getConfig(cls).indices ?? []).filter((x): x is IndexConfig<T> => !supportedTypes || supportedTypes.includes(x.type));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get expiry field
|
|
134
|
+
* @param cls
|
|
135
|
+
*/
|
|
136
|
+
getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
|
|
137
|
+
const expiry = this.getConfig(cls).expiresAt;
|
|
138
|
+
if (!expiry) {
|
|
139
|
+
throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`);
|
|
140
|
+
}
|
|
141
|
+
return castTo(expiry);
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/registry/types.ts
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
import type { Class,
|
|
1
|
+
import type { Class, RetainPrimitiveFields } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { ModelType } from '../types/model.ts';
|
|
4
4
|
|
|
5
|
-
type ValidFieldNames<T> = {
|
|
6
|
-
[K in keyof T]:
|
|
7
|
-
(T[K] extends (Primitive | undefined) ? K :
|
|
8
|
-
(T[K] extends (Function | undefined) ? never :
|
|
9
|
-
K))
|
|
10
|
-
}[keyof T];
|
|
11
|
-
|
|
12
|
-
type RetainFields<T> = Pick<T, ValidFieldNames<T>>;
|
|
13
|
-
|
|
14
5
|
export type SortClauseRaw<T> = {
|
|
15
6
|
[P in keyof T]?:
|
|
16
|
-
T[P] extends object ? SortClauseRaw<
|
|
7
|
+
T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
|
|
17
8
|
};
|
|
18
9
|
|
|
19
10
|
type IndexClauseRaw<T> = {
|
|
20
11
|
[P in keyof T]?:
|
|
21
|
-
T[P] extends object ? IndexClauseRaw<
|
|
12
|
+
T[P] extends object ? IndexClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1 | true;
|
|
22
13
|
};
|
|
23
14
|
|
|
24
15
|
export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
|
|
@@ -26,9 +17,9 @@ export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | voi
|
|
|
26
17
|
export type PrePersistScope = 'full' | 'partial' | 'all';
|
|
27
18
|
|
|
28
19
|
/**
|
|
29
|
-
* Model
|
|
20
|
+
* Model config
|
|
30
21
|
*/
|
|
31
|
-
export class
|
|
22
|
+
export class ModelConfig<T extends ModelType = ModelType> {
|
|
32
23
|
/**
|
|
33
24
|
* Class for model
|
|
34
25
|
*/
|
|
@@ -36,15 +27,7 @@ export class ModelOptions<T extends ModelType = ModelType> {
|
|
|
36
27
|
/**
|
|
37
28
|
* Store name
|
|
38
29
|
*/
|
|
39
|
-
store
|
|
40
|
-
/**
|
|
41
|
-
* If a sub type
|
|
42
|
-
*/
|
|
43
|
-
subType?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Is a base type?
|
|
46
|
-
*/
|
|
47
|
-
baseType?: boolean;
|
|
30
|
+
store: string;
|
|
48
31
|
/**
|
|
49
32
|
* Indices
|
|
50
33
|
*/
|
|
@@ -54,13 +37,13 @@ export class ModelOptions<T extends ModelType = ModelType> {
|
|
|
54
37
|
*/
|
|
55
38
|
extra?: object;
|
|
56
39
|
/**
|
|
57
|
-
*
|
|
40
|
+
* Expiry field
|
|
58
41
|
*/
|
|
59
|
-
expiresAt
|
|
42
|
+
expiresAt?: string;
|
|
60
43
|
/**
|
|
61
44
|
* Auto create in development mode
|
|
62
45
|
*/
|
|
63
|
-
autoCreate
|
|
46
|
+
autoCreate?: boolean;
|
|
64
47
|
/**
|
|
65
48
|
* Pre-persist handlers
|
|
66
49
|
*/
|
|
@@ -87,11 +70,11 @@ export type IndexConfig<T extends ModelType> = {
|
|
|
87
70
|
/**
|
|
88
71
|
* Fields and sort order
|
|
89
72
|
*/
|
|
90
|
-
fields: IndexClauseRaw<
|
|
73
|
+
fields: IndexClauseRaw<RetainPrimitiveFields<T>>[];
|
|
91
74
|
/**
|
|
92
75
|
* Type
|
|
93
76
|
*/
|
|
94
77
|
type: IndexType;
|
|
95
78
|
};
|
|
96
79
|
|
|
97
|
-
export type IndexField<T extends ModelType> = IndexClauseRaw<
|
|
80
|
+
export type IndexField<T extends ModelType> = IndexClauseRaw<RetainPrimitiveFields<T>>;
|
package/src/util/crud.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { castTo, Class, Util,
|
|
2
|
-
import { DataUtil,
|
|
1
|
+
import { castTo, Class, Util, AppError, hasFunction } from '@travetto/runtime';
|
|
2
|
+
import { DataUtil, SchemaRegistryIndex, SchemaValidator, ValidationError, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
5
5
|
import { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
|
|
6
6
|
import { NotFoundError } from '../error/not-found.ts';
|
|
7
7
|
import { ExistsError } from '../error/exists.ts';
|
|
@@ -47,7 +47,7 @@ export class ModelCrudUtil {
|
|
|
47
47
|
resolvedInput = input;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const result =
|
|
50
|
+
const result = SchemaRegistryIndex.getBaseClass(cls).from(resolvedInput);
|
|
51
51
|
|
|
52
52
|
if (!(result instanceof cls || result.constructor.Ⲑid === cls.Ⲑid)) {
|
|
53
53
|
if (onTypeMismatch === 'notfound') {
|
|
@@ -75,10 +75,7 @@ export class ModelCrudUtil {
|
|
|
75
75
|
item = cls.from(castTo(item));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
if (config.subType) { // Sub-typing, assign type
|
|
80
|
-
SchemaRegistry.ensureInstanceTypeField(cls, item);
|
|
81
|
-
}
|
|
78
|
+
SchemaRegistryIndex.get(cls).ensureInstanceTypeField(item);
|
|
82
79
|
|
|
83
80
|
item = await this.prePersist(cls, item, scope);
|
|
84
81
|
|
|
@@ -105,7 +102,8 @@ export class ModelCrudUtil {
|
|
|
105
102
|
* Ensure subtype is not supported
|
|
106
103
|
*/
|
|
107
104
|
static ensureNotSubType(cls: Class): void {
|
|
108
|
-
|
|
105
|
+
const config = SchemaRegistryIndex.getConfig(cls);
|
|
106
|
+
if (config.discriminatedType && !config.discriminatedBase) {
|
|
109
107
|
throw new SubTypeNotSupportedError(cls);
|
|
110
108
|
}
|
|
111
109
|
}
|
|
@@ -114,7 +112,7 @@ export class ModelCrudUtil {
|
|
|
114
112
|
* Pre persist behavior
|
|
115
113
|
*/
|
|
116
114
|
static async prePersist<T>(cls: Class<T>, item: T, scope: PrePersistScope): Promise<T> {
|
|
117
|
-
const config =
|
|
115
|
+
const config = ModelRegistryIndex.getConfig(cls);
|
|
118
116
|
for (const state of (config.prePersist ?? [])) {
|
|
119
117
|
if (state.scope === scope || scope === 'all' || state.scope === 'all') {
|
|
120
118
|
const handler: DataHandler<T> = castTo(state.handler);
|
|
@@ -131,7 +129,7 @@ export class ModelCrudUtil {
|
|
|
131
129
|
* Post load behavior
|
|
132
130
|
*/
|
|
133
131
|
static async postLoad<T>(cls: Class<T>, item: T): Promise<T> {
|
|
134
|
-
const config =
|
|
132
|
+
const config = ModelRegistryIndex.getConfig(cls);
|
|
135
133
|
for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
|
|
136
134
|
item = await handler(item) ?? item;
|
|
137
135
|
}
|
package/src/util/expiry.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ShutdownManager, Class, TimeSpan, TimeUtil, Util, castTo, hasFunction } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { ModelRegistry } from '../registry/model.ts';
|
|
4
3
|
import { ModelExpirySupport } from '../types/expiry.ts';
|
|
5
4
|
import { ModelType } from '../types/model.ts';
|
|
5
|
+
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Utils for model expiry
|
|
@@ -18,7 +18,7 @@ export class ModelExpiryUtil {
|
|
|
18
18
|
* Get expiry info for a given item
|
|
19
19
|
*/
|
|
20
20
|
static getExpiryState<T extends ModelType>(cls: Class<T>, item: T): { expiresAt?: Date, expired?: boolean } {
|
|
21
|
-
const expKey =
|
|
21
|
+
const expKey = ModelRegistryIndex.getExpiryFieldName(cls);
|
|
22
22
|
const expiresAt: Date = castTo(item[expKey]);
|
|
23
23
|
|
|
24
24
|
return {
|
|
@@ -32,7 +32,7 @@ export class ModelExpiryUtil {
|
|
|
32
32
|
* @param svc
|
|
33
33
|
*/
|
|
34
34
|
static registerCull(svc: ModelExpirySupport & { readonly config?: { cullRate?: number | TimeSpan } }): void {
|
|
35
|
-
const cullable =
|
|
35
|
+
const cullable = ModelRegistryIndex.getClasses().filter(cls => !!ModelRegistryIndex.getConfig(cls).expiresAt);
|
|
36
36
|
if (svc.deleteExpired && cullable.length) {
|
|
37
37
|
const running = new AbortController();
|
|
38
38
|
const cullInterval = TimeUtil.asMillis(svc.config?.cullRate ?? '10m');
|
package/src/util/indexed.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { castTo, Class, DeepPartial, hasFunction, TypedObject } from '@travetto/
|
|
|
2
2
|
|
|
3
3
|
import { IndexNotSupported } from '../error/invalid-index.ts';
|
|
4
4
|
import { NotFoundError } from '../error/not-found.ts';
|
|
5
|
-
import { ModelRegistry } from '../registry/model.ts';
|
|
6
5
|
import type { IndexConfig } from '../registry/types.ts';
|
|
7
6
|
import type { ModelCrudSupport } from '../types/crud.ts';
|
|
8
7
|
import type { ModelIndexedSupport } from '../types/indexed.ts';
|
|
9
8
|
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
9
|
+
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
10
10
|
|
|
11
11
|
type ComputeConfig = {
|
|
12
12
|
includeSortInFields?: boolean;
|
|
@@ -38,7 +38,7 @@ 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 cfg = typeof idx === 'string' ?
|
|
41
|
+
const cfg = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
42
42
|
const sortField = cfg.type === 'sorted' ? cfg.fields.at(-1) : undefined;
|
|
43
43
|
|
|
44
44
|
const fields: IndexFieldPart[] = [];
|
|
@@ -115,7 +115,7 @@ export class ModelIndexedUtil {
|
|
|
115
115
|
): { type: string, key: string, sort?: number | Date } {
|
|
116
116
|
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(opts ?? {}), includeSortInFields: false });
|
|
117
117
|
const key = fields.map(({ value }) => value).map(x => `${x}`).join(opts?.sep ?? DEFAULT_SEP);
|
|
118
|
-
const cfg = typeof idx === 'string' ?
|
|
118
|
+
const cfg = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
119
119
|
return !sorted ? { type: cfg.type, key } : { type: cfg.type, key, sort: sorted.value };
|
|
120
120
|
}
|
|
121
121
|
|
package/src/util/storage.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Class, hasFunction, Runtime } from '@travetto/runtime';
|
|
2
|
-
import { SchemaChangeListener } from '@travetto/schema';
|
|
2
|
+
import { SchemaChangeListener, SchemaRegistryIndex } from '@travetto/schema';
|
|
3
|
+
import { Registry } from '@travetto/registry';
|
|
3
4
|
|
|
4
|
-
import { ModelRegistry } from '../registry/model.ts';
|
|
5
5
|
import { ModelStorageSupport } from '../types/storage.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Model storage util
|
|
@@ -24,29 +24,29 @@ export class ModelStorageUtil {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const checkType = (cls: Class, enforceBase = true): boolean => {
|
|
27
|
-
if (enforceBase &&
|
|
27
|
+
if (enforceBase && SchemaRegistryIndex.getBaseClass(cls) !== cls) {
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
-
const { autoCreate } =
|
|
31
|
-
return autoCreate;
|
|
30
|
+
const { autoCreate } = ModelRegistryIndex.getConfig(cls) ?? {};
|
|
31
|
+
return autoCreate ?? false;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
// If listening for model add/removes/updates
|
|
35
35
|
if (storage.createModel || storage.deleteModel || storage.changeModel) {
|
|
36
|
-
|
|
36
|
+
Registry.onClassChange(ev => {
|
|
37
37
|
switch (ev.type) {
|
|
38
|
-
case 'added': checkType(ev.curr
|
|
39
|
-
case 'changed': checkType(ev.curr
|
|
40
|
-
case 'removing': checkType(ev.prev
|
|
38
|
+
case 'added': checkType(ev.curr) ? storage.createModel?.(ev.curr) : undefined; break;
|
|
39
|
+
case 'changed': checkType(ev.curr, false) ? storage.changeModel?.(ev.curr) : undefined; break;
|
|
40
|
+
case 'removing': checkType(ev.prev) ? storage.deleteModel?.(ev.prev) : undefined; break;
|
|
41
41
|
}
|
|
42
|
-
});
|
|
42
|
+
}, ModelRegistryIndex);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Initialize on startup (test manages)
|
|
46
46
|
await storage.createStorage();
|
|
47
47
|
|
|
48
48
|
if (storage.createModel) {
|
|
49
|
-
for (const cls of
|
|
49
|
+
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
50
50
|
if (checkType(cls)) {
|
|
51
51
|
await storage.createModel(cls);
|
|
52
52
|
}
|
package/support/base-command.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Env } from '@travetto/runtime';
|
|
2
2
|
import { CliValidationError, CliCommandShape, cliTpl } from '@travetto/cli';
|
|
3
|
-
import {
|
|
3
|
+
import { Registry } from '@travetto/registry';
|
|
4
|
+
import { Schema } from '@travetto/schema';
|
|
4
5
|
|
|
5
|
-
import type { ModelStorageSupport } from '../src/
|
|
6
|
+
import type { ModelStorageSupport } from '../src/types/storage.ts';
|
|
6
7
|
|
|
7
8
|
import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* CLI Entry point for exporting model schemas
|
|
11
12
|
*/
|
|
13
|
+
@Schema()
|
|
12
14
|
export abstract class BaseModelCommand implements CliCommandShape {
|
|
13
15
|
|
|
14
16
|
/** Application Environment */
|
|
@@ -21,7 +23,7 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
async help(): Promise<string[]> {
|
|
24
|
-
await
|
|
26
|
+
await Registry.init();
|
|
25
27
|
|
|
26
28
|
const candidates = await ModelCandidateUtil.export(this.getOp());
|
|
27
29
|
return [
|
|
@@ -36,7 +38,7 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
async validate(provider: string, models: string[]): Promise<CliValidationError | undefined> {
|
|
39
|
-
await
|
|
41
|
+
await Registry.init();
|
|
40
42
|
|
|
41
43
|
const candidates = await ModelCandidateUtil.export(this.getOp());
|
|
42
44
|
if (provider && !candidates.providers.includes(provider)) {
|
package/support/bin/candidate.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { toConcrete, Class } from '@travetto/runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { InjectableCandidate, DependencyRegistryIndex } from '@travetto/di';
|
|
3
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
3
4
|
|
|
4
|
-
import { ModelRegistry } from '../../src/registry/model.ts';
|
|
5
5
|
import type { ModelStorageSupport } from '../../src/types/storage.ts';
|
|
6
6
|
import type { ModelType } from '../../src/types/model.ts';
|
|
7
|
+
import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Utilities for finding candidates for model operations
|
|
@@ -23,23 +24,23 @@ export class ModelCandidateUtil {
|
|
|
23
24
|
static async #getModels(models?: string[]): Promise<Class<ModelType>[]> {
|
|
24
25
|
const names = new Set(models ?? []);
|
|
25
26
|
const all = names.has('*');
|
|
26
|
-
return
|
|
27
|
-
.map(x =>
|
|
28
|
-
.filter(x => !models || all || names.has(
|
|
27
|
+
return ModelRegistryIndex.getClasses()
|
|
28
|
+
.map(x => SchemaRegistryIndex.getBaseClass(x))
|
|
29
|
+
.filter(x => !models || all || names.has(ModelRegistryIndex.getStoreName(x)));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Get model names
|
|
33
34
|
*/
|
|
34
35
|
static async getModelNames(): Promise<string[]> {
|
|
35
|
-
return (await this.#getModels()).map(x =>
|
|
36
|
+
return (await this.#getModels()).map(x => ModelRegistryIndex.getStoreName(x)).toSorted();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Get all providers that are viable candidates
|
|
40
41
|
*/
|
|
41
|
-
static async getProviders(op?: keyof ModelStorageSupport): Promise<
|
|
42
|
-
const types =
|
|
42
|
+
static async getProviders(op?: keyof ModelStorageSupport): Promise<InjectableCandidate[]> {
|
|
43
|
+
const types = DependencyRegistryIndex.getCandidates(toConcrete<ModelStorageSupport>());
|
|
43
44
|
return types.filter(x => !op || x.class.prototype?.[op]);
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -57,7 +58,7 @@ export class ModelCandidateUtil {
|
|
|
57
58
|
*/
|
|
58
59
|
static async getProvider(provider: string): Promise<ModelStorageSupport> {
|
|
59
60
|
const config = (await this.getProviders()).find(x => x.class.name === `${provider}ModelService`)!;
|
|
60
|
-
return
|
|
61
|
+
return DependencyRegistryIndex.getInstance<ModelStorageSupport>(config.candidateType, config.qualifier);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
package/support/test/base.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
2
2
|
import { AppError, castTo, Class, classConstruct } from '@travetto/runtime';
|
|
3
3
|
|
|
4
4
|
import { ModelBulkUtil } from '../../src/util/bulk.ts';
|
|
@@ -49,7 +49,7 @@ export abstract class BaseModelSuite<T> {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
get service(): Promise<T> {
|
|
52
|
-
return
|
|
52
|
+
return DependencyRegistryIndex.getInstance(this.serviceClass);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
@@ -3,7 +3,7 @@ import timers from 'node:timers/promises';
|
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
5
|
import { castTo } from '@travetto/runtime';
|
|
6
|
-
import {
|
|
6
|
+
import { Schema, DiscriminatorField, Text, TypeMismatchError, Discriminated } from '@travetto/schema';
|
|
7
7
|
import {
|
|
8
8
|
ModelIndexedSupport, Index, ModelCrudSupport, Model,
|
|
9
9
|
NotFoundError, SubTypeNotSupportedError, PersistValue
|
|
@@ -14,10 +14,11 @@ import { ExistsError } from '../../src/error/exists.ts';
|
|
|
14
14
|
|
|
15
15
|
import { BaseModelSuite } from './base.ts';
|
|
16
16
|
|
|
17
|
-
@
|
|
18
|
-
|
|
17
|
+
@Schema()
|
|
18
|
+
@Model()
|
|
19
|
+
export abstract class Worker {
|
|
19
20
|
id: string;
|
|
20
|
-
@
|
|
21
|
+
@DiscriminatorField()
|
|
21
22
|
_type: string;
|
|
22
23
|
@Text()
|
|
23
24
|
name: string;
|
|
@@ -42,19 +43,19 @@ export class Engineer extends Worker {
|
|
|
42
43
|
major: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
@Model(
|
|
46
|
+
@Model()
|
|
46
47
|
@Index({
|
|
47
48
|
name: 'worker-name',
|
|
48
49
|
type: 'sorted',
|
|
49
50
|
fields: [{ name: 1 }, { age: 1 }]
|
|
50
51
|
})
|
|
52
|
+
@Discriminated('type')
|
|
51
53
|
export class IndexedWorker {
|
|
52
54
|
id: string;
|
|
53
55
|
type: string;
|
|
54
56
|
name: string;
|
|
55
57
|
age?: number;
|
|
56
58
|
}
|
|
57
|
-
|
|
58
59
|
@Model()
|
|
59
60
|
export class IndexedDoctor extends IndexedWorker {
|
|
60
61
|
specialty: string;
|
package/support/test/suite.ts
CHANGED
|
@@ -1,90 +1,87 @@
|
|
|
1
1
|
import { Class } from '@travetto/runtime';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
3
|
+
import { Registry } from '@travetto/registry';
|
|
4
|
+
import { SuiteRegistryIndex, TestFixtures } from '@travetto/test';
|
|
5
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
5
6
|
|
|
6
7
|
import { ModelBlobUtil } from '../../src/util/blob.ts';
|
|
7
8
|
import { ModelStorageUtil } from '../../src/util/storage.ts';
|
|
8
|
-
import {
|
|
9
|
+
import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
|
|
9
10
|
|
|
10
11
|
const Loaded = Symbol();
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @augments `@travetto/schema:Schema`
|
|
15
|
+
* @kind decorator
|
|
16
|
+
*/
|
|
12
17
|
export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean, namespace?: string }>, serviceClass: Class }>(qualifier?: symbol) {
|
|
13
18
|
const fixtures = new TestFixtures(['@travetto/model']);
|
|
14
19
|
return (target: Class<T>): void => {
|
|
15
20
|
target.prototype.fixtures = fixtures;
|
|
21
|
+
SuiteRegistryIndex.getForRegister(target).register({
|
|
22
|
+
beforeAll: [
|
|
23
|
+
async function (this: T & { [Loaded]?: boolean }) {
|
|
24
|
+
await Registry.init();
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
|
|
26
|
+
if (!this[Loaded]) {
|
|
27
|
+
const config = await DependencyRegistryIndex.getInstance(this.configClass);
|
|
28
|
+
if ('namespace' in config) {
|
|
29
|
+
config.namespace = `test_${Math.trunc(Math.random() * 10000)}`;
|
|
30
|
+
}
|
|
31
|
+
// We manually create
|
|
32
|
+
config.autoCreate = false;
|
|
33
|
+
this[Loaded] = true;
|
|
26
34
|
}
|
|
27
|
-
// We manually create
|
|
28
|
-
config.autoCreate = false;
|
|
29
|
-
this[Loaded] = true;
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.filter(x => x === ModelRegistry.getBaseModel(x))
|
|
43
|
-
.map(m => service.createModel!(m)));
|
|
36
|
+
],
|
|
37
|
+
beforeEach: [
|
|
38
|
+
async function (this: T) {
|
|
39
|
+
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
40
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
41
|
+
await service.createStorage();
|
|
42
|
+
if (service.createModel) {
|
|
43
|
+
await Promise.all(ModelRegistryIndex.getClasses()
|
|
44
|
+
.filter(x => x === SchemaRegistryIndex.getBaseClass(x))
|
|
45
|
+
.map(m => service.createModel!(m)));
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const service = await DependencyRegistry.getInstance(this.serviceClass, qualifier);
|
|
53
|
-
if (ModelStorageUtil.isSupported(service)) {
|
|
54
|
-
const models = ModelRegistry.getClasses().filter(m => m === ModelRegistry.getBaseModel(m));
|
|
49
|
+
],
|
|
50
|
+
afterEach: [
|
|
51
|
+
async function (this: T) {
|
|
52
|
+
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
53
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
54
|
+
const models = ModelRegistryIndex.getClasses().filter(m => m === SchemaRegistryIndex.getBaseClass(m));
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
|
|
57
|
+
await service.truncateBlob();
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
if (service.truncateModel) {
|
|
61
|
+
await Promise.all(models.map(x => service.truncateModel!(x)));
|
|
62
|
+
} else if (service.deleteModel) {
|
|
63
|
+
await Promise.all(models.map(x => service.deleteModel!(x)));
|
|
64
|
+
} else {
|
|
65
|
+
await service.deleteStorage(); // Purge it all
|
|
66
|
+
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (m === ModelRegistry.getBaseModel(m)) {
|
|
79
|
-
await service.deleteModel(m);
|
|
69
|
+
],
|
|
70
|
+
afterAll: [
|
|
71
|
+
async function (this: T) {
|
|
72
|
+
const service = await DependencyRegistryIndex.getInstance(this.serviceClass, qualifier);
|
|
73
|
+
if (ModelStorageUtil.isSupported(service)) {
|
|
74
|
+
if (service.deleteModel) {
|
|
75
|
+
for (const m of ModelRegistryIndex.getClasses()) {
|
|
76
|
+
if (m === SchemaRegistryIndex.getBaseClass(m)) {
|
|
77
|
+
await service.deleteModel(m);
|
|
78
|
+
}
|
|
80
79
|
}
|
|
81
80
|
}
|
|
81
|
+
await service.deleteStorage();
|
|
82
82
|
}
|
|
83
|
-
await service.deleteStorage();
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
|
|
84
|
+
]
|
|
85
|
+
});
|
|
89
86
|
};
|
|
90
87
|
}
|
package/src/registry/model.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { SchemaRegistry } from '@travetto/schema';
|
|
2
|
-
import { MetadataRegistry } from '@travetto/registry';
|
|
3
|
-
import { DependencyRegistry } from '@travetto/di';
|
|
4
|
-
import { AppError, castTo, Class, describeFunction, asFull } from '@travetto/runtime';
|
|
5
|
-
|
|
6
|
-
import { IndexConfig, IndexType, ModelOptions } from './types.ts';
|
|
7
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
8
|
-
import { ModelType } from '../types/model.ts';
|
|
9
|
-
import { IndexNotSupported } from '../error/invalid-index.ts';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Registry for all models, built on the Metadata registry
|
|
13
|
-
*/
|
|
14
|
-
class $ModelRegistry extends MetadataRegistry<ModelOptions<ModelType>> {
|
|
15
|
-
/**
|
|
16
|
-
* All stores names
|
|
17
|
-
*/
|
|
18
|
-
stores = new Map<Class, string>();
|
|
19
|
-
/**
|
|
20
|
-
* All base model classes (inherited from)
|
|
21
|
-
*/
|
|
22
|
-
baseModels = new Map<Class, Class>();
|
|
23
|
-
/**
|
|
24
|
-
* Indexed base model classes to all subclasses
|
|
25
|
-
*/
|
|
26
|
-
baseModelGrouped = new Map<Class, Class[]>();
|
|
27
|
-
/**
|
|
28
|
-
* Default mapping of classes by class name or
|
|
29
|
-
* by requested store name. This is the state at the
|
|
30
|
-
* start of the application.
|
|
31
|
-
*/
|
|
32
|
-
initialModelNameMapping = new Map<string, Class[]>();
|
|
33
|
-
|
|
34
|
-
constructor() {
|
|
35
|
-
// Listen to schema and dependency
|
|
36
|
-
super(SchemaRegistry, DependencyRegistry);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getInitialNameMapping(): Map<string, Class[]> {
|
|
40
|
-
if (this.initialModelNameMapping.size === 0) {
|
|
41
|
-
for (const cls of this.getClasses()) {
|
|
42
|
-
const store = this.get(cls).store ?? cls.name;
|
|
43
|
-
if (!this.initialModelNameMapping.has(store)) {
|
|
44
|
-
this.initialModelNameMapping.set(store, []);
|
|
45
|
-
}
|
|
46
|
-
this.initialModelNameMapping.get(store)!.push(cls);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return this.initialModelNameMapping;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
createPending(cls: Class): Partial<ModelOptions<ModelType>> {
|
|
53
|
-
return {
|
|
54
|
-
class: cls,
|
|
55
|
-
indices: [],
|
|
56
|
-
autoCreate: true,
|
|
57
|
-
baseType: describeFunction(cls).abstract,
|
|
58
|
-
postLoad: [],
|
|
59
|
-
prePersist: []
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
registerDataHandlers(cls: Class, pConfig?: Partial<ModelOptions<ModelType>>): void {
|
|
64
|
-
const cfg = this.getOrCreatePending(cls);
|
|
65
|
-
this.register(cls, {
|
|
66
|
-
...cfg,
|
|
67
|
-
prePersist: [...cfg.prePersist ?? [], ...pConfig?.prePersist ?? []],
|
|
68
|
-
postLoad: [...cfg.postLoad ?? [], ...pConfig?.postLoad ?? []],
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
onInstallFinalize(cls: Class): ModelOptions<ModelType> {
|
|
73
|
-
const config = asFull(this.pending.get(cls.Ⲑid)!);
|
|
74
|
-
|
|
75
|
-
const schema = SchemaRegistry.get(cls);
|
|
76
|
-
const view = schema.totalView.schema;
|
|
77
|
-
delete view.id.required; // Allow ids to be optional
|
|
78
|
-
|
|
79
|
-
if (schema.subTypeField in view && this.getBaseModel(cls) !== cls) {
|
|
80
|
-
config.subType = !!schema.subTypeName; // Copy from schema
|
|
81
|
-
delete view[schema.subTypeField].required; // Allow type to be optional
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const parent = this.getParentClass(cls);
|
|
85
|
-
if (parent && parent !== cls) {
|
|
86
|
-
const pCfg = this.get(parent) ?? this.pending.get(MetadataRegistry.id(parent));
|
|
87
|
-
config.prePersist = [...pCfg?.prePersist ?? [], ...config.prePersist ?? []];
|
|
88
|
-
config.postLoad = [...pCfg?.postLoad ?? [], ...config.postLoad ?? []];
|
|
89
|
-
}
|
|
90
|
-
return config;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
override onUninstallFinalize(cls: Class): void {
|
|
94
|
-
this.stores.delete(cls);
|
|
95
|
-
|
|
96
|
-
// Force system to recompute on uninstall
|
|
97
|
-
this.baseModels.clear();
|
|
98
|
-
this.baseModelGrouped.clear();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Find base class for a given model
|
|
103
|
-
*/
|
|
104
|
-
getBaseModel<T extends ModelType>(cls: Class<T>): Class<T> {
|
|
105
|
-
if (!this.baseModels.has(cls)) {
|
|
106
|
-
let conf = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
107
|
-
let parent = cls;
|
|
108
|
-
|
|
109
|
-
while (conf && !conf.baseType) {
|
|
110
|
-
parent = this.getParentClass(parent)!;
|
|
111
|
-
conf = this.get(parent) ?? this.pending.get(MetadataRegistry.id(parent));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
this.baseModels.set(cls, conf ? parent : cls);
|
|
115
|
-
}
|
|
116
|
-
return this.baseModels.get(cls)!;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Find all classes by their base types
|
|
121
|
-
*/
|
|
122
|
-
getAllClassesByBaseType(): Map<Class, Class[]> {
|
|
123
|
-
if (!this.baseModelGrouped.size) {
|
|
124
|
-
const out = new Map<Class, Class[]>();
|
|
125
|
-
for (const el of this.entries.keys()) {
|
|
126
|
-
const conf = this.entries.get(el)!;
|
|
127
|
-
if (conf.baseType) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const parent = this.getBaseModel(conf.class);
|
|
132
|
-
|
|
133
|
-
if (!out.has(parent)) {
|
|
134
|
-
out.set(parent, []);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
out.get(parent)!.push(conf.class);
|
|
138
|
-
}
|
|
139
|
-
this.baseModelGrouped = out;
|
|
140
|
-
}
|
|
141
|
-
return this.baseModelGrouped;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get all classes for a given base type
|
|
146
|
-
*/
|
|
147
|
-
getClassesByBaseType(base: Class): Class[] {
|
|
148
|
-
return this.getAllClassesByBaseType().get(base) ?? [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get the apparent store for a type, handling polymorphism when appropriate
|
|
153
|
-
*/
|
|
154
|
-
getStore(cls: Class): string {
|
|
155
|
-
if (!this.stores.has(cls)) {
|
|
156
|
-
const config = this.get(cls) ?? this.getOrCreatePending(cls);
|
|
157
|
-
const base = this.getBaseModel(cls);
|
|
158
|
-
if (base !== cls) {
|
|
159
|
-
return this.getStore(base);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const name = config.store ?? cls.name.toLowerCase();
|
|
163
|
-
|
|
164
|
-
const candidates = this.getInitialNameMapping().get(name) || [];
|
|
165
|
-
|
|
166
|
-
// Don't allow two models with same class name, or same store name
|
|
167
|
-
if (candidates.length > 1) {
|
|
168
|
-
if (config.store) {
|
|
169
|
-
throw new AppError('Duplicate models with same store name', {
|
|
170
|
-
details: { classes: candidates.map(x => x.Ⲑid) }
|
|
171
|
-
});
|
|
172
|
-
} else {
|
|
173
|
-
throw new AppError('Duplicate models with same class name, but no store name provided', {
|
|
174
|
-
details: { classes: candidates.map(x => x.Ⲑid) }
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
this.stores.set(cls, name);
|
|
180
|
-
}
|
|
181
|
-
return this.stores.get(cls)!;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get Index
|
|
186
|
-
*/
|
|
187
|
-
getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexConfig<T> & { type: K[number] } {
|
|
188
|
-
const cfg = this.get(cls).indices?.find((x): x is IndexConfig<T> => x.name === name);
|
|
189
|
-
if (!cfg) {
|
|
190
|
-
throw new NotFoundError(`${cls.name} Index`, `${name}`);
|
|
191
|
-
}
|
|
192
|
-
if (supportedTypes && !supportedTypes.includes(cfg.type)) {
|
|
193
|
-
throw new IndexNotSupported(cls, cfg, `${cfg.type} indices are not supported.`);
|
|
194
|
-
}
|
|
195
|
-
return cfg;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get Indices
|
|
200
|
-
*/
|
|
201
|
-
getIndices<T extends ModelType, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): (IndexConfig<T> & { type: K[number] })[] {
|
|
202
|
-
return (this.get(cls).indices ?? []).filter((x): x is IndexConfig<T> => !supportedTypes || supportedTypes.includes(x.type));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get expiry field
|
|
207
|
-
* @param cls
|
|
208
|
-
*/
|
|
209
|
-
getExpiry<T extends ModelType>(cls: Class<T>): keyof T {
|
|
210
|
-
const expiry = this.get(cls).expiresAt;
|
|
211
|
-
if (!expiry) {
|
|
212
|
-
throw new AppError(`${cls.name} is not configured with expiry support, please use @ExpiresAt to declare expiration behavior`);
|
|
213
|
-
}
|
|
214
|
-
return castTo(expiry);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export const ModelRegistry = new $ModelRegistry();
|