@travetto/model 8.0.0-alpha.1 → 8.0.0-alpha.11
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 +17 -50
- 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/types/crud.ts +8 -3
- package/src/types/model.ts +10 -1
- package/src/util/crud.ts +15 -6
- package/support/doc.support.tsx +0 -2
- package/support/test/base.ts +5 -5
- package/support/test/blob.ts +1 -1
- package/support/test/crud.ts +39 -5
- package/support/test/polymorphism.ts +6 -114
- 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.
|
|
@@ -110,51 +110,18 @@ export interface ModelCrudSupport extends ModelBasicSupport {
|
|
|
110
110
|
updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T>;
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* List all items
|
|
113
|
+
* List all items of a collection, results returned in batches of items.
|
|
114
|
+
*
|
|
115
|
+
* Note: Batch size hint can be used to optimize batch size, but is not guaranteed.
|
|
116
|
+
*
|
|
117
|
+
* @param cls The class to list
|
|
118
|
+
* @param options Options for listing
|
|
114
119
|
*/
|
|
115
|
-
list<T extends ModelType>(cls: Class<T
|
|
120
|
+
list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
|
|
116
121
|
}
|
|
117
122
|
```
|
|
118
123
|
|
|
119
|
-
|
|
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
|
-
```
|
|
124
|
+
The `list` operation returns batches of model records as an async stream. It also accepts listing options such as `limit` to cap how many records are produced, alongside other runtime controls such as abort signals and batch size hints.
|
|
158
125
|
|
|
159
126
|
### Expiry
|
|
160
127
|
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.
|
|
@@ -302,8 +269,8 @@ export abstract class BaseModelSuite<T> {
|
|
|
302
269
|
const svc = (await this.service);
|
|
303
270
|
if (ModelCrudUtil.isSupported(svc)) {
|
|
304
271
|
let i = 0;
|
|
305
|
-
for await (const
|
|
306
|
-
i +=
|
|
272
|
+
for await (const batch of svc.list(cls)) {
|
|
273
|
+
i += batch.length;
|
|
307
274
|
}
|
|
308
275
|
return i;
|
|
309
276
|
} else {
|
|
@@ -332,12 +299,12 @@ export abstract class BaseModelSuite<T> {
|
|
|
332
299
|
return DependencyRegistryIndex.getInstance(this.serviceClass);
|
|
333
300
|
}
|
|
334
301
|
|
|
335
|
-
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
336
|
-
const out: U[] = [];
|
|
302
|
+
async toArray<U>(src: AsyncIterable<U | U[]> | AsyncGenerator<U | U[]>): Promise<U[]> {
|
|
303
|
+
const out: (U | U[])[] = [];
|
|
337
304
|
for await (const el of src) {
|
|
338
305
|
out.push(el);
|
|
339
306
|
}
|
|
340
|
-
return out;
|
|
307
|
+
return castTo(out.flat());
|
|
341
308
|
}
|
|
342
309
|
}
|
|
343
310
|
```
|
|
@@ -354,7 +321,7 @@ Usage: model:export [options] <provider:string> <models...:string>
|
|
|
354
321
|
Options:
|
|
355
322
|
-p, --profile <string> Application profiles
|
|
356
323
|
-m, --module <module> Module to run for
|
|
357
|
-
|
|
324
|
+
--help display help for command
|
|
358
325
|
|
|
359
326
|
Providers
|
|
360
327
|
--------------------
|
|
@@ -377,7 +344,7 @@ Usage: model:install [options] <provider:string> <models...:string>
|
|
|
377
344
|
Options:
|
|
378
345
|
-p, --profile <string> Application profiles
|
|
379
346
|
-m, --module <module> Module to run for
|
|
380
|
-
|
|
347
|
+
--help display help for command
|
|
381
348
|
|
|
382
349
|
Providers
|
|
383
350
|
--------------------
|
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.11",
|
|
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.11",
|
|
31
|
+
"@travetto/di": "^8.0.0-alpha.11",
|
|
32
|
+
"@travetto/registry": "^8.0.0-alpha.11",
|
|
33
|
+
"@travetto/schema": "^8.0.0-alpha.11"
|
|
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.16",
|
|
37
|
+
"@travetto/test": "^8.0.0-alpha.11"
|
|
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/types/crud.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { ModelType, OptionalId } from '../types/model.ts';
|
|
3
|
+
import type { ModelListOptions, ModelType, OptionalId } from '../types/model.ts';
|
|
4
4
|
|
|
5
5
|
import type { ModelBasicSupport } from './basic.ts';
|
|
6
6
|
|
|
@@ -38,7 +38,12 @@ export interface ModelCrudSupport extends ModelBasicSupport {
|
|
|
38
38
|
updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T>;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* List all items
|
|
41
|
+
* List all items of a collection, results returned in batches of items.
|
|
42
|
+
*
|
|
43
|
+
* Note: Batch size hint can be used to optimize batch size, but is not guaranteed.
|
|
44
|
+
*
|
|
45
|
+
* @param cls The class to list
|
|
46
|
+
* @param options Options for listing
|
|
42
47
|
*/
|
|
43
|
-
list<T extends ModelType>(cls: Class<T
|
|
48
|
+
list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
|
|
44
49
|
}
|
package/src/types/model.ts
CHANGED
|
@@ -16,4 +16,13 @@ export interface ModelType {
|
|
|
16
16
|
id: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
|
|
19
|
+
export type OptionalId<T extends ModelType> = Omit<T, 'id'> & { id?: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for listing items
|
|
23
|
+
*/
|
|
24
|
+
export interface ModelListOptions {
|
|
25
|
+
abort?: AbortSignal;
|
|
26
|
+
limit?: number;
|
|
27
|
+
batchSizeHint?: number;
|
|
28
|
+
}
|
package/src/util/crud.ts
CHANGED
|
@@ -34,6 +34,21 @@ export class ModelCrudUtil {
|
|
|
34
34
|
return { create, valid };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
static async filterOutNotFound<T extends ModelType>(actions: Promise<T>[] | undefined): Promise<T[]> {
|
|
38
|
+
if (!actions) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return (await Promise.allSettled(actions)).map(p => {
|
|
42
|
+
if (p.status === 'fulfilled') {
|
|
43
|
+
return p.value;
|
|
44
|
+
} else if (p.reason instanceof NotFoundError) {
|
|
45
|
+
return undefined!;
|
|
46
|
+
} else {
|
|
47
|
+
throw p.reason;
|
|
48
|
+
}
|
|
49
|
+
}).filter(item => !!item);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
/**
|
|
38
53
|
* Load model
|
|
39
54
|
* @param cls Class to load model for
|
|
@@ -117,9 +132,6 @@ export class ModelCrudUtil {
|
|
|
117
132
|
item = await handler(item) ?? item;
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
|
-
if (typeof item === 'object' && item && 'prePersist' in item && typeof item['prePersist'] === 'function') {
|
|
121
|
-
item = await item.prePersist() ?? item;
|
|
122
|
-
}
|
|
123
135
|
return item;
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -131,9 +143,6 @@ export class ModelCrudUtil {
|
|
|
131
143
|
for (const handler of castTo<DataHandler<T>[]>(config.postLoad ?? [])) {
|
|
132
144
|
item = await handler(item) ?? item;
|
|
133
145
|
}
|
|
134
|
-
if (typeof item === 'object' && item && 'postLoad' in item && typeof item['postLoad'] === 'function') {
|
|
135
|
-
item = await item.postLoad() ?? item;
|
|
136
|
-
}
|
|
137
146
|
return item;
|
|
138
147
|
}
|
|
139
148
|
|
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/base.ts
CHANGED
|
@@ -22,8 +22,8 @@ export abstract class BaseModelSuite<T> {
|
|
|
22
22
|
const svc = (await this.service);
|
|
23
23
|
if (ModelCrudUtil.isSupported(svc)) {
|
|
24
24
|
let i = 0;
|
|
25
|
-
for await (const
|
|
26
|
-
i +=
|
|
25
|
+
for await (const batch of svc.list(cls)) {
|
|
26
|
+
i += batch.length;
|
|
27
27
|
}
|
|
28
28
|
return i;
|
|
29
29
|
} else {
|
|
@@ -52,11 +52,11 @@ export abstract class BaseModelSuite<T> {
|
|
|
52
52
|
return DependencyRegistryIndex.getInstance(this.serviceClass);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
|
|
56
|
-
const out: U[] = [];
|
|
55
|
+
async toArray<U>(src: AsyncIterable<U | U[]> | AsyncGenerator<U | U[]>): Promise<U[]> {
|
|
56
|
+
const out: (U | U[])[] = [];
|
|
57
57
|
for await (const el of src) {
|
|
58
58
|
out.push(el);
|
|
59
59
|
}
|
|
60
|
-
return out;
|
|
60
|
+
return castTo(out.flat());
|
|
61
61
|
}
|
|
62
62
|
}
|
package/support/test/blob.ts
CHANGED
|
@@ -92,7 +92,7 @@ export abstract class ModelBlobSuite extends BaseModelSuite<ModelBlobSupport> {
|
|
|
92
92
|
const range = BinaryMetadataUtil.enforceRange({ start: 10, end: 20 }, partialMeta);
|
|
93
93
|
assert(subContent.length === (range.end - range.start) + 1);
|
|
94
94
|
|
|
95
|
-
const og = await this.fixture.
|
|
95
|
+
const og = await this.fixture.readUTF8('/text.txt');
|
|
96
96
|
|
|
97
97
|
assert(subContent === og.substring(10, 21));
|
|
98
98
|
|
package/support/test/crud.ts
CHANGED
|
@@ -3,7 +3,7 @@ import timers from 'node:timers/promises';
|
|
|
3
3
|
|
|
4
4
|
import { Suite, Test } from '@travetto/test';
|
|
5
5
|
import { Schema, Text, Precision, Required, } from '@travetto/schema';
|
|
6
|
-
import { type ModelCrudSupport, Model, NotFoundError, PersistValue } from '@travetto/model';
|
|
6
|
+
import { type ModelCrudSupport, Model, NotFoundError, PersistValue, PrePersist } from '@travetto/model';
|
|
7
7
|
|
|
8
8
|
import { BaseModelSuite } from './base.ts';
|
|
9
9
|
|
|
@@ -42,14 +42,13 @@ class SimpleList {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
@Model()
|
|
45
|
+
@PrePersist((item) => {
|
|
46
|
+
item.name = `${item.name}-suffix`;
|
|
47
|
+
})
|
|
45
48
|
class User2 {
|
|
46
49
|
id: string;
|
|
47
50
|
address?: Address;
|
|
48
51
|
name: string;
|
|
49
|
-
|
|
50
|
-
prePersist() {
|
|
51
|
-
this.name = `${this.name}-suffix`;
|
|
52
|
-
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
@Model()
|
|
@@ -77,6 +76,8 @@ class BigIntModel {
|
|
|
77
76
|
@Suite()
|
|
78
77
|
export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
79
78
|
|
|
79
|
+
indexLimitSkew = 0;
|
|
80
|
+
|
|
80
81
|
@Test('save it')
|
|
81
82
|
async save() {
|
|
82
83
|
const service = await this.service;
|
|
@@ -268,6 +269,39 @@ export abstract class ModelCrudSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
|
268
269
|
assert(found[2].age === people[2].age);
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
@Test('verify list abort signal')
|
|
273
|
+
async listAbortSignal() {
|
|
274
|
+
const service = await this.service;
|
|
275
|
+
|
|
276
|
+
await Promise.all(
|
|
277
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => service.upsert(Person, Person.from({
|
|
278
|
+
id: service.idSource.create(),
|
|
279
|
+
name: 'Bob',
|
|
280
|
+
age: 20 + x,
|
|
281
|
+
gender: 'm',
|
|
282
|
+
address: {
|
|
283
|
+
street1: 'a',
|
|
284
|
+
...(x === 1 ? { street2: 'b' } : {})
|
|
285
|
+
}
|
|
286
|
+
})))
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const controller = new AbortController();
|
|
290
|
+
const found: Person[] = [];
|
|
291
|
+
|
|
292
|
+
for await (const items of service.list(Person, { abort: controller.signal, batchSizeHint: 1 })) {
|
|
293
|
+
found.push(...items);
|
|
294
|
+
controller.abort();
|
|
295
|
+
await timers.setTimeout(10);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.indexLimitSkew) {
|
|
299
|
+
assert(found.length > 0 && found.length < this.indexLimitSkew);
|
|
300
|
+
} else {
|
|
301
|
+
assert(found.length === 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
271
305
|
@Test('save it')
|
|
272
306
|
async verifyRaw() {
|
|
273
307
|
const service = await this.service;
|
|
@@ -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 this.toArray(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 this.toArray(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 this.toArray(service.list(Worker));
|
|
138
101
|
assert(all2.length === 4);
|
|
139
102
|
|
|
140
|
-
const engineers2 = await
|
|
103
|
+
const engineers2 = await this.toArray(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/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
|
-
}
|