@travetto/model-indexed 8.0.0-alpha.10 → 8.0.0-alpha.12

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
@@ -136,7 +136,7 @@ export const specificOrders = keyedIndex(Order, {
136
136
  ```
137
137
 
138
138
  ## Using Indexes
139
- Model services that implement [ModelIndexedSupport](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L23) allow you to query using the indexes you've defined.
139
+ Model services that implement [ModelIndexedSupport](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L15) allow you to query using the indexes you've defined.
140
140
 
141
141
  ### Service Interface
142
142
 
@@ -204,17 +204,36 @@ export interface ModelIndexedSupport extends ModelBasicSupport {
204
204
  >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T>;
205
205
 
206
206
  /**
207
- * List entity by ranged index as defined by fields of idx
207
+ * Page through entities by ranged index as defined by fields of idx
208
+ *
209
+ * Note: Limit is generally honored, but can vary depending on the underlying storage implementation.
210
+ *
211
+ * @param cls The type to search by
212
+ * @param idx The index to search against
213
+ * @param body The payload of fields needed to search
214
+ * @param options The configuration for pagination
215
+ */
216
+ pageByIndex<
217
+ T extends ModelType,
218
+ S extends SortedIndexSelection<T>,
219
+ K extends KeyedIndexSelection<T>
220
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ModelPageOptions): Promise<ModelPageResult<T>>;
221
+
222
+ /**
223
+ * List all entities by ranged index as defined by fields of idx
224
+ *
225
+ * Note: Limit is generally honored, but can vary depending on the underlying storage implementation.
226
+ * Batch size hint can be used to optimize batch size, but is not guaranteed.
227
+ *
208
228
  * @param cls The type to search by
209
229
  * @param idx The index to search against
210
230
  * @param body The payload of fields needed to search
211
- * @param options The configuration for listing
212
231
  */
213
232
  listByIndex<
214
233
  T extends ModelType,
215
234
  S extends SortedIndexSelection<T>,
216
235
  K extends KeyedIndexSelection<T>
217
- >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ListPageOptions): Promise<ListPageResult<T>>;
236
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ModelListOptions): AsyncIterable<T[]>;
218
237
  }
219
238
  ```
220
239
 
@@ -224,41 +243,63 @@ The service provides these operations:
224
243
  * `upsertByIndex` — Insert or update by index
225
244
  * `updateByIndex` — Update an existing item by index
226
245
  * `updatePartialByIndex` — Partially update an item by index
227
- * `listByIndex` — List items with pagination and sorting
246
+ * `pageByIndex` — Fetch a page of items with pagination metadata
247
+ * `listByIndex` — Stream matching items from a sorted index in batches, optionally capped by `limit`
228
248
 
229
249
  ### Getting Items
230
250
  Use `getByIndex` to fetch a single item by providing all required key fields.
231
251
 
232
252
  **Code: Getting by Keyed Index**
233
253
  ```typescript
234
- export async function getExample(modelService: any) {
254
+ export async function getExample(modelService: ModelIndexedSupport) {
235
255
  const user = await modelService.getByIndex(User, userByName, {
236
256
  name: 'John Doe'
237
257
  });
238
258
  return user;
239
259
  }
260
+
261
+ export async function getScopedExample(modelService: ModelIndexedSupport) {
262
+ const user = await modelService.getByIndex(User, userByName, {
263
+ name: 'John Doe',
264
+ id: 'user-123'
265
+ });
266
+ return user;
267
+ }
240
268
  ```
241
269
 
242
- For sorted indexes with key fields, you must provide all key values plus the sort value if using it to identify a specific item.
270
+ For sorted indexes with key fields, you must provide all key values plus the sort value if using it to identify a specific item. All single-item index operations also accept an optional `id` in the request body. This is useful when the index is not unique and you need to ensure the supplied index values resolve to the same record as the provided `id`, such as enforcing a pattern like "userId matches".
271
+
272
+ **Code: Disambiguating with id**
273
+ ```typescript
274
+ export async function getScopedExample(modelService: ModelIndexedSupport) {
275
+ const user = await modelService.getByIndex(User, userByName, {
276
+ name: 'John Doe',
277
+ id: 'user-123'
278
+ });
279
+ return user;
280
+ }
281
+ ```
243
282
 
244
283
  ### Deleting Items
245
284
  Use `deleteByIndex` to remove an item by index.
246
285
 
247
286
  **Code: Deleting by Index**
248
287
  ```typescript
249
- export async function deleteExample(modelService: any) {
288
+ export async function deleteExample(modelService: ModelIndexedSupport) {
250
289
  await modelService.deleteByIndex(User, userByName, {
251
290
  name: 'John Doe'
252
291
  });
253
292
  }
254
293
  ```
255
294
 
295
+ As with `getByIndex`, you can pass an optional `id` to ensure the computed index values resolve to the expected record before deleting it.
296
+
256
297
  ### Upserting Items
257
298
  Use `upsertByIndex` to insert a new item or update an existing one. The index acts as a primary key.
258
299
 
259
300
  **Code: Upserting by Index**
260
301
  ```typescript
261
- export async function upsertExample(modelService: any) {
302
+ export async function upsertExample(modelService: ModelIndexedSupport) {
262
303
  const user = await modelService.upsertByIndex(User, userByName, {
263
304
  id: 'user-1',
264
305
  name: 'John Doe',
@@ -273,7 +314,7 @@ Use `updateByIndex` to update an existing item, or `updatePartialByIndex` for pa
273
314
 
274
315
  **Code: Updating by Index**
275
316
  ```typescript
276
- export async function updateExample(modelService: any) {
317
+ export async function updateExample(modelService: ModelIndexedSupport) {
277
318
  // Full update — all fields required
278
319
  const user = await modelService.updateByIndex(User, userByName, {
279
320
  id: 'user-1',
@@ -284,7 +325,7 @@ export async function updateExample(modelService: any) {
284
325
  return user;
285
326
  }
286
327
 
287
- export async function updatePartialExample(modelService: any) {
328
+ export async function updatePartialExample(modelService: ModelIndexedSupport) {
288
329
  // Partial update — only updated fields required
289
330
  const user = await modelService.updatePartialByIndex(User, userByName, {
290
331
  name: 'John Doe',
@@ -295,12 +336,12 @@ export async function updatePartialExample(modelService: any) {
295
336
  ```
296
337
 
297
338
  ### Listing Items
298
- Use `listByIndex` to fetch multiple items from a sorted index with pagination.
339
+ Use `pageByIndex` when you want paginated access to a sorted index.
299
340
 
300
- **Code: Listing by Sorted Index**
341
+ **Code: Paging by Sorted Index**
301
342
  ```typescript
302
- export async function listExample(modelService: any) {
303
- const result = await modelService.listByIndex(User, recentUsers, {}, {
343
+ export async function listExample(modelService: ModelIndexedSupport) {
344
+ const result = await modelService.pageByIndex(User, recentUsers, {}, {
304
345
  limit: 20,
305
346
  offset: '0'
306
347
  });
@@ -311,13 +352,28 @@ export async function listExample(modelService: any) {
311
352
  }
312
353
  ```
313
354
 
314
- You can also provide key values to filter within a sorted index:
355
+ Use `listByIndex` when you want to iterate through matching items as an async stream of batches. The same list options used by `list` are supported here, including `limit` when you want to stop after a fixed number of records.
356
+
357
+ **Code: Streaming by Sorted Index**
358
+ ```typescript
359
+ export async function listStreamExample(modelService: ModelIndexedSupport) {
360
+ const items: User[] = [];
361
+
362
+ for await (const batch of modelService.listByIndex(User, recentUsers, {}, { limit: 25 })) {
363
+ items.push(...batch);
364
+ }
365
+
366
+ return items;
367
+ }
368
+ ```
369
+
370
+ You can also provide key values to filter within a sorted index with `pageByIndex`:
315
371
 
316
372
  **Code: Listing with Key Filter**
317
373
  ```typescript
318
- export async function listWithFilterExample(modelService: any) {
374
+ export async function listWithFilterExample(modelService: ModelIndexedSupport) {
319
375
  // Get all users named 'John' sorted by age
320
- const result = await modelService.listByIndex(User, usersByNameAge, {
376
+ const result = await modelService.pageByIndex(User, usersByNameAge, {
321
377
  name: 'John'
322
378
  }, {
323
379
  limit: 10
@@ -327,7 +383,7 @@ export async function listWithFilterExample(modelService: any) {
327
383
  ```
328
384
 
329
385
  ## Integration
330
- Index registration happens automatically when models are decorated with [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14). Model services like [Memory Model Support](https://github.com/travetto/travetto/tree/main/module/model-memory#readme "Memory backing for the travetto model module."), [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module."), and [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") implement the [ModelIndexedSupport](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L23) interface to provide indexed access.
386
+ Index registration happens automatically when models are decorated with [@Model](https://github.com/travetto/travetto/tree/main/module/model/src/registry/decorator.ts#L14). Model services like [Memory Model Support](https://github.com/travetto/travetto/tree/main/module/model-memory#readme "Memory backing for the travetto model module."), [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module."), and [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") implement the [ModelIndexedSupport](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L15) interface to provide indexed access.
331
387
 
332
388
  ### Reading Registry Information
333
389
  You can access registered indexes via [ModelRegistryIndex](https://github.com/travetto/travetto/tree/main/module/model/src/registry/registry-index.ts#L12) at runtime:
@@ -335,11 +391,11 @@ You can access registered indexes via [ModelRegistryIndex](https://github.com/tr
335
391
  **Code: Accessing Model Indexes**
336
392
  ```typescript
337
393
  export function registryAccessExample() {
338
- const registry = ModelRegistryIndex.get(User);
394
+ const registry = ModelRegistryIndex.getConfig(User);
339
395
  const indexes = registry.indices; // Map of all indexes for the model
340
396
 
341
397
  // Access a specific index
342
- const userByName = indexes['userByName'];
398
+ const userByName = indexes?.['userByName'];
343
399
  return userByName;
344
400
  }
345
401
  ```
@@ -350,4 +406,4 @@ export function registryAccessExample() {
350
406
  * **Use composite keys** — When filtering by multiple fields, include all of them in a single index
351
407
  * **Leverage sorting** — Use sorted indexes for paginated lists and range queries
352
408
  * **Enforce uniqueness** — Use [uniqueIndex](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/indexes.ts#L47) for fields that must be globally unique
353
- * **Handle errors gracefully** — Catch [IndexedFieldError](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/indexes.ts#L88) when working with user input
409
+ * **Handle errors gracefully** — Catch [IndexedFieldError](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/error.ts#L7) when working with user input
package/__index__.ts CHANGED
@@ -1,4 +1,6 @@
1
+ export * from './src/types/error.ts';
1
2
  export * from './src/types/indexes.ts';
3
+ export * from './src/types/list.ts';
2
4
  export * from './src/types/service.ts';
3
5
  export * from './src/computed.ts';
4
6
  export * from './src/indexes.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-indexed",
3
- "version": "8.0.0-alpha.10",
3
+ "version": "8.0.0-alpha.12",
4
4
  "type": "module",
5
5
  "description": "Basic indexing support for model sources that support it.",
6
6
  "keywords": [
@@ -26,13 +26,13 @@
26
26
  "directory": "module/model-indexed"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/model": "^8.0.0-alpha.10",
30
- "@travetto/registry": "^8.0.0-alpha.10",
31
- "@travetto/schema": "^8.0.0-alpha.10"
29
+ "@travetto/model": "^8.0.0-alpha.11",
30
+ "@travetto/registry": "^8.0.0-alpha.11",
31
+ "@travetto/schema": "^8.0.0-alpha.11"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/cli": "^8.0.0-alpha.15",
35
- "@travetto/test": "^8.0.0-alpha.10"
34
+ "@travetto/cli": "^8.0.0-alpha.16",
35
+ "@travetto/test": "^8.0.0-alpha.11"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@travetto/cli": {
package/src/computed.ts CHANGED
@@ -2,15 +2,16 @@ import type { ModelType } from '@travetto/model';
2
2
  import { castTo, type Any } from '@travetto/runtime';
3
3
 
4
4
  import {
5
- type KeyedIndexSelection, type SortedIndexSelection, type AllIndexes, type KeyedIndexBody, IndexedFieldError,
5
+ type KeyedIndexSelection, type SortedIndexSelection, type AllIndexes, type KeyedIndexBody,
6
6
  type FullKeyedIndexBody, type TemplateValue, type TemplatePart
7
7
  } from './types/indexes.ts';
8
+ import { IndexedFieldError } from './types/error.ts';
8
9
 
9
10
  const DEFAULT_SEP = '\u8203';
10
11
 
11
- type IndexPart<T extends TemplateValue = TemplateValue> = {
12
+ type IndexPart<T extends TemplateValue = TemplateValue, V = unknown> = {
12
13
  state: 'missing' | 'empty' | 'mismatch' | 'found';
13
- value: unknown;
14
+ value: V;
14
15
  path: string[];
15
16
  templateValue: T;
16
17
  };
@@ -29,11 +30,11 @@ function buildIndexParts<T extends TemplateValue = TemplateValue>(
29
30
  if (value && pathItem in value) {
30
31
  value = castTo<Record<string, unknown>>(value)[pathItem];
31
32
  } else {
32
- bodyPart = { value: undefined!, state: 'missing' };
33
+ bodyPart = { value: null, state: 'missing' };
33
34
  break;
34
35
  }
35
36
  } else {
36
- bodyPart = { value: undefined!, state: 'mismatch' };
37
+ bodyPart = { value: castTo(value), state: 'mismatch' };
37
38
  break;
38
39
  }
39
40
  }
@@ -75,6 +76,7 @@ export class ModelIndexedComputedIndex<T extends ModelType> {
75
76
 
76
77
  keyedParts: IndexPart<true>[];
77
78
  sortParts: IndexPart<-1 | 1>[];
79
+ idPart: IndexPart<true, string> | undefined;
78
80
  idx: AllIndexes<T>;
79
81
 
80
82
  constructor(
@@ -84,6 +86,9 @@ export class ModelIndexedComputedIndex<T extends ModelType> {
84
86
  this.idx = idx;
85
87
  this.keyedParts = buildIndexParts(idx.keyTemplate, castTo(body));
86
88
  this.sortParts = buildIndexParts(idx.sortTemplate, castTo(body), value => typeof value === 'number' || value instanceof Date);
89
+ if ('id' in body && typeof body.id === 'string') {
90
+ this.idPart = { path: ['id'], value: body.id, state: body.id === null || body.id === undefined ? 'empty' : 'found', templateValue: true };
91
+ }
87
92
  }
88
93
 
89
94
  get allParts(): IndexPart[] {
@@ -119,8 +124,8 @@ export class ModelIndexedComputedIndex<T extends ModelType> {
119
124
  }
120
125
  }
121
126
 
122
- project(config: IndexProcessConfig<{ emptyValue?: unknown }> = {}): Record<string, unknown> {
123
- const { keyed = true, sort = false, emptyValue = null } = config;
127
+ project(config: IndexProcessConfig<{ emptyValue?: unknown, includeId?: boolean }> = {}): Record<string, unknown> {
128
+ const { keyed = true, sort = false, emptyValue = null, includeId } = config;
124
129
  const response: Record<string, unknown> = {};
125
130
  if (keyed) {
126
131
  for (const { path, value, state } of this.keyedParts) {
@@ -144,6 +149,9 @@ export class ModelIndexedComputedIndex<T extends ModelType> {
144
149
  sub[last] = state === 'empty' ? emptyValue : value;
145
150
  }
146
151
  }
152
+ if (includeId && this.idPart) {
153
+ response.id = this.idPart.state === 'empty' ? emptyValue : this.idPart.value;
154
+ }
147
155
  return response;
148
156
  }
149
157
  }
@@ -0,0 +1,13 @@
1
+ import type { ModelType } from '@travetto/model';
2
+ import { RuntimeError, type Class } from '@travetto/runtime';
3
+
4
+ import type { AllIndexes } from './indexes.ts';
5
+
6
+
7
+ export class IndexedFieldError<T extends ModelType> extends RuntimeError {
8
+ constructor(cls: Class<T>, idx: AllIndexes<T>, fieldPath: string, message: string) {
9
+ super(`${message}: ${idx.name} on ${cls.name} at path ${fieldPath}`, {
10
+ details: { cls: cls.name, index: idx.name, fieldPath }
11
+ });
12
+ }
13
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ModelType, IndexConfig } from '@travetto/model';
2
- import { type IntrinsicType, type Any, type DeepPartial, RuntimeError, type Class } from '@travetto/runtime';
2
+ import { type IntrinsicType, type Any, type DeepPartial } from '@travetto/runtime';
3
3
 
4
4
  type TypeProjection<T, V> = {
5
5
  [P in keyof T]?:
@@ -10,8 +10,8 @@ type TypeProjection<T, V> = {
10
10
  );
11
11
  };
12
12
 
13
- export type KeyedIndexSelection<T> = TypeProjection<T, true>;
14
- export type SortedIndexSelection<T> = TypeProjection<T, 1 | -1>;
13
+ export type KeyedIndexSelection<T extends ModelType> = TypeProjection<T, true>;
14
+ export type SortedIndexSelection<T extends ModelType> = TypeProjection<T, 1 | -1>;
15
15
 
16
16
  export type KeyedIndexBody<T, K> = {
17
17
  [P in keyof K]: (P extends keyof T ?
@@ -44,8 +44,8 @@ export type KeyedIndexWithPartialBody<T, K> = {
44
44
  DeepPartial<Omit<T, keyof K>>;
45
45
 
46
46
 
47
- export type FullKeyedIndexBody<T, K, S> = KeyedIndexBody<T, Merge<K, S>>;
48
- export type FullKeyedIndexWithPartialBody<T, K, S> = KeyedIndexWithPartialBody<T, Merge<K, S>>;
47
+ export type FullKeyedIndexBody<T, K, S> = KeyedIndexBody<Omit<T, 'id'>, Merge<K, S>> & { id?: string };
48
+ export type FullKeyedIndexWithPartialBody<T, K, S> = KeyedIndexWithPartialBody<Omit<T, 'id'>, Merge<K, S>> & { id?: string };
49
49
 
50
50
  export type TemplateValue = 1 | -1 | true;
51
51
  export type TemplatePart<T extends TemplateValue = TemplateValue> = { path: string[], value: T, part: 'key' | 'sort' };
@@ -85,10 +85,3 @@ export type AllIndexes<
85
85
  S extends SortedIndexSelection<T> = Any
86
86
  > = KeyedIndex<T, K, S> | SortedIndex<T, K, S>;
87
87
 
88
- export class IndexedFieldError<T extends ModelType> extends RuntimeError {
89
- constructor(cls: Class<T>, idx: AllIndexes<T>, fieldPath: string, message: string) {
90
- super(`${message}: ${idx.name} on ${cls.name} at path ${fieldPath}`, {
91
- details: { cls: cls.name, index: idx.name, fieldPath }
92
- });
93
- }
94
- }
@@ -0,0 +1,12 @@
1
+ import type { ModelType } from '@travetto/model';
2
+
3
+ export interface ModelPageOptions<O = string> {
4
+ batchSizeHint?: number;
5
+ limit?: number;
6
+ offset?: O;
7
+ }
8
+
9
+ export interface ModelPageResult<T extends ModelType> {
10
+ items: T[];
11
+ nextOffset?: string;
12
+ }
@@ -1,19 +1,11 @@
1
- import type { ModelType, ModelBasicSupport, OptionalId } from '@travetto/model';
1
+ import type { ModelType, ModelBasicSupport, OptionalId, ModelListOptions } from '@travetto/model';
2
2
  import type { Class } from '@travetto/runtime';
3
+
3
4
  import type {
4
5
  KeyedIndexSelection, KeyedIndexBody, SortedIndexSelection, SortedIndex,
5
6
  SingleItemIndex, FullKeyedIndexBody, FullKeyedIndexWithPartialBody
6
7
  } from './indexes.ts';
7
-
8
- export type ListPageOptions<O = string> = {
9
- limit?: number;
10
- offset?: O;
11
- };
12
-
13
- export type ListPageResult<T extends ModelType> = {
14
- items: T[];
15
- nextOffset?: string;
16
- };
8
+ import type { ModelPageOptions, ModelPageResult } from './list.ts';
17
9
 
18
10
  /**
19
11
  * Support for simple indexed activity
@@ -82,15 +74,34 @@ export interface ModelIndexedSupport extends ModelBasicSupport {
82
74
  >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T>;
83
75
 
84
76
  /**
85
- * List entity by ranged index as defined by fields of idx
77
+ * Page through entities by ranged index as defined by fields of idx
78
+ *
79
+ * Note: Limit is generally honored, but can vary depending on the underlying storage implementation.
80
+ *
81
+ * @param cls The type to search by
82
+ * @param idx The index to search against
83
+ * @param body The payload of fields needed to search
84
+ * @param options The configuration for pagination
85
+ */
86
+ pageByIndex<
87
+ T extends ModelType,
88
+ S extends SortedIndexSelection<T>,
89
+ K extends KeyedIndexSelection<T>
90
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ModelPageOptions): Promise<ModelPageResult<T>>;
91
+
92
+ /**
93
+ * List all entities by ranged index as defined by fields of idx
94
+ *
95
+ * Note: Limit is generally honored, but can vary depending on the underlying storage implementation.
96
+ * Batch size hint can be used to optimize batch size, but is not guaranteed.
97
+ *
86
98
  * @param cls The type to search by
87
99
  * @param idx The index to search against
88
100
  * @param body The payload of fields needed to search
89
- * @param options The configuration for listing
90
101
  */
91
102
  listByIndex<
92
103
  T extends ModelType,
93
104
  S extends SortedIndexSelection<T>,
94
105
  K extends KeyedIndexSelection<T>
95
- >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ListPageOptions): Promise<ListPageResult<T>>;
106
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ModelListOptions): AsyncIterable<T[]>;
96
107
  }
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert';
2
+ import timers from 'node:timers/promises';
2
3
 
3
4
  import { Suite, Test } from '@travetto/test';
4
5
  import { Schema } from '@travetto/schema';
@@ -8,7 +9,7 @@ import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
8
9
 
9
10
  import type { ModelIndexedSupport } from '../../src/types/service.ts';
10
11
  import { keyedIndex, sortedIndex } from '../../src/indexes.ts';
11
- import { IndexedFieldError } from '../../__index__.ts';
12
+ import { IndexedFieldError } from '../../src/types/error.ts';
12
13
 
13
14
  @Model('index_user')
14
15
  class User {
@@ -102,6 +103,33 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
102
103
  assert(found2.name === 'bob2');
103
104
  }
104
105
 
106
+ @Test()
107
+ async readByKeyedIndexUsingId() {
108
+ const service = await this.service;
109
+
110
+ const first = await service.create(User, User.from({ name: 'sam' }));
111
+ const second = await service.create(User, User.from({ name: 'bob' }));
112
+
113
+ const found = await service.getByIndex(User, userNameIndex, {
114
+ name: 'bob',
115
+ id: second.id
116
+ });
117
+
118
+ assert(found.id === second.id);
119
+
120
+ await assert.rejects(
121
+ () => service.getByIndex(User, userNameIndex, { name: 'bob', id: first.id }),
122
+ NotFoundError
123
+ );
124
+
125
+ await service.deleteByIndex(User, userNameIndex, { name: 'sam', id: first.id });
126
+
127
+ await assert.rejects(() => service.get(User, first.id), NotFoundError);
128
+
129
+ const remaining = await service.getByIndex(User, userNameIndex, { name: 'bob', id: second.id });
130
+ assert(remaining.id === second.id);
131
+ }
132
+
105
133
  @Test()
106
134
  async readMissingValue() {
107
135
  const service = await this.service;
@@ -133,6 +161,30 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
133
161
  await assert.rejects(() => service.getByIndex(User3, userAgeIndex, { name: 'bob' }), IndexedFieldError);
134
162
  }
135
163
 
164
+ @Test()
165
+ async readBySortedIndexUsingId() {
166
+ const service = await this.service;
167
+
168
+ const first = await service.create(User3, User3.from({ name: 'bob', age: 40, color: 'blue' }));
169
+ const second = await service.create(User3, User3.from({ name: 'bob', age: 40, color: 'green' }));
170
+
171
+ const found = await service.getByIndex(User3, userAgeIndex, {
172
+ name: 'bob',
173
+ age: 40,
174
+ id: second.id
175
+ });
176
+
177
+ assert(found.id === second.id);
178
+ assert(found.color === 'green');
179
+
180
+ await service.deleteByIndex(User3, userAgeIndex, { name: 'bob', age: 40, id: first.id });
181
+
182
+ await assert.rejects(() => service.get(User3, first.id), NotFoundError);
183
+
184
+ const remaining = await service.getByIndex(User3, userAgeIndex, { name: 'bob', age: 40, id: second.id });
185
+ assert(remaining.id === second.id);
186
+ }
187
+
136
188
  @Test()
137
189
  async queryList() {
138
190
  const service = await this.service;
@@ -141,7 +193,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
141
193
  await service.create(User3, User3.from({ name: 'bob', age: 30, color: 'red' }));
142
194
  await service.create(User3, User3.from({ name: 'bob', age: 50, color: 'green' }));
143
195
 
144
- const { items: arr } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
196
+ const { items: arr } = await service.pageByIndex(User3, userAgeIndex, { name: 'bob' });
145
197
 
146
198
  console.error(arr);
147
199
 
@@ -153,7 +205,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
153
205
  assert(arr[2].name === 'bob');
154
206
 
155
207
  // @ts-expect-error
156
- await assert.rejects(() => service.listByIndex(User3, userAgeIndex, {}), IndexedFieldError);
208
+ await assert.rejects(() => service.pageByIndex(User3, userAgeIndex, {}), IndexedFieldError);
157
209
  }
158
210
 
159
211
  @Test()
@@ -164,7 +216,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
164
216
  await service.create(User3, User3.from({ name: 'alice', age: 30, color: 'red' }));
165
217
  await service.create(User3, User3.from({ name: 'bob', age: 50, color: 'green' }));
166
218
 
167
- const { items: arr } = await service.listByIndex(User3, userAgeNoKeyIndex, {});
219
+ const { items: arr } = await service.pageByIndex(User3, userAgeNoKeyIndex, {});
168
220
 
169
221
  assert(arr[0].name === 'alice' && arr[0].age === 30);
170
222
  assert(arr[1].name === 'charlie' && arr[1].age === 40);
@@ -185,13 +237,13 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
185
237
  await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, color: 'red' }));
186
238
  await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, color: 'green' }));
187
239
 
188
- const { items: arr } = await service.listByIndex(User4, childAgeIndex, { child: { name: 'bob' } });
240
+ const { items: arr } = await service.pageByIndex(User4, childAgeIndex, { child: { name: 'bob' } });
189
241
  assert(arr[0].color === 'red' && arr[0].child.name === 'bob' && arr[0].child.age === 30);
190
242
  assert(arr[1].color === 'blue' && arr[1].child.name === 'bob' && arr[1].child.age === 40);
191
243
  assert(arr[2].color === 'green' && arr[2].child.name === 'bob' && arr[2].child.age === 50);
192
244
 
193
245
  // @ts-expect-error
194
- await assert.rejects(() => service.listByIndex(User4, childAgeIndex, {}), IndexedFieldError);
246
+ await assert.rejects(() => service.pageByIndex(User4, childAgeIndex, {}), IndexedFieldError);
195
247
  }
196
248
 
197
249
  @Test({ skip: (self) => !castTo<ModelIndexedSuite>(self).supportsDeepIndexes })
@@ -202,14 +254,14 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
202
254
  await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.fromNow('2d'), color: 'red' }));
203
255
  await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.fromNow('-1d'), color: 'green' }));
204
256
 
205
- const { items: arr } = await service.listByIndex(User4, nameCreatedIndex, { child: { name: 'bob' } });
257
+ const { items: arr } = await service.pageByIndex(User4, nameCreatedIndex, { child: { name: 'bob' } });
206
258
 
207
259
  assert(arr[0].color === 'green' && arr[0].child.name === 'bob' && arr[0].child.age === 50);
208
260
  assert(arr[1].color === 'red' && arr[1].child.name === 'bob' && arr[1].child.age === 30);
209
261
  assert(arr[2].color === 'blue' && arr[2].child.name === 'bob' && arr[2].child.age === 40);
210
262
 
211
263
  // @ts-expect-error
212
- await assert.rejects(() => service.listByIndex(User4, nameCreatedIndex, {}), IndexedFieldError);
264
+ await assert.rejects(() => service.pageByIndex(User4, nameCreatedIndex, {}), IndexedFieldError);
213
265
  }
214
266
 
215
267
  @Test()
@@ -220,7 +272,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
220
272
  const user2 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 40, color: 'green' });
221
273
  const user3 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 40, color: 'red' });
222
274
 
223
- const { items: arr } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
275
+ const { items: arr } = await service.pageByIndex(User3, userAgeIndex, { name: 'bob' });
224
276
  assert(arr.length === 1);
225
277
 
226
278
  assert(user1.id === user2.id);
@@ -229,12 +281,12 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
229
281
  assert(user3.color === 'red');
230
282
 
231
283
  const user4 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 30, color: 'red' });
232
- const { items: arr2 } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
284
+ const { items: arr2 } = await service.pageByIndex(User3, userAgeIndex, { name: 'bob' });
233
285
  assert(arr2.length === 2);
234
286
 
235
287
  await service.deleteByIndex(User3, userAgeIndex, user1);
236
288
 
237
- const { items: arr3 } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
289
+ const { items: arr3 } = await service.pageByIndex(User3, userAgeIndex, { name: 'bob' });
238
290
  assert(arr3.length === 1);
239
291
  assert(arr3[0].id === user4.id);
240
292
  }
@@ -288,7 +340,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
288
340
  let offset: string | undefined;
289
341
 
290
342
  do {
291
- const page = await service.listByIndex(User3, userAgeIndex, { name: 'page' }, { limit, offset });
343
+ const page = await service.pageByIndex(User3, userAgeIndex, { name: 'page' }, { limit, offset });
292
344
  items.push(...page.items.map(u => u.color!));
293
345
  offset = page.nextOffset;
294
346
  } while (offset);
@@ -312,7 +364,7 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
312
364
  let offset: string | undefined;
313
365
 
314
366
  do {
315
- const page = await service.listByIndex(User3, userAgeReversedIndex, { name: 'page' }, { limit, offset });
367
+ const page = await service.pageByIndex(User3, userAgeReversedIndex, { name: 'page' }, { limit, offset });
316
368
  items.push(...page.items.map(u => u.color!));
317
369
  offset = page.nextOffset;
318
370
  } while (offset);
@@ -320,4 +372,28 @@ export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSuppo
320
372
  assert(items.length === allColors.length);
321
373
  assert.deepEqual(items, allColors.toReversed());
322
374
  }
375
+
376
+ @Test()
377
+ async listByIndexAbortSignal() {
378
+ const service = await this.service;
379
+
380
+ await Promise.all(
381
+ [20, 30, 40].map(age => service.create(User3, User3.from({ name: 'page', age, color: `${age}` })))
382
+ );
383
+
384
+ const controller = new AbortController();
385
+ const found: User3[] = [];
386
+
387
+ for await (const items of service.listByIndex(User3, userAgeIndex, { name: 'page' }, { abort: controller.signal, batchSizeHint: 1 })) {
388
+ found.push(...items);
389
+ controller.abort();
390
+ await timers.setTimeout(10);
391
+ }
392
+
393
+ if (this.indexLimitSkew) {
394
+ assert(found.length < this.indexLimitSkew && found.length > 0);
395
+ } else {
396
+ assert(found.length === 1);
397
+ }
398
+ }
323
399
  }