@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 +78 -22
- package/__index__.ts +2 -0
- package/package.json +6 -6
- package/src/computed.ts +15 -7
- package/src/types/error.ts +13 -0
- package/src/types/indexes.ts +5 -12
- package/src/types/list.ts +12 -0
- package/src/types/service.ts +25 -14
- package/support/test/indexed.ts +89 -13
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#
|
|
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
|
-
*
|
|
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?:
|
|
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
|
-
* `
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 `
|
|
339
|
+
Use `pageByIndex` when you want paginated access to a sorted index.
|
|
299
340
|
|
|
300
|
-
**Code:
|
|
341
|
+
**Code: Paging by Sorted Index**
|
|
301
342
|
```typescript
|
|
302
|
-
export async function listExample(modelService:
|
|
303
|
-
const result = await modelService.
|
|
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
|
-
|
|
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:
|
|
374
|
+
export async function listWithFilterExample(modelService: ModelIndexedSupport) {
|
|
319
375
|
// Get all users named 'John' sorted by age
|
|
320
|
-
const result = await modelService.
|
|
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#
|
|
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.
|
|
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/
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-indexed",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
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.
|
|
30
|
-
"@travetto/registry": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
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.
|
|
35
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
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,
|
|
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:
|
|
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:
|
|
33
|
+
bodyPart = { value: null, state: 'missing' };
|
|
33
34
|
break;
|
|
34
35
|
}
|
|
35
36
|
} else {
|
|
36
|
-
bodyPart = { value:
|
|
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
|
+
}
|
package/src/types/indexes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelType, IndexConfig } from '@travetto/model';
|
|
2
|
-
import { type IntrinsicType, type Any, type DeepPartial
|
|
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
|
+
}
|
package/src/types/service.ts
CHANGED
|
@@ -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
|
-
*
|
|
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?:
|
|
106
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ModelListOptions): AsyncIterable<T[]>;
|
|
96
107
|
}
|
package/support/test/indexed.ts
CHANGED
|
@@ -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 '../../
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|