@travetto/model 8.0.0-alpha.2 → 8.0.0-alpha.20

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.
@@ -76,7 +76,7 @@ export interface ModelBasicSupport<C = unknown> {
76
76
  ```
77
77
 
78
78
  ### CRUD
79
- The [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11) contract, builds upon the basic contract, and is built around the idea of simple data retrieval and storage, to create a foundation for other services that need only basic support. The model extension in [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework"), is an example of a module that only needs create, read and delete, and so any implementation of [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") that honors this contract, can be used with the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") model extension.
79
+ The [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11) contract, builds upon the basic contract, and is built around the idea of simple data retrieval and storage, to create a foundation for other services that need only basic support. The model extension in [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication support for the Travetto framework"), is an example of a module that only needs create, read and delete, and so any implementation of [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") that honors this contract, can be used with the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication support for the Travetto framework") model extension.
80
80
 
81
81
  **Code: Crud Contract**
82
82
  ```typescript
@@ -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>): AsyncIterable<T>;
120
+ list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
116
121
  }
117
122
  ```
118
123
 
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
- ```
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.
@@ -213,12 +180,13 @@ export interface ModelBlobSupport {
213
180
  updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
214
181
 
215
182
  /**
216
- * Produces an externally usable URL for sharing limited read access to a specific resource
183
+ * Produces an externally usable URL for sharing limited read access to a specific resource.
184
+ * If expiresIn is explicitly set to false, returns a direct/public URL.
217
185
  *
218
186
  * @param location The asset location to read from
219
- * @param expiresIn Expiry
187
+ * @param expiresIn Expiry or false for public/direct URL
220
188
  */
221
- getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
189
+ getBlobReadUrl?(location: string, expiresIn?: TimeSpan | false): Promise<string>;
222
190
 
223
191
  /**
224
192
  * Produces an externally usable URL for sharing allowing direct write access
@@ -302,8 +270,8 @@ export abstract class BaseModelSuite<T> {
302
270
  const svc = (await this.service);
303
271
  if (ModelCrudUtil.isSupported(svc)) {
304
272
  let i = 0;
305
- for await (const __el of svc.list(cls)) {
306
- i += 1;
273
+ for await (const batch of svc.list(cls)) {
274
+ i += batch.length;
307
275
  }
308
276
  return i;
309
277
  } else {
@@ -332,12 +300,12 @@ export abstract class BaseModelSuite<T> {
332
300
  return DependencyRegistryIndex.getInstance(this.serviceClass);
333
301
  }
334
302
 
335
- async toArray<U>(src: AsyncIterable<U> | AsyncGenerator<U>): Promise<U[]> {
336
- const out: U[] = [];
303
+ async toArray<U>(src: AsyncIterable<U | U[]> | AsyncGenerator<U | U[]>): Promise<U[]> {
304
+ const out: (U | U[])[] = [];
337
305
  for await (const el of src) {
338
306
  out.push(el);
339
307
  }
340
- return out;
308
+ return castTo(out.flat());
341
309
  }
342
310
  }
343
311
  ```
@@ -345,16 +313,22 @@ export abstract class BaseModelSuite<T> {
345
313
  ## CLI - model:export
346
314
  The module provides the ability to generate an export of the model structure from all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14)s within the application. This is useful for being able to generate the appropriate files to manually create the data schemas in production.
347
315
 
348
- **Terminal: Running model export**
316
+ **Terminal: Help for model:export**
349
317
  ```bash
350
318
  $ trv model:export --help
351
319
 
352
320
  Usage: model:export [options] <provider:string> <models...:string>
353
321
 
322
+ Description:
323
+ Export model definitions for a selected provider and model set.
324
+
325
+ The command resolves candidate models and delegates to provider-specific
326
+ export logic to produce schema/install artifacts.
327
+
354
328
  Options:
355
329
  -p, --profile <string> Application profiles
356
330
  -m, --module <module> Module to run for
357
- -h, --help display help for command
331
+ --help display help for command
358
332
 
359
333
  Providers
360
334
  --------------------
@@ -363,21 +337,29 @@ Providers
363
337
  Models
364
338
  --------------------
365
339
  * samplemodel
340
+
341
+ Examples:
366
342
  ```
367
343
 
368
344
  ## CLI - model:install
369
345
  The module provides the ability to install all the various [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14)s within the application given the current configuration being targeted. This is useful for being able to prepare the datastore manually.
370
346
 
371
- **Terminal: Running model install**
347
+ **Terminal: Help for model:install**
372
348
  ```bash
373
349
  $ trv model:install --help
374
350
 
375
351
  Usage: model:install [options] <provider:string> <models...:string>
376
352
 
353
+ Description:
354
+ Install or update model definitions for a selected provider.
355
+
356
+ The command resolves candidate models and applies provider install/upsert
357
+ operations so backing stores are prepared for runtime usage.
358
+
377
359
  Options:
378
360
  -p, --profile <string> Application profiles
379
361
  -m, --module <module> Module to run for
380
- -h, --help display help for command
362
+ --help display help for command
381
363
 
382
364
  Providers
383
365
  --------------------
@@ -386,4 +368,6 @@ Providers
386
368
  Models
387
369
  --------------------
388
370
  * samplemodel
371
+
372
+ Examples:
389
373
  ```
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.2",
3
+ "version": "8.0.0-alpha.20",
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.2",
31
- "@travetto/di": "^8.0.0-alpha.2",
32
- "@travetto/registry": "^8.0.0-alpha.2",
33
- "@travetto/schema": "^8.0.0-alpha.2"
30
+ "@travetto/config": "^8.0.0-alpha.19",
31
+ "@travetto/di": "^8.0.0-alpha.18",
32
+ "@travetto/registry": "^8.0.0-alpha.18",
33
+ "@travetto/schema": "^8.0.0-alpha.19"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^8.0.0-alpha.3",
37
- "@travetto/test": "^8.0.0-alpha.2"
36
+ "@travetto/cli": "^8.0.0-alpha.25",
37
+ "@travetto/test": "^8.0.0-alpha.18"
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
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Class } from '@travetto/runtime';
2
- import type { ModelIdSource, ModelType, OptionalId } from '../types/model.ts';
2
+ import type { ModelIdSource, ModelType, OptionalId } from './model.ts';
3
3
 
4
4
  /**
5
5
  * Interface for basic data interface
package/src/types/blob.ts CHANGED
@@ -42,12 +42,13 @@ export interface ModelBlobSupport {
42
42
  updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void>;
43
43
 
44
44
  /**
45
- * Produces an externally usable URL for sharing limited read access to a specific resource
45
+ * Produces an externally usable URL for sharing limited read access to a specific resource.
46
+ * If expiresIn is explicitly set to false, returns a direct/public URL.
46
47
  *
47
48
  * @param location The asset location to read from
48
- * @param expiresIn Expiry
49
+ * @param expiresIn Expiry or false for public/direct URL
49
50
  */
50
- getBlobReadUrl?(location: string, expiresIn?: TimeSpan): Promise<string>;
51
+ getBlobReadUrl?(location: string, expiresIn?: TimeSpan | false): Promise<string>;
51
52
 
52
53
  /**
53
54
  * Produces an externally usable URL for sharing allowing direct write access
package/src/types/bulk.ts CHANGED
@@ -2,7 +2,7 @@ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
  import type { ValidationError, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import type { ModelCrudSupport } from './crud.ts';
5
- import type { ModelType, OptionalId } from '../types/model.ts';
5
+ import type { ModelType, OptionalId } from './model.ts';
6
6
 
7
7
  /**
8
8
  * Bulk operation. Each operation has a single action and payload
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 './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>): AsyncIterable<T>;
48
+ list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]>;
44
49
  }
@@ -1,6 +1,6 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import type { ModelType } from '../types/model.ts';
3
+ import type { ModelType } from './model.ts';
4
4
  import type { ModelCrudSupport } from './crud.ts';
5
5
 
6
6
  /**
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import type { ModelType } from '../types/model.ts';
3
+ import type { ModelType } from './model.ts';
4
4
 
5
5
  /**
6
6
  * This interface defines the behavior for dealing with the
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
 
@@ -6,7 +6,10 @@ import { ModelExportUtil } from './bin/export.ts';
6
6
  import { ModelCandidateUtil } from './bin/candidate.ts';
7
7
 
8
8
  /**
9
- * Exports model schemas
9
+ * Export model definitions for a selected provider and model set.
10
+ *
11
+ * The command resolves candidate models and delegates to provider-specific
12
+ * export logic to produce schema/install artifacts.
10
13
  */
11
14
  @CliCommand()
12
15
  export class ModelExportCommand extends BaseModelCommand {
@@ -6,7 +6,10 @@ import { ModelInstallUtil } from './bin/install.ts';
6
6
  import { ModelCandidateUtil } from './bin/candidate.ts';
7
7
 
8
8
  /**
9
- * Installing models
9
+ * Install or update model definitions for a selected provider.
10
+ *
11
+ * The command resolves candidate models and applies provider install/upsert
12
+ * operations so backing stores are prepared for runtime usage.
10
13
  */
11
14
  @CliCommand()
12
15
  export class ModelInstallCommand extends BaseModelCommand {
@@ -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
  };
@@ -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 __el of svc.list(cls)) {
26
- i += 1;
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
  }
@@ -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,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, 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 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 collect(service.list(Engineer));
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 collect(service.list(Worker));
100
+ const all2 = await this.toArray(service.list(Worker));
138
101
  assert(all2.length === 4);
139
102
 
140
- const engineers2 = await collect(service.list(Engineer));
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
  }
@@ -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
- }