@travetto/model 8.0.0-alpha.0 → 8.0.0-alpha.10
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 +4 -44
- package/__index__.ts +0 -2
- package/package.json +7 -7
- package/src/error/invalid-index.ts +2 -2
- package/src/registry/decorator.ts +2 -15
- package/src/registry/registry-adapter.ts +2 -2
- package/src/registry/registry-index.ts +5 -27
- package/src/registry/types.ts +21 -40
- package/src/util/blob.ts +5 -0
- package/support/base-command.ts +20 -16
- package/support/cli.model_export.ts +3 -1
- package/support/cli.model_install.ts +3 -1
- package/support/doc.support.tsx +0 -2
- package/support/test/blob.ts +3 -3
- package/support/test/polymorphism.ts +6 -114
- package/support/test/suite.ts +4 -3
- package/src/types/indexed.ts +0 -43
- package/src/util/indexed.ts +0 -144
- package/support/test/indexed.ts +0 -190
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#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#
|
|
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#L12).
|
|
20
20
|
|
|
21
21
|
**Code: Basic Structure**
|
|
22
22
|
```typescript
|
|
@@ -33,7 +33,7 @@ export class SampleModel {
|
|
|
33
33
|
Once the model is defined, it can be leveraged with any of the services that implement the various model storage contracts. These contracts allow for persisting and fetching of the associated model object.
|
|
34
34
|
|
|
35
35
|
## Contracts
|
|
36
|
-
The module is mainly composed of contracts. The contracts define the expected interface for various model patterns. The primary contracts are [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/types/basic.ts#L8), [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11), [
|
|
36
|
+
The module is mainly composed of contracts. The contracts define the expected interface for various model patterns. The primary contracts are [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/types/basic.ts#L8), [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11), [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10), [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8) and [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L64).
|
|
37
37
|
|
|
38
38
|
### Basic
|
|
39
39
|
All [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementations, must honor the [Basic](https://github.com/travetto/travetto/tree/main/module/model/src/types/basic.ts#L8) contract to be able to participate in the model ecosystem. This contract represents the bare minimum for a model service.
|
|
@@ -116,46 +116,6 @@ export interface ModelCrudSupport extends ModelBasicSupport {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
### Indexed
|
|
120
|
-
Additionally, an implementation may support the ability for basic [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11) queries. This is not the full featured query support of [Data Model Querying](https://github.com/travetto/travetto/tree/main/module/model-query#readme "Datastore abstraction for advanced query support."), but allowing for indexed lookups. This does not support listing by index, but may be added at a later date.
|
|
121
|
-
|
|
122
|
-
**Code: Indexed Contract**
|
|
123
|
-
```typescript
|
|
124
|
-
export interface ModelIndexedSupport extends ModelBasicSupport {
|
|
125
|
-
/**
|
|
126
|
-
* Get entity by index as defined by fields of idx and the body fields
|
|
127
|
-
* @param cls The type to search by
|
|
128
|
-
* @param idx The index name to search against
|
|
129
|
-
* @param body The payload of fields needed to search
|
|
130
|
-
*/
|
|
131
|
-
getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Delete entity by index as defined by fields of idx and the body fields
|
|
135
|
-
* @param cls The type to search by
|
|
136
|
-
* @param idx The index name to search against
|
|
137
|
-
* @param body The payload of fields needed to search
|
|
138
|
-
*/
|
|
139
|
-
deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* List entity by ranged index as defined by fields of idx and the body fields
|
|
143
|
-
* @param cls The type to search by
|
|
144
|
-
* @param idx The index name to search against
|
|
145
|
-
* @param body The payload of fields needed to search
|
|
146
|
-
*/
|
|
147
|
-
listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Upsert by index, allowing the index to act as a primary key
|
|
151
|
-
* @param cls The type to create for
|
|
152
|
-
* @param idx The index name to use
|
|
153
|
-
* @param body The document to potentially store
|
|
154
|
-
*/
|
|
155
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
119
|
### Expiry
|
|
160
120
|
Certain implementations will also provide support for automatic [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10) of data at runtime. This is extremely useful for temporary data as, and is used in the [Caching](https://github.com/travetto/travetto/tree/main/module/cache#readme "Caching functionality with decorators for declarative use.") module for expiring data accordingly.
|
|
161
121
|
|
|
@@ -354,7 +314,7 @@ Usage: model:export [options] <provider:string> <models...:string>
|
|
|
354
314
|
Options:
|
|
355
315
|
-p, --profile <string> Application profiles
|
|
356
316
|
-m, --module <module> Module to run for
|
|
357
|
-
|
|
317
|
+
--help display help for command
|
|
358
318
|
|
|
359
319
|
Providers
|
|
360
320
|
--------------------
|
|
@@ -377,7 +337,7 @@ Usage: model:install [options] <provider:string> <models...:string>
|
|
|
377
337
|
Options:
|
|
378
338
|
-p, --profile <string> Application profiles
|
|
379
339
|
-m, --module <module> Module to run for
|
|
380
|
-
|
|
340
|
+
--help display help for command
|
|
381
341
|
|
|
382
342
|
Providers
|
|
383
343
|
--------------------
|
package/__index__.ts
CHANGED
|
@@ -8,7 +8,6 @@ export * from './src/types/basic.ts';
|
|
|
8
8
|
export * from './src/types/blob.ts';
|
|
9
9
|
export * from './src/types/bulk.ts';
|
|
10
10
|
export * from './src/types/crud.ts';
|
|
11
|
-
export * from './src/types/indexed.ts';
|
|
12
11
|
export * from './src/types/expiry.ts';
|
|
13
12
|
export * from './src/types/storage.ts';
|
|
14
13
|
|
|
@@ -16,7 +15,6 @@ export * from './src/util/blob.ts';
|
|
|
16
15
|
export * from './src/util/bulk.ts';
|
|
17
16
|
export * from './src/util/crud.ts';
|
|
18
17
|
export * from './src/util/expiry.ts';
|
|
19
|
-
export * from './src/util/indexed.ts';
|
|
20
18
|
export * from './src/util/storage.ts';
|
|
21
19
|
|
|
22
20
|
export * from './src/error/exists.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Datastore abstraction for core operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
"directory": "module/model"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/di": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/registry": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.10",
|
|
31
|
+
"@travetto/di": "^8.0.0-alpha.10",
|
|
32
|
+
"@travetto/registry": "^8.0.0-alpha.10",
|
|
33
|
+
"@travetto/schema": "^8.0.0-alpha.10"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
37
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.15",
|
|
37
|
+
"@travetto/test": "^8.0.0-alpha.10"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@travetto/cli": {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type Class, RuntimeError } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { IndexConfig } from '../registry/types.ts';
|
|
4
3
|
import type { ModelType } from '../types/model.ts';
|
|
4
|
+
import type { IndexConfig } from '../registry/types.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Represents when an index is invalid
|
|
8
8
|
*/
|
|
9
9
|
export class IndexNotSupported<T extends ModelType> extends RuntimeError {
|
|
10
|
-
constructor(cls: Class<T>, idx: IndexConfig
|
|
10
|
+
constructor(cls: Class<T>, idx: IndexConfig, message: string = '') {
|
|
11
11
|
super(`${typeof cls === 'string' ? cls : cls.name} and index ${idx.name} of type ${idx.type} is not supported. ${message}`.trim(), { category: 'data' });
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { castTo, type Class, getClass } from '@travetto/runtime';
|
|
2
2
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import type { ModelType } from '../types/model.ts';
|
|
5
|
-
import type { DataHandler,
|
|
5
|
+
import type { DataHandler, ModelConfig, PrePersistScope } from './types.ts';
|
|
6
6
|
import { ModelRegistryIndex } from './registry-index.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -24,19 +24,6 @@ export function Model(config: Partial<ModelConfig<ModelType>> | string = {}) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Defines an index on a model
|
|
29
|
-
* @kind decorator
|
|
30
|
-
*/
|
|
31
|
-
export function Index<T extends ModelType>(...indices: IndexConfig<T>[]) {
|
|
32
|
-
if (indices.some(config => config.fields.some(field => field === 'id'))) {
|
|
33
|
-
throw new RuntimeError('Cannot create an index with the id field');
|
|
34
|
-
}
|
|
35
|
-
return function (cls: Class<T>): void {
|
|
36
|
-
ModelRegistryIndex.getForRegister(cls).register({ indices });
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
27
|
/**
|
|
41
28
|
* Model field decorator for denoting expiry date/time
|
|
42
29
|
* @augments `@travetto/schema:Field`
|
|
@@ -7,7 +7,7 @@ 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
13
|
});
|
|
@@ -29,7 +29,7 @@ export class ModelRegistryAdapter implements RegistryAdapter<ModelConfig> {
|
|
|
29
29
|
register(...data: Partial<ModelConfig>[]): ModelConfig {
|
|
30
30
|
const config = this.#config ??= {
|
|
31
31
|
class: this.#cls,
|
|
32
|
-
indices:
|
|
32
|
+
indices: {},
|
|
33
33
|
autoCreate: 'development',
|
|
34
34
|
store: this.#cls.name,
|
|
35
35
|
postLoad: [],
|
|
@@ -2,13 +2,9 @@ import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/regi
|
|
|
2
2
|
import { RuntimeError, castTo, type Class } from '@travetto/runtime';
|
|
3
3
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import type { IndexConfig,
|
|
5
|
+
import type { IndexConfig, ModelConfig } from './types.ts';
|
|
6
6
|
import type { ModelType } from '../types/model.ts';
|
|
7
7
|
import { ModelRegistryAdapter } from './registry-adapter.ts';
|
|
8
|
-
import { IndexNotSupported } from '../error/invalid-index.ts';
|
|
9
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
10
|
-
|
|
11
|
-
type IndexResult<T extends ModelType, K extends IndexType[]> = IndexConfig<T> & { type: K[number] };
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
10
|
* Model registry index for managing model configurations across classes
|
|
@@ -33,12 +29,8 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
33
29
|
return this.#instance.getStoreName(cls);
|
|
34
30
|
}
|
|
35
31
|
|
|
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);
|
|
32
|
+
static getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
|
|
33
|
+
return this.#instance.getIndices(cls);
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
static getExpiryFieldName<T extends ModelType>(cls: Class<T>): keyof T {
|
|
@@ -94,25 +86,11 @@ export class ModelRegistryIndex implements RegistryIndex {
|
|
|
94
86
|
return this.store.get(cls).get().store;
|
|
95
87
|
}
|
|
96
88
|
|
|
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
89
|
/**
|
|
112
90
|
* Get Indices
|
|
113
91
|
*/
|
|
114
|
-
getIndices<T extends ModelType
|
|
115
|
-
return (this.getConfig(cls).indices ?? [])
|
|
92
|
+
getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
|
|
93
|
+
return Object.values(this.getConfig(cls).indices ?? []);
|
|
116
94
|
}
|
|
117
95
|
|
|
118
96
|
/**
|
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 RetainPrimitiveFields<T> = Pick<T, ValidFields<T, Primitive | Date>>;
|
|
6
|
-
|
|
7
|
-
export type SortClauseRaw<T> = {
|
|
8
|
-
[P in keyof T]?:
|
|
9
|
-
T[P] extends object ? SortClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type IndexClauseRaw<T> = {
|
|
13
|
-
[P in keyof T]?:
|
|
14
|
-
T[P] extends object ? IndexClauseRaw<RetainPrimitiveFields<T[P]>> : 1 | -1 | true;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
5
|
export type DataHandler<T = unknown> = (inst: T) => (Promise<T | void> | T | void);
|
|
18
6
|
|
|
19
7
|
export type PrePersistScope = 'full' | 'partial' | 'all';
|
|
20
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Index options
|
|
11
|
+
*/
|
|
12
|
+
export type IndexConfig<V extends string = string> = {
|
|
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<ModelType>;
|
|
25
|
+
};
|
|
26
|
+
|
|
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
|
*/
|
|
@@ -54,29 +60,4 @@ export class ModelConfig<T extends ModelType = ModelType> {
|
|
|
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
|
-
/**
|
|
73
|
-
* Fields and sort order
|
|
74
|
-
*/
|
|
75
|
-
fields: IndexClauseRaw<RetainPrimitiveFields<T>>[];
|
|
76
|
-
/**
|
|
77
|
-
* Type
|
|
78
|
-
*/
|
|
79
|
-
type: IndexType;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export type IndexField<T extends ModelType> = IndexClauseRaw<RetainPrimitiveFields<T>>;
|
|
63
|
+
}
|
package/src/util/blob.ts
CHANGED
|
@@ -10,4 +10,9 @@ export class ModelBlobUtil {
|
|
|
10
10
|
* Type guard for determining if service supports blob operations
|
|
11
11
|
*/
|
|
12
12
|
static isSupported = hasFunction<ModelBlobSupport>('getBlob');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type guard for determining if service supports blob write urls
|
|
16
|
+
*/
|
|
17
|
+
static isWriteUrlSupported = hasFunction<ModelBlobSupport>('getBlobWriteUrl');
|
|
13
18
|
}
|
package/support/base-command.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Env } from '@travetto/runtime';
|
|
2
|
-
import { type
|
|
2
|
+
import { type CliCommandShape, cliTpl, CliModuleFlag, CliProfilesFlag } from '@travetto/cli';
|
|
3
3
|
import { Registry } from '@travetto/registry';
|
|
4
|
-
import { Schema } from '@travetto/schema';
|
|
4
|
+
import { Schema, type ValidationError } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import type { ModelStorageSupport } from '../src/types/storage.ts';
|
|
7
7
|
|
|
@@ -13,9 +13,26 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
13
13
|
@Schema()
|
|
14
14
|
export abstract class BaseModelCommand implements CliCommandShape {
|
|
15
15
|
|
|
16
|
+
static async validate(operation: keyof ModelStorageSupport, provider: string, models: string[]): Promise<ValidationError | undefined> {
|
|
17
|
+
const candidates = await ModelCandidateUtil.export(operation);
|
|
18
|
+
if (provider && !candidates.providers.includes(provider)) {
|
|
19
|
+
return { message: `provider: ${provider} is not a valid provider`, source: 'arg', kind: 'invalid', path: 'provider' };
|
|
20
|
+
}
|
|
21
|
+
const badModel = models.find(model => model !== '*' && !candidates.models.includes(model));
|
|
22
|
+
if (badModel) {
|
|
23
|
+
return { message: `model: ${badModel} is not a valid model`, source: 'arg', kind: 'invalid', path: 'models' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@CliProfilesFlag()
|
|
28
|
+
profile: string[];
|
|
29
|
+
|
|
30
|
+
@CliModuleFlag({ short: 'm' })
|
|
31
|
+
module: string;
|
|
32
|
+
|
|
16
33
|
abstract getOperation(): keyof ModelStorageSupport;
|
|
17
34
|
|
|
18
|
-
|
|
35
|
+
finalize(): void {
|
|
19
36
|
Env.DEBUG.set(false);
|
|
20
37
|
}
|
|
21
38
|
|
|
@@ -34,18 +51,5 @@ export abstract class BaseModelCommand implements CliCommandShape {
|
|
|
34
51
|
];
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
async validate(provider: string, models: string[]): Promise<CliValidationError | undefined> {
|
|
38
|
-
await Registry.init();
|
|
39
|
-
|
|
40
|
-
const candidates = await ModelCandidateUtil.export(this.getOperation());
|
|
41
|
-
if (provider && !candidates.providers.includes(provider)) {
|
|
42
|
-
return { message: `provider: ${provider} is not a valid provider`, source: 'arg' };
|
|
43
|
-
}
|
|
44
|
-
const badModel = models.find(model => model !== '*' && !candidates.models.includes(model));
|
|
45
|
-
if (badModel) {
|
|
46
|
-
return { message: `model: ${badModel} is not a valid model`, source: 'arg' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
54
|
abstract main(...args: unknown[]): ReturnType<CliCommandShape['main']>;
|
|
51
55
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliCommand } from '@travetto/cli';
|
|
2
|
+
import { MethodValidator } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { BaseModelCommand } from './base-command.ts';
|
|
4
5
|
import { ModelExportUtil } from './bin/export.ts';
|
|
@@ -7,11 +8,12 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
7
8
|
/**
|
|
8
9
|
* Exports model schemas
|
|
9
10
|
*/
|
|
10
|
-
@CliCommand(
|
|
11
|
+
@CliCommand()
|
|
11
12
|
export class ModelExportCommand extends BaseModelCommand {
|
|
12
13
|
|
|
13
14
|
getOperation(): 'exportModel' { return 'exportModel'; }
|
|
14
15
|
|
|
16
|
+
@MethodValidator(BaseModelCommand.validate.bind(null, 'exportModel'))
|
|
15
17
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
18
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
|
17
19
|
await ModelExportUtil.run(resolved.provider, resolved.models);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliCommand, cliTpl } from '@travetto/cli';
|
|
2
|
+
import { MethodValidator } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { BaseModelCommand } from './base-command.ts';
|
|
4
5
|
import { ModelInstallUtil } from './bin/install.ts';
|
|
@@ -7,11 +8,12 @@ import { ModelCandidateUtil } from './bin/candidate.ts';
|
|
|
7
8
|
/**
|
|
8
9
|
* Installing models
|
|
9
10
|
*/
|
|
10
|
-
@CliCommand(
|
|
11
|
+
@CliCommand()
|
|
11
12
|
export class ModelInstallCommand extends BaseModelCommand {
|
|
12
13
|
|
|
13
14
|
getOperation(): 'upsertModel' { return 'upsertModel'; }
|
|
14
15
|
|
|
16
|
+
@MethodValidator(BaseModelCommand.validate.bind(null, 'upsertModel'))
|
|
15
17
|
async main(provider: string, models: string[]): Promise<void> {
|
|
16
18
|
const resolved = await ModelCandidateUtil.resolve(provider, models);
|
|
17
19
|
await ModelInstallUtil.run(resolved.provider, resolved.models);
|
package/support/doc.support.tsx
CHANGED
|
@@ -8,7 +8,6 @@ import type { ModelBlobSupport } from '../src/types/blob.ts';
|
|
|
8
8
|
import type { ModelBulkSupport } from '../src/types/bulk.ts';
|
|
9
9
|
import type { ModelCrudSupport } from '../src/types/crud.ts';
|
|
10
10
|
import type { ModelExpirySupport } from '../src/types/expiry.ts';
|
|
11
|
-
import type { ModelIndexedSupport } from '../src/types/indexed.ts';
|
|
12
11
|
|
|
13
12
|
const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'> =>
|
|
14
13
|
d.codeLink(title, Runtime.getSourceFile(target), new RegExp(`\\binterface\\s+${target.name}`));
|
|
@@ -17,7 +16,6 @@ export const Links = {
|
|
|
17
16
|
Basic: toLink('Basic', toConcrete<ModelBasicSupport>()),
|
|
18
17
|
Crud: toLink('CRUD', toConcrete<ModelCrudSupport>()),
|
|
19
18
|
Expiry: toLink('Expiry', toConcrete<ModelExpirySupport>()),
|
|
20
|
-
Indexed: toLink('Indexed', toConcrete<ModelIndexedSupport>()),
|
|
21
19
|
Bulk: toLink('Bulk', toConcrete<ModelBulkSupport>()),
|
|
22
20
|
Blob: toLink('Blob', toConcrete<ModelBlobSupport>()),
|
|
23
21
|
};
|
package/support/test/blob.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { BinaryMetadataUtil, BinaryUtil, Util } from '@travetto/runtime';
|
|
|
6
6
|
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
7
7
|
|
|
8
8
|
import type { ModelBlobSupport } from '../../src/types/blob.ts';
|
|
9
|
+
import { ModelBlobUtil } from '../../src/util/blob.ts';
|
|
9
10
|
|
|
10
11
|
@Suite()
|
|
11
12
|
export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
@@ -91,7 +92,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
91
92
|
const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
|
|
92
93
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
93
94
|
|
|
94
|
-
const og = await this.fixture.
|
|
95
|
+
const og = await this.fixture.readUTF8('/text.txt');
|
|
95
96
|
|
|
96
97
|
assert(subContent === og.substring(10, 21));
|
|
97
98
|
|
|
@@ -150,8 +151,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
150
151
|
assert(savedMeta.hash === undefined);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
@Test({ skip: (x: unknown) => !(x as ModelBlobSuite).serviceClass.prototype.getBlobWriteUrl })
|
|
154
|
+
@Test({ skip: ModelBlobSuite.ifNot(ModelBlobUtil.isWriteUrlSupported) })
|
|
155
155
|
async signedUrl() {
|
|
156
156
|
const service = await this.service;
|
|
157
157
|
|
|
@@ -3,13 +3,12 @@ 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 { Schema, DiscriminatorField, Text, TypeMismatchError
|
|
6
|
+
import { Schema, DiscriminatorField, Text, TypeMismatchError } from '@travetto/schema';
|
|
7
7
|
import {
|
|
8
|
-
type
|
|
8
|
+
type ModelCrudSupport, Model,
|
|
9
9
|
NotFoundError, SubTypeNotSupportedError, PersistValue
|
|
10
10
|
} from '@travetto/model';
|
|
11
11
|
|
|
12
|
-
import { ModelIndexedUtil } from '../../src/util/indexed.ts';
|
|
13
12
|
import { ExistsError } from '../../src/error/exists.ts';
|
|
14
13
|
|
|
15
14
|
import { BaseModelSuite } from './base.ts';
|
|
@@ -43,42 +42,6 @@ export class Engineer extends Worker {
|
|
|
43
42
|
major: string;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
@Model()
|
|
47
|
-
@Index({
|
|
48
|
-
name: 'worker-name',
|
|
49
|
-
type: 'sorted',
|
|
50
|
-
fields: [{ name: 1 }, { age: 1 }]
|
|
51
|
-
})
|
|
52
|
-
@Discriminated('type')
|
|
53
|
-
export class IndexedWorker {
|
|
54
|
-
id: string;
|
|
55
|
-
type: string;
|
|
56
|
-
name: string;
|
|
57
|
-
age?: number;
|
|
58
|
-
}
|
|
59
|
-
@Model()
|
|
60
|
-
export class IndexedDoctor extends IndexedWorker {
|
|
61
|
-
specialty: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@Model()
|
|
65
|
-
export class IndexedFirefighter extends IndexedWorker {
|
|
66
|
-
firehouse: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@Model()
|
|
70
|
-
export class IndexedEngineer extends IndexedWorker {
|
|
71
|
-
major: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
75
|
-
const out: T[] = [];
|
|
76
|
-
for await (const el of iterable) {
|
|
77
|
-
out.push(el);
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
45
|
@Suite()
|
|
83
46
|
export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
84
47
|
|
|
@@ -108,7 +71,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
108
71
|
const fire2 = await service.get(Worker, fire.id);
|
|
109
72
|
assert(fire2 instanceof Firefighter);
|
|
110
73
|
|
|
111
|
-
const all = await
|
|
74
|
+
const all = await Array.fromAsync(service.list(Worker));
|
|
112
75
|
assert(all.length === 3);
|
|
113
76
|
|
|
114
77
|
const doc3 = all.find(x => x instanceof Doctor);
|
|
@@ -126,7 +89,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
126
89
|
assert(eng3.major === 'oranges');
|
|
127
90
|
assert(eng3.name === 'cob');
|
|
128
91
|
|
|
129
|
-
const engineers = await
|
|
92
|
+
const engineers = await Array.fromAsync(service.list(Engineer));
|
|
130
93
|
assert(engineers.length === 1);
|
|
131
94
|
|
|
132
95
|
await service.create(Engineer, Engineer.from({
|
|
@@ -134,10 +97,10 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
134
97
|
name: 'bob2'
|
|
135
98
|
}));
|
|
136
99
|
|
|
137
|
-
const all2 = await
|
|
100
|
+
const all2 = await Array.fromAsync(service.list(Worker));
|
|
138
101
|
assert(all2.length === 4);
|
|
139
102
|
|
|
140
|
-
const engineers2 = await
|
|
103
|
+
const engineers2 = await Array.fromAsync(service.list(Engineer));
|
|
141
104
|
assert(engineers2.length === 2);
|
|
142
105
|
}
|
|
143
106
|
|
|
@@ -207,75 +170,4 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
|
|
|
207
170
|
e => e instanceof SubTypeNotSupportedError || e instanceof NotFoundError
|
|
208
171
|
);
|
|
209
172
|
}
|
|
210
|
-
|
|
211
|
-
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
212
|
-
async polymorphicIndexGet() {
|
|
213
|
-
const service: ModelIndexedSupport = castTo(await this.service);
|
|
214
|
-
const now = 30;
|
|
215
|
-
const [doc, fire, eng] = [
|
|
216
|
-
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
217
|
-
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
218
|
-
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
219
|
-
];
|
|
220
|
-
|
|
221
|
-
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
222
|
-
|
|
223
|
-
const result = await service.getByIndex(IndexedWorker, 'worker-name', {
|
|
224
|
-
age: now,
|
|
225
|
-
name: 'rob'
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
assert(result instanceof IndexedFirefighter);
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const res2 = await service.getByIndex(IndexedFirefighter, 'worker-name', {
|
|
232
|
-
age: now,
|
|
233
|
-
name: 'rob'
|
|
234
|
-
});
|
|
235
|
-
assert(res2 instanceof IndexedFirefighter); // If service allows for get by subtype
|
|
236
|
-
} catch (err) {
|
|
237
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError); // If it does not
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
242
|
-
async polymorphicIndexDelete() {
|
|
243
|
-
const service: ModelIndexedSupport = castTo(await this.service);
|
|
244
|
-
const now = 30;
|
|
245
|
-
const [doc, fire, eng] = [
|
|
246
|
-
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
247
|
-
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
248
|
-
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
252
|
-
|
|
253
|
-
assert(await this.getSize(IndexedWorker) === 3);
|
|
254
|
-
|
|
255
|
-
await service.deleteByIndex(IndexedWorker, 'worker-name', {
|
|
256
|
-
age: now,
|
|
257
|
-
name: 'bob'
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
assert(await this.getSize(IndexedWorker) === 2);
|
|
261
|
-
assert(await this.getSize(IndexedDoctor) === 0);
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
await service.deleteByIndex(IndexedFirefighter, 'worker-name', {
|
|
265
|
-
age: now,
|
|
266
|
-
name: 'rob'
|
|
267
|
-
});
|
|
268
|
-
} catch (err) {
|
|
269
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
await service.deleteByIndex(IndexedEngineer, 'worker-name', {
|
|
274
|
-
age: now,
|
|
275
|
-
name: 'bob'
|
|
276
|
-
});
|
|
277
|
-
} catch (err) {
|
|
278
|
-
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
173
|
}
|
package/support/test/suite.ts
CHANGED
|
@@ -45,16 +45,17 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
|
|
|
45
45
|
async afterEach(instance: T) {
|
|
46
46
|
const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
|
|
47
47
|
if (ModelStorageUtil.isSupported(service)) {
|
|
48
|
-
const models = ModelRegistryIndex.getClasses()
|
|
48
|
+
const models = ModelRegistryIndex.getClasses()
|
|
49
|
+
.filter(model => model === SchemaRegistryIndex.getBaseClass(model));
|
|
49
50
|
|
|
50
51
|
if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
|
|
51
52
|
await service.truncateBlob();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
if (service.truncateModel) {
|
|
55
|
-
await Promise.all(models.map(
|
|
56
|
+
await Promise.all(models.map(model => service.truncateModel!(model)));
|
|
56
57
|
} else if (service.deleteModel) {
|
|
57
|
-
await Promise.all(models.map(
|
|
58
|
+
await Promise.all(models.map(model => service.deleteModel!(model)));
|
|
58
59
|
} else {
|
|
59
60
|
await service.deleteStorage(); // Purge it all
|
|
60
61
|
}
|
package/src/types/indexed.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Class, DeepPartial } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
4
|
-
import type { ModelBasicSupport } from './basic.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Support for simple indexed activity
|
|
8
|
-
*
|
|
9
|
-
* @concrete
|
|
10
|
-
*/
|
|
11
|
-
export interface ModelIndexedSupport extends ModelBasicSupport {
|
|
12
|
-
/**
|
|
13
|
-
* Get entity by index as defined by fields of idx and the body fields
|
|
14
|
-
* @param cls The type to search by
|
|
15
|
-
* @param idx The index name to search against
|
|
16
|
-
* @param body The payload of fields needed to search
|
|
17
|
-
*/
|
|
18
|
-
getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Delete entity by index as defined by fields of idx and the body fields
|
|
22
|
-
* @param cls The type to search by
|
|
23
|
-
* @param idx The index name to search against
|
|
24
|
-
* @param body The payload of fields needed to search
|
|
25
|
-
*/
|
|
26
|
-
deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* List entity by ranged index as defined by fields of idx and the body fields
|
|
30
|
-
* @param cls The type to search by
|
|
31
|
-
* @param idx The index name to search against
|
|
32
|
-
* @param body The payload of fields needed to search
|
|
33
|
-
*/
|
|
34
|
-
listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Upsert by index, allowing the index to act as a primary key
|
|
38
|
-
* @param cls The type to create for
|
|
39
|
-
* @param idx The index name to use
|
|
40
|
-
* @param body The document to potentially store
|
|
41
|
-
*/
|
|
42
|
-
upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
|
|
43
|
-
}
|
package/src/util/indexed.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { castTo, type Class, type DeepPartial, hasFunction, TypedObject } from '@travetto/runtime';
|
|
2
|
-
|
|
3
|
-
import { IndexNotSupported } from '../error/invalid-index.ts';
|
|
4
|
-
import { NotFoundError } from '../error/not-found.ts';
|
|
5
|
-
import type { IndexConfig } from '../registry/types.ts';
|
|
6
|
-
import type { ModelCrudSupport } from '../types/crud.ts';
|
|
7
|
-
import type { ModelIndexedSupport } from '../types/indexed.ts';
|
|
8
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
9
|
-
import { ModelRegistryIndex } from '../registry/registry-index.ts';
|
|
10
|
-
|
|
11
|
-
type ComputeConfig = {
|
|
12
|
-
includeSortInFields?: boolean;
|
|
13
|
-
emptyValue?: unknown;
|
|
14
|
-
emptySortValue?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
|
|
18
|
-
type IndexSortPart = { path: string[], dir: number, value: number | Date };
|
|
19
|
-
|
|
20
|
-
const DEFAULT_SEP = '\u8203';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Utils for working with indexed model services
|
|
24
|
-
*/
|
|
25
|
-
export class ModelIndexedUtil {
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Type guard for determining if service supports indexed operation
|
|
29
|
-
*/
|
|
30
|
-
static isSupported = hasFunction<ModelIndexedSupport>('getByIndex');
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Compute flattened field to value mappings
|
|
34
|
-
* @param cls Class to get info for
|
|
35
|
-
* @param idx Index config
|
|
36
|
-
* @param item Item to read values from
|
|
37
|
-
*/
|
|
38
|
-
static computeIndexParts<T extends ModelType>(
|
|
39
|
-
cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
|
|
40
|
-
): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
|
|
41
|
-
const config = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
42
|
-
const sortField = config.type === 'sorted' ? config.fields.at(-1) : undefined;
|
|
43
|
-
|
|
44
|
-
const fields: IndexFieldPart[] = [];
|
|
45
|
-
let sortDirection: number = 0;
|
|
46
|
-
let sorted: IndexSortPart | undefined;
|
|
47
|
-
|
|
48
|
-
for (const field of config.fields) {
|
|
49
|
-
let fieldRef: Record<string, unknown> = field;
|
|
50
|
-
let itemRef: Record<string, unknown> = item;
|
|
51
|
-
const parts = [];
|
|
52
|
-
|
|
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
|
-
}
|
|
61
|
-
break; // At the bottom
|
|
62
|
-
} else {
|
|
63
|
-
fieldRef = castTo(fieldRef[key]);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (field === sortField) {
|
|
67
|
-
sorted = { path: parts, dir: sortDirection, value: castTo(itemRef) };
|
|
68
|
-
}
|
|
69
|
-
if (itemRef === undefined || itemRef === null) {
|
|
70
|
-
const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
|
|
71
|
-
if (empty === undefined || empty === Error) {
|
|
72
|
-
throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
if (field !== sortField || (opts.includeSortInFields ?? true)) {
|
|
76
|
-
fields.push({ path: parts, value: castTo(itemRef) });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { fields, sorted };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Project item via index
|
|
86
|
-
* @param cls Type to get index for
|
|
87
|
-
* @param idx Index config
|
|
88
|
-
*/
|
|
89
|
-
static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, config?: ComputeConfig): Record<string, unknown> {
|
|
90
|
-
const response: Record<string, unknown> = {};
|
|
91
|
-
for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, config).fields) {
|
|
92
|
-
let sub: Record<string, unknown> = response;
|
|
93
|
-
const all = path.slice(0);
|
|
94
|
-
const last = all.pop()!;
|
|
95
|
-
for (const part of all) {
|
|
96
|
-
sub = castTo(sub[part] ??= {});
|
|
97
|
-
}
|
|
98
|
-
sub[last] = value;
|
|
99
|
-
}
|
|
100
|
-
return response;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Compute index key as a single value
|
|
105
|
-
* @param cls Class to get index for
|
|
106
|
-
* @param idx Index config
|
|
107
|
-
* @param item item to process
|
|
108
|
-
*/
|
|
109
|
-
static computeIndexKey<T extends ModelType>(
|
|
110
|
-
cls: Class<T>,
|
|
111
|
-
idx: IndexConfig<T> | string,
|
|
112
|
-
item: DeepPartial<T> = {},
|
|
113
|
-
config?: ComputeConfig & { separator?: string }
|
|
114
|
-
): { type: string, key: string, sort?: number | Date } {
|
|
115
|
-
const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(config ?? {}), includeSortInFields: false });
|
|
116
|
-
const key = fields.map(({ value }) => value).map(value => `${value}`).join(config?.separator ?? DEFAULT_SEP);
|
|
117
|
-
const indexConfig = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
|
|
118
|
-
return !sorted ? { type: indexConfig.type, key } : { type: indexConfig.type, key, sort: sorted.value };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Naive upsert by index
|
|
123
|
-
* @param service
|
|
124
|
-
* @param cls
|
|
125
|
-
* @param idx
|
|
126
|
-
* @param body
|
|
127
|
-
*/
|
|
128
|
-
static async naiveUpsert<T extends ModelType>(
|
|
129
|
-
service: ModelIndexedSupport & ModelCrudSupport,
|
|
130
|
-
cls: Class<T>, idx: string, body: OptionalId<T>
|
|
131
|
-
): Promise<T> {
|
|
132
|
-
try {
|
|
133
|
-
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
134
|
-
body.id = id;
|
|
135
|
-
return await service.update(cls, castTo(body));
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (error instanceof NotFoundError) {
|
|
138
|
-
return await service.create(cls, body);
|
|
139
|
-
} else {
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
package/support/test/indexed.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
|
|
3
|
-
import { Suite, Test } from '@travetto/test';
|
|
4
|
-
import { Schema } from '@travetto/schema';
|
|
5
|
-
import { TimeUtil } from '@travetto/runtime';
|
|
6
|
-
|
|
7
|
-
import { Index, Model } from '../../src/registry/decorator.ts';
|
|
8
|
-
import type { ModelIndexedSupport } from '../../src/types/indexed.ts';
|
|
9
|
-
import { NotFoundError } from '../../src/error/not-found.ts';
|
|
10
|
-
import { IndexNotSupported } from '../../src/error/invalid-index.ts';
|
|
11
|
-
|
|
12
|
-
import { BaseModelSuite } from './base.ts';
|
|
13
|
-
|
|
14
|
-
@Model('index_user')
|
|
15
|
-
@Index({
|
|
16
|
-
name: 'userName',
|
|
17
|
-
type: 'unsorted',
|
|
18
|
-
fields: [{ name: 1 }]
|
|
19
|
-
})
|
|
20
|
-
class User {
|
|
21
|
-
id: string;
|
|
22
|
-
name: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Model('index_user_2')
|
|
26
|
-
class User2 {
|
|
27
|
-
id: string;
|
|
28
|
-
name: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
@Model()
|
|
32
|
-
@Index({ type: 'sorted', name: 'userAge', fields: [{ name: 1 }, { age: 1 }] })
|
|
33
|
-
class User3 {
|
|
34
|
-
id: string;
|
|
35
|
-
name: string;
|
|
36
|
-
age: number;
|
|
37
|
-
color?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@Schema()
|
|
41
|
-
class Child {
|
|
42
|
-
name: string;
|
|
43
|
-
age: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@Model()
|
|
47
|
-
@Index({ type: 'sorted', name: 'childAge', fields: [{ child: { name: 1 } }, { child: { age: 1 } }] })
|
|
48
|
-
@Index({ type: 'sorted', name: 'nameCreated', fields: [{ child: { name: 1 } }, { createdDate: 1 }] })
|
|
49
|
-
class User4 {
|
|
50
|
-
id: string;
|
|
51
|
-
createdDate?: Date = new Date();
|
|
52
|
-
color: string;
|
|
53
|
-
child: Child;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@Suite()
|
|
57
|
-
export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSupport> {
|
|
58
|
-
@Test()
|
|
59
|
-
async writeAndRead() {
|
|
60
|
-
const service = await this.service;
|
|
61
|
-
|
|
62
|
-
await service.create(User, User.from({ name: 'bob1' }));
|
|
63
|
-
await service.create(User, User.from({ name: 'bob2' }));
|
|
64
|
-
|
|
65
|
-
const found1 = await service.getByIndex(User, 'userName', {
|
|
66
|
-
name: 'bob1'
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
assert(found1.name === 'bob1');
|
|
70
|
-
|
|
71
|
-
const found2 = await service.getByIndex(User, 'userName', {
|
|
72
|
-
name: 'bob2'
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
assert(found2.name === 'bob2');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@Test()
|
|
79
|
-
async readMissingIndex() {
|
|
80
|
-
const service = await this.service;
|
|
81
|
-
await assert.rejects(() => service.getByIndex(User, 'missing', {}), NotFoundError);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
@Test()
|
|
85
|
-
async readMissingValue() {
|
|
86
|
-
const service = await this.service;
|
|
87
|
-
await assert.rejects(() => service.getByIndex(User, 'userName', { name: 'jim' }), NotFoundError);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@Test()
|
|
91
|
-
async readDifferentType() {
|
|
92
|
-
const service = await this.service;
|
|
93
|
-
await assert.rejects(() => service.getByIndex(User2, 'userName', { name: 'jim' }), NotFoundError);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@Test()
|
|
97
|
-
async queryMultiple() {
|
|
98
|
-
const service = await this.service;
|
|
99
|
-
|
|
100
|
-
await service.create(User3, User3.from({ name: 'bob', age: 20 }));
|
|
101
|
-
await service.create(User3, User3.from({ name: 'bob', age: 30, color: 'green' }));
|
|
102
|
-
|
|
103
|
-
const found = await service.getByIndex(User3, 'userAge', { name: 'bob', age: 30 });
|
|
104
|
-
|
|
105
|
-
assert(found.color === 'green');
|
|
106
|
-
|
|
107
|
-
const found2 = await service.getByIndex(User3, 'userAge', { name: 'bob', age: 20 });
|
|
108
|
-
|
|
109
|
-
assert(!found2.color);
|
|
110
|
-
|
|
111
|
-
await assert.rejects(() => service.getByIndex(User3, 'userAge', { name: 'bob' }));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@Test()
|
|
115
|
-
async queryList() {
|
|
116
|
-
const service = await this.service;
|
|
117
|
-
|
|
118
|
-
await service.create(User3, User3.from({ name: 'bob', age: 40, color: 'blue' }));
|
|
119
|
-
await service.create(User3, User3.from({ name: 'bob', age: 30, color: 'red' }));
|
|
120
|
-
await service.create(User3, User3.from({ name: 'bob', age: 50, color: 'green' }));
|
|
121
|
-
|
|
122
|
-
const arr = await this.toArray(service.listByIndex(User3, 'userAge', { name: 'bob' }));
|
|
123
|
-
|
|
124
|
-
assert(arr[0].color === 'red' && arr[0].name === 'bob');
|
|
125
|
-
assert(arr[1].color === 'blue' && arr[1].name === 'bob');
|
|
126
|
-
assert(arr[2].color === 'green' && arr[2].name === 'bob');
|
|
127
|
-
|
|
128
|
-
await assert.rejects(() => this.toArray(service.listByIndex(User3, 'userAge', {})), IndexNotSupported);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@Test()
|
|
132
|
-
async queryDeepList() {
|
|
133
|
-
const service = await this.service;
|
|
134
|
-
|
|
135
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, color: 'blue' }));
|
|
136
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, color: 'red' }));
|
|
137
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, color: 'green' }));
|
|
138
|
-
|
|
139
|
-
const arr = await this.toArray(service.listByIndex(User4, 'childAge', User4.from({ child: { name: 'bob' } })));
|
|
140
|
-
assert(arr[0].color === 'red' && arr[0].child.name === 'bob' && arr[0].child.age === 30);
|
|
141
|
-
assert(arr[1].color === 'blue' && arr[1].child.name === 'bob' && arr[1].child.age === 40);
|
|
142
|
-
assert(arr[2].color === 'green' && arr[2].child.name === 'bob' && arr[2].child.age === 50);
|
|
143
|
-
|
|
144
|
-
await assert.rejects(() => this.toArray(service.listByIndex(User4, 'childAge', {})), IndexNotSupported);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@Test()
|
|
148
|
-
async queryComplexDateList() {
|
|
149
|
-
const service = await this.service;
|
|
150
|
-
|
|
151
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, createdDate: TimeUtil.fromNow('3d'), color: 'blue' }));
|
|
152
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.fromNow('2d'), color: 'red' }));
|
|
153
|
-
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.fromNow('-1d'), color: 'green' }));
|
|
154
|
-
|
|
155
|
-
const arr = await this.toArray(service.listByIndex(User4, 'nameCreated', { child: { name: 'bob' } }));
|
|
156
|
-
|
|
157
|
-
assert(arr[0].color === 'green' && arr[0].child.name === 'bob' && arr[0].child.age === 50);
|
|
158
|
-
assert(arr[1].color === 'red' && arr[1].child.name === 'bob' && arr[1].child.age === 30);
|
|
159
|
-
assert(arr[2].color === 'blue' && arr[2].child.name === 'bob' && arr[2].child.age === 40);
|
|
160
|
-
|
|
161
|
-
await assert.rejects(() => this.toArray(service.listByIndex(User4, 'nameCreated', {})), IndexNotSupported);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
@Test()
|
|
165
|
-
async upsertByIndex() {
|
|
166
|
-
const service = await this.service;
|
|
167
|
-
|
|
168
|
-
const user1 = await service.upsertByIndex(User4, 'childAge', { child: { name: 'bob', age: 40 }, color: 'blue' });
|
|
169
|
-
const user2 = await service.upsertByIndex(User4, 'childAge', { child: { name: 'bob', age: 40 }, color: 'green' });
|
|
170
|
-
const user3 = await service.upsertByIndex(User4, 'childAge', { child: { name: 'bob', age: 40 }, color: 'red' });
|
|
171
|
-
|
|
172
|
-
const arr = await this.toArray(service.listByIndex(User4, 'childAge', { child: { name: 'bob' } }));
|
|
173
|
-
assert(arr.length === 1);
|
|
174
|
-
|
|
175
|
-
assert(user1.id === user2.id);
|
|
176
|
-
assert(user2.id === user3.id);
|
|
177
|
-
assert(user1.color === 'blue');
|
|
178
|
-
assert(user3.color === 'red');
|
|
179
|
-
|
|
180
|
-
const user4 = await service.upsertByIndex(User4, 'childAge', { child: { name: 'bob', age: 30 }, color: 'red' });
|
|
181
|
-
const arr2 = await this.toArray(service.listByIndex(User4, 'childAge', { child: { name: 'bob' } }));
|
|
182
|
-
assert(arr2.length === 2);
|
|
183
|
-
|
|
184
|
-
await service.deleteByIndex(User4, 'childAge', user1);
|
|
185
|
-
|
|
186
|
-
const arr3 = await this.toArray(service.listByIndex(User4, 'childAge', { child: { name: 'bob' } }));
|
|
187
|
-
assert(arr3.length === 1);
|
|
188
|
-
assert(arr3[0].id === user4.id);
|
|
189
|
-
}
|
|
190
|
-
}
|