@travetto/model 8.0.0-alpha.1 → 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 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#L16).
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), [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.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).
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
- -h, --help display help for command
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
- -h, --help display help for command
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.1",
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.1",
31
- "@travetto/di": "^8.0.0-alpha.1",
32
- "@travetto/registry": "^8.0.0-alpha.1",
33
- "@travetto/schema": "^8.0.0-alpha.1"
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.1",
37
- "@travetto/test": "^8.0.0-alpha.1"
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<T>, message: string = '') {
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 { RuntimeError, castTo, type Class, getClass } from '@travetto/runtime';
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, IndexConfig, ModelConfig, PrePersistScope } from './types.ts';
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: [...(target.indices || []), ...(source.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, IndexType, ModelConfig } from './types.ts';
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, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
37
- return this.#instance.getIndices(cls, supportedTypes);
38
- }
39
-
40
- static getIndex<T extends ModelType, K extends IndexType[]>(cls: Class<T>, name: string, supportedTypes?: K): IndexResult<T, K> {
41
- return this.#instance.getIndex(cls, name, supportedTypes);
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, K extends IndexType[]>(cls: Class<T>, supportedTypes?: K): IndexResult<T, K>[] {
115
- return (this.getConfig(cls).indices ?? []).filter((idx): idx is IndexConfig<T> => !supportedTypes || supportedTypes.includes(idx.type));
92
+ getIndices<T extends ModelType>(cls: Class<T>): IndexConfig[] {
93
+ return Object.values(this.getConfig(cls).indices ?? []);
116
94
  }
117
95
 
118
96
  /**
@@ -1,23 +1,29 @@
1
- import type { Class, Primitive, ValidFields } from '@travetto/runtime';
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<T>[];
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
+ }
@@ -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
  };
@@ -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.readText('/text.txt');
95
+ const og = await this.fixture.readUTF8('/text.txt');
96
96
 
97
97
  assert(subContent === og.substring(10, 21));
98
98
 
@@ -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, Discriminated } from '@travetto/schema';
6
+ import { Schema, DiscriminatorField, Text, TypeMismatchError } from '@travetto/schema';
7
7
  import {
8
- type ModelIndexedSupport, Index, type ModelCrudSupport, Model,
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 collect(service.list(Worker));
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 collect(service.list(Engineer));
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 collect(service.list(Worker));
100
+ const all2 = await Array.fromAsync(service.list(Worker));
138
101
  assert(all2.length === 4);
139
102
 
140
- const engineers2 = await collect(service.list(Engineer));
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
  }
@@ -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
- }
@@ -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
- }
@@ -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
- }