@travetto/model-indexed 8.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +353 -0
- package/__index__.ts +5 -0
- package/package.json +51 -0
- package/src/computed.ts +149 -0
- package/src/indexes.ts +83 -0
- package/src/types/indexes.ts +94 -0
- package/src/types/service.ts +96 -0
- package/src/util.ts +62 -0
- package/support/doc.support.tsx +27 -0
- package/support/test/indexed.ts +323 -0
- package/support/test/polymorphism.ts +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/model-indexed/DOC.tsx and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Data Model Indexing Support
|
|
4
|
+
|
|
5
|
+
## Basic indexing support for model sources that support it.
|
|
6
|
+
|
|
7
|
+
**Install: @travetto/model-indexed**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @travetto/model-indexed
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
|
|
13
|
+
yarn add @travetto/model-indexed
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This module provides computed index support for data model sources that support it. It enables efficient lookups and list operations using composite keys extracted from model fields, without requiring a full query engine.
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
The module allows you to define indexes on your models and use them for fast single-item lookups, uniqueness enforcement, and efficient paginated list operations. Indexes are computed from model field values and act as alternative keys for data access.
|
|
20
|
+
|
|
21
|
+
### Index Types
|
|
22
|
+
Three types of indexes are supported:
|
|
23
|
+
* **Keyed Indexes** — Fast single-item lookups using composite keys
|
|
24
|
+
* **Unique Indexes** — Enforce uniqueness constraints on key fields
|
|
25
|
+
* **Sorted Indexes** — Enable range queries and paginated listing with sorting
|
|
26
|
+
|
|
27
|
+
## Defining Indexes
|
|
28
|
+
Indexes are defined using factory functions provided by the module. Each index is registered with the model at decoration time.
|
|
29
|
+
|
|
30
|
+
### Keyed Indexes
|
|
31
|
+
A [keyedIndex](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/indexes.ts#L29) provides fast lookups by computed key values. It's useful when you want to query records by specific field combinations.
|
|
32
|
+
|
|
33
|
+
**Code: Creating a Keyed Index**
|
|
34
|
+
```typescript
|
|
35
|
+
import { keyedIndex } from '@travetto/model-indexed';
|
|
36
|
+
import { Model } from '@travetto/model';
|
|
37
|
+
|
|
38
|
+
@Model()
|
|
39
|
+
export class User {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
email: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const userByName = keyedIndex(User, {
|
|
46
|
+
name: 'userByName',
|
|
47
|
+
key: { name: true }
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The index definition specifies:
|
|
52
|
+
* `name` — The identifier for this index
|
|
53
|
+
* `key` — An object where each key path should be included in the index (set to `true`)
|
|
54
|
+
|
|
55
|
+
### Unique Indexes
|
|
56
|
+
A [uniqueIndex](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/indexes.ts#L47) enforces uniqueness constraints on key fields. This is useful for emails, usernames, or any field that should be globally unique.
|
|
57
|
+
|
|
58
|
+
**Code: Creating a Unique Index**
|
|
59
|
+
```typescript
|
|
60
|
+
import { uniqueIndex } from '@travetto/model-indexed';
|
|
61
|
+
import { Model } from '@travetto/model';
|
|
62
|
+
|
|
63
|
+
@Model()
|
|
64
|
+
export class User {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
email: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const emailUnique = uniqueIndex(User, {
|
|
71
|
+
name: 'uniqueEmail',
|
|
72
|
+
key: { email: true }
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Unique indexes work exactly like keyed indexes, but enforce a uniqueness constraint. A model service will reject writes that violate the uniqueness guarantee.
|
|
77
|
+
|
|
78
|
+
### Sorted Indexes
|
|
79
|
+
A [sortedIndex](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/indexes.ts#L65) enables range queries and paginated listing. It requires both a `key` for filtering and a `sort` field for ordering.
|
|
80
|
+
|
|
81
|
+
**Code: Creating a Sorted Index**
|
|
82
|
+
```typescript
|
|
83
|
+
import { sortedIndex } from '@travetto/model-indexed';
|
|
84
|
+
import { Model } from '@travetto/model';
|
|
85
|
+
|
|
86
|
+
@Model()
|
|
87
|
+
export class User {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
age: number;
|
|
91
|
+
createdAt: Date;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const usersByNameAge = sortedIndex(User, {
|
|
95
|
+
name: 'usersByNameAge',
|
|
96
|
+
key: { name: true },
|
|
97
|
+
sort: { age: 1 } // 1 for ascending, -1 for descending
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const recentUsers = sortedIndex(User, {
|
|
101
|
+
name: 'recentUsers',
|
|
102
|
+
key: {}, // No key filtering
|
|
103
|
+
sort: { createdAt: -1 } // Most recent first
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The `sort` field must be numeric or a `Date` type. The value `1` means ascending order, `-1` means descending.
|
|
108
|
+
|
|
109
|
+
### Composite Keys
|
|
110
|
+
Indexes can use multiple fields or nested fields in their keys. This allows querying by combinations of values.
|
|
111
|
+
|
|
112
|
+
**Code: Composite Key Index**
|
|
113
|
+
```typescript
|
|
114
|
+
import { keyedIndex } from '@travetto/model-indexed';
|
|
115
|
+
import { Model } from '@travetto/model';
|
|
116
|
+
|
|
117
|
+
@Model()
|
|
118
|
+
export class Order {
|
|
119
|
+
id: string;
|
|
120
|
+
customerId: string;
|
|
121
|
+
status: string;
|
|
122
|
+
productId: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Find orders by customer and status
|
|
126
|
+
export const orders = keyedIndex(Order, {
|
|
127
|
+
name: 'ordersByCustomerStatus',
|
|
128
|
+
key: { customerId: true, status: true }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Find orders by customer, status, and product
|
|
132
|
+
export const specificOrders = keyedIndex(Order, {
|
|
133
|
+
name: 'ordersByCustomerStatusProduct',
|
|
134
|
+
key: { customerId: true, status: true, productId: true }
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
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.
|
|
140
|
+
|
|
141
|
+
### Service Interface
|
|
142
|
+
|
|
143
|
+
**Code: ModelIndexedSupport Interface**
|
|
144
|
+
```typescript
|
|
145
|
+
export interface ModelIndexedSupport extends ModelBasicSupport {
|
|
146
|
+
/**
|
|
147
|
+
* Get entity by index as defined by fields of idx and the body fields
|
|
148
|
+
* @param cls The type to search by
|
|
149
|
+
* @param idx The index to search against
|
|
150
|
+
* @param body The payload of fields needed to search
|
|
151
|
+
*/
|
|
152
|
+
getByIndex<
|
|
153
|
+
T extends ModelType,
|
|
154
|
+
K extends KeyedIndexSelection<T>,
|
|
155
|
+
S extends SortedIndexSelection<T>
|
|
156
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Delete entity by index as defined by fields of idx and the body fields
|
|
160
|
+
* @param cls The type to search by
|
|
161
|
+
* @param idx The index to search against
|
|
162
|
+
* @param body The payload of fields needed to search
|
|
163
|
+
*/
|
|
164
|
+
deleteByIndex<
|
|
165
|
+
T extends ModelType,
|
|
166
|
+
K extends KeyedIndexSelection<T>,
|
|
167
|
+
S extends SortedIndexSelection<T>
|
|
168
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Upsert by index, allowing the index to act as a primary key
|
|
172
|
+
* @param cls The type to create for
|
|
173
|
+
* @param idx The index to use
|
|
174
|
+
* @param body The document to potentially store
|
|
175
|
+
*/
|
|
176
|
+
upsertByIndex<
|
|
177
|
+
T extends ModelType,
|
|
178
|
+
K extends KeyedIndexSelection<T>,
|
|
179
|
+
S extends SortedIndexSelection<T>
|
|
180
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update by index
|
|
184
|
+
* @param cls The type to update for
|
|
185
|
+
* @param idx The index to update by
|
|
186
|
+
* @param body The document to update
|
|
187
|
+
*/
|
|
188
|
+
updateByIndex<
|
|
189
|
+
T extends ModelType,
|
|
190
|
+
K extends KeyedIndexSelection<T>,
|
|
191
|
+
S extends SortedIndexSelection<T>
|
|
192
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T>;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Update partial by index
|
|
196
|
+
* @param cls The type to update for
|
|
197
|
+
* @param idx The index to update by
|
|
198
|
+
* @param body The partial document to update
|
|
199
|
+
*/
|
|
200
|
+
updatePartialByIndex<
|
|
201
|
+
T extends ModelType,
|
|
202
|
+
K extends KeyedIndexSelection<T>,
|
|
203
|
+
S extends SortedIndexSelection<T>
|
|
204
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* List entity by ranged index as defined by fields of idx
|
|
208
|
+
* @param cls The type to search by
|
|
209
|
+
* @param idx The index to search against
|
|
210
|
+
* @param body The payload of fields needed to search
|
|
211
|
+
* @param options The configuration for listing
|
|
212
|
+
*/
|
|
213
|
+
listByIndex<
|
|
214
|
+
T extends ModelType,
|
|
215
|
+
S extends SortedIndexSelection<T>,
|
|
216
|
+
K extends KeyedIndexSelection<T>
|
|
217
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ListPageOptions): Promise<ListPageResult<T>>;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The service provides these operations:
|
|
222
|
+
* `getByIndex` — Fetch a single item by index
|
|
223
|
+
* `deleteByIndex` — Delete a single item by index
|
|
224
|
+
* `upsertByIndex` — Insert or update by index
|
|
225
|
+
* `updateByIndex` — Update an existing item by index
|
|
226
|
+
* `updatePartialByIndex` — Partially update an item by index
|
|
227
|
+
* `listByIndex` — List items with pagination and sorting
|
|
228
|
+
|
|
229
|
+
### Getting Items
|
|
230
|
+
Use `getByIndex` to fetch a single item by providing all required key fields.
|
|
231
|
+
|
|
232
|
+
**Code: Getting by Keyed Index**
|
|
233
|
+
```typescript
|
|
234
|
+
export async function getExample(modelService: any) {
|
|
235
|
+
const user = await modelService.getByIndex(User, userByName, {
|
|
236
|
+
name: 'John Doe'
|
|
237
|
+
});
|
|
238
|
+
return user;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
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.
|
|
243
|
+
|
|
244
|
+
### Deleting Items
|
|
245
|
+
Use `deleteByIndex` to remove an item by index.
|
|
246
|
+
|
|
247
|
+
**Code: Deleting by Index**
|
|
248
|
+
```typescript
|
|
249
|
+
export async function deleteExample(modelService: any) {
|
|
250
|
+
await modelService.deleteByIndex(User, userByName, {
|
|
251
|
+
name: 'John Doe'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Upserting Items
|
|
257
|
+
Use `upsertByIndex` to insert a new item or update an existing one. The index acts as a primary key.
|
|
258
|
+
|
|
259
|
+
**Code: Upserting by Index**
|
|
260
|
+
```typescript
|
|
261
|
+
export async function upsertExample(modelService: any) {
|
|
262
|
+
const user = await modelService.upsertByIndex(User, userByName, {
|
|
263
|
+
id: 'user-1',
|
|
264
|
+
name: 'John Doe',
|
|
265
|
+
email: 'john@example.com'
|
|
266
|
+
});
|
|
267
|
+
return user;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Updating Items
|
|
272
|
+
Use `updateByIndex` to update an existing item, or `updatePartialByIndex` for partial updates.
|
|
273
|
+
|
|
274
|
+
**Code: Updating by Index**
|
|
275
|
+
```typescript
|
|
276
|
+
export async function updateExample(modelService: any) {
|
|
277
|
+
// Full update — all fields required
|
|
278
|
+
const user = await modelService.updateByIndex(User, userByName, {
|
|
279
|
+
id: 'user-1',
|
|
280
|
+
name: 'John Doe',
|
|
281
|
+
email: 'john.new@example.com',
|
|
282
|
+
age: 31
|
|
283
|
+
});
|
|
284
|
+
return user;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function updatePartialExample(modelService: any) {
|
|
288
|
+
// Partial update — only updated fields required
|
|
289
|
+
const user = await modelService.updatePartialByIndex(User, userByName, {
|
|
290
|
+
name: 'John Doe',
|
|
291
|
+
email: 'john.newer@example.com'
|
|
292
|
+
});
|
|
293
|
+
return user;
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Listing Items
|
|
298
|
+
Use `listByIndex` to fetch multiple items from a sorted index with pagination.
|
|
299
|
+
|
|
300
|
+
**Code: Listing by Sorted Index**
|
|
301
|
+
```typescript
|
|
302
|
+
export async function listExample(modelService: any) {
|
|
303
|
+
const result = await modelService.listByIndex(User, recentUsers, {}, {
|
|
304
|
+
limit: 20,
|
|
305
|
+
offset: '0'
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
console.log(result.items); // Array of users
|
|
309
|
+
console.log(result.nextOffset); // Token for next page, if more results exist
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
You can also provide key values to filter within a sorted index:
|
|
315
|
+
|
|
316
|
+
**Code: Listing with Key Filter**
|
|
317
|
+
```typescript
|
|
318
|
+
export async function listWithFilterExample(modelService: any) {
|
|
319
|
+
// Get all users named 'John' sorted by age
|
|
320
|
+
const result = await modelService.listByIndex(User, usersByNameAge, {
|
|
321
|
+
name: 'John'
|
|
322
|
+
}, {
|
|
323
|
+
limit: 10
|
|
324
|
+
});
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## 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.
|
|
331
|
+
|
|
332
|
+
### Reading Registry Information
|
|
333
|
+
You can access registered indexes via [ModelRegistryIndex](https://github.com/travetto/travetto/tree/main/module/model/src/registry/registry-index.ts#L12) at runtime:
|
|
334
|
+
|
|
335
|
+
**Code: Accessing Model Indexes**
|
|
336
|
+
```typescript
|
|
337
|
+
export function registryAccessExample() {
|
|
338
|
+
const registry = ModelRegistryIndex.get(User);
|
|
339
|
+
const indexes = registry.indices; // Map of all indexes for the model
|
|
340
|
+
|
|
341
|
+
// Access a specific index
|
|
342
|
+
const userByName = indexes['userByName'];
|
|
343
|
+
return userByName;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Best Practices
|
|
348
|
+
|
|
349
|
+
* **Plan indexes strategically** — Define indexes for your common query patterns
|
|
350
|
+
* **Use composite keys** — When filtering by multiple fields, include all of them in a single index
|
|
351
|
+
* **Leverage sorting** — Use sorted indexes for paginated lists and range queries
|
|
352
|
+
* **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
|
package/__index__.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@travetto/model-indexed",
|
|
3
|
+
"version": "8.0.0-alpha.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Basic indexing support for model sources that support it.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"datastore",
|
|
8
|
+
"indexing",
|
|
9
|
+
"typescript",
|
|
10
|
+
"travetto"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://travetto.io",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"email": "travetto.framework@gmail.com",
|
|
16
|
+
"name": "Travetto Framework"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"__index__.ts",
|
|
20
|
+
"src",
|
|
21
|
+
"support"
|
|
22
|
+
],
|
|
23
|
+
"main": "__index__.ts",
|
|
24
|
+
"repository": {
|
|
25
|
+
"url": "git+https://github.com/travetto/travetto.git",
|
|
26
|
+
"directory": "module/model-indexed"
|
|
27
|
+
},
|
|
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"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@travetto/cli": "^8.0.0-alpha.15",
|
|
35
|
+
"@travetto/test": "^8.0.0-alpha.10"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@travetto/cli": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@travetto/test": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"travetto": {
|
|
46
|
+
"displayName": "Data Model Indexing Support"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/computed.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ModelType } from '@travetto/model';
|
|
2
|
+
import { castTo, type Any } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type KeyedIndexSelection, type SortedIndexSelection, type AllIndexes, type KeyedIndexBody, IndexedFieldError,
|
|
6
|
+
type FullKeyedIndexBody, type TemplateValue, type TemplatePart
|
|
7
|
+
} from './types/indexes.ts';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SEP = '\u8203';
|
|
10
|
+
|
|
11
|
+
type IndexPart<T extends TemplateValue = TemplateValue> = {
|
|
12
|
+
state: 'missing' | 'empty' | 'mismatch' | 'found';
|
|
13
|
+
value: unknown;
|
|
14
|
+
path: string[];
|
|
15
|
+
templateValue: T;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function buildIndexParts<T extends TemplateValue = TemplateValue>(
|
|
19
|
+
template: TemplatePart<T>[],
|
|
20
|
+
body: Record<string, unknown> | undefined,
|
|
21
|
+
checkValueType?: (value: unknown) => boolean,
|
|
22
|
+
): IndexPart<T>[] {
|
|
23
|
+
const out: IndexPart<T>[] = [];
|
|
24
|
+
for (const { path, value: templateValue } of template) {
|
|
25
|
+
let value: unknown = body;
|
|
26
|
+
let bodyPart: Pick<IndexPart, 'value' | 'state'> | undefined;
|
|
27
|
+
for (const pathItem of path) {
|
|
28
|
+
if (typeof value === 'object' && value !== null) {
|
|
29
|
+
if (value && pathItem in value) {
|
|
30
|
+
value = castTo<Record<string, unknown>>(value)[pathItem];
|
|
31
|
+
} else {
|
|
32
|
+
bodyPart = { value: undefined!, state: 'missing' };
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
bodyPart = { value: undefined!, state: 'mismatch' };
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (bodyPart === undefined) {
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
bodyPart = { value: null, state: 'empty' };
|
|
43
|
+
} else if (checkValueType && !checkValueType(value)) {
|
|
44
|
+
bodyPart = { value, state: 'mismatch' };
|
|
45
|
+
} else {
|
|
46
|
+
bodyPart = { value, state: 'found' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
out.push({ ...bodyPart, path, templateValue });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validate<T extends ModelType>(idx: AllIndexes<T>, parts: IndexPart[]): void {
|
|
55
|
+
for (const { state, path } of parts) {
|
|
56
|
+
if (state === 'missing') {
|
|
57
|
+
throw new IndexedFieldError(idx.class, idx, path.join('.'), 'Missing field');
|
|
58
|
+
} else if (state === 'mismatch') {
|
|
59
|
+
throw new IndexedFieldError(idx.class, idx, path.join('.'), 'Field type mismatch');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type Body<T extends ModelType> = KeyedIndexBody<T, Any> | FullKeyedIndexBody<T, Any, Any> | Partial<T>;
|
|
65
|
+
|
|
66
|
+
type IndexProcessConfig<T = {}> = T & { keyed?: boolean, sort?: boolean };
|
|
67
|
+
|
|
68
|
+
export class ModelIndexedComputedIndex<T extends ModelType> {
|
|
69
|
+
static get<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
70
|
+
idx: AllIndexes<T, K, S>,
|
|
71
|
+
body: Body<T>,
|
|
72
|
+
): ModelIndexedComputedIndex<T> {
|
|
73
|
+
return new ModelIndexedComputedIndex(idx, body);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
keyedParts: IndexPart<true>[];
|
|
77
|
+
sortParts: IndexPart<-1 | 1>[];
|
|
78
|
+
idx: AllIndexes<T>;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
idx: AllIndexes<T>,
|
|
82
|
+
body: Body<T>,
|
|
83
|
+
) {
|
|
84
|
+
this.idx = idx;
|
|
85
|
+
this.keyedParts = buildIndexParts(idx.keyTemplate, castTo(body));
|
|
86
|
+
this.sortParts = buildIndexParts(idx.sortTemplate, castTo(body), value => typeof value === 'number' || value instanceof Date);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get allParts(): IndexPart[] {
|
|
90
|
+
return [...this.keyedParts, ...this.sortParts];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
validate(config: IndexProcessConfig = {}): this {
|
|
94
|
+
const { keyed = true, sort = false } = config;
|
|
95
|
+
if (keyed) {
|
|
96
|
+
validate(this.idx, this.keyedParts);
|
|
97
|
+
}
|
|
98
|
+
if (sort) {
|
|
99
|
+
validate(this.idx, this.sortParts);
|
|
100
|
+
}
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getKey(config: IndexProcessConfig<{ sep?: string }> = {}): string {
|
|
105
|
+
const { keyed = true, sort = false, sep = DEFAULT_SEP } = config;
|
|
106
|
+
const parts = [keyed ? this.keyedParts : [], sort ? this.sortParts : []].flat();
|
|
107
|
+
return parts.map(({ value }) => value).map(value => `${value}`).join(sep);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSort(): number {
|
|
111
|
+
const { value } = this.sortParts[0] ?? {};
|
|
112
|
+
const direction = (this.sortParts[0]?.templateValue ?? 1);
|
|
113
|
+
if (value instanceof Date) {
|
|
114
|
+
return value.getTime() * direction;
|
|
115
|
+
} else if (typeof value === 'number') {
|
|
116
|
+
return value * direction;
|
|
117
|
+
} else {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
project(config: IndexProcessConfig<{ emptyValue?: unknown }> = {}): Record<string, unknown> {
|
|
123
|
+
const { keyed = true, sort = false, emptyValue = null } = config;
|
|
124
|
+
const response: Record<string, unknown> = {};
|
|
125
|
+
if (keyed) {
|
|
126
|
+
for (const { path, value, state } of this.keyedParts) {
|
|
127
|
+
let sub: Record<string, unknown> = response;
|
|
128
|
+
const all = path.slice(0);
|
|
129
|
+
const last = all.pop()!;
|
|
130
|
+
for (const part of all) {
|
|
131
|
+
sub = castTo(sub[part] ??= {});
|
|
132
|
+
}
|
|
133
|
+
sub[last] = state === 'empty' ? emptyValue : value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (sort) {
|
|
137
|
+
for (const { path, value, state } of this.sortParts) {
|
|
138
|
+
let sub: Record<string, unknown> = response;
|
|
139
|
+
const all = path.slice(0);
|
|
140
|
+
const last = all.pop()!;
|
|
141
|
+
for (const part of all) {
|
|
142
|
+
sub = castTo(sub[part] ??= {});
|
|
143
|
+
}
|
|
144
|
+
sub[last] = state === 'empty' ? emptyValue : value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/indexes.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ModelType, ModelRegistryIndex } from '@travetto/model';
|
|
2
|
+
import { type Class, type Any, castTo } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type AllIndexes, type KeyedIndexSelection, type KeyedIndex,
|
|
6
|
+
type SortedIndexSelection, type SortedIndex, type TemplatePart, type TemplateValue
|
|
7
|
+
} from './types/indexes.ts';
|
|
8
|
+
|
|
9
|
+
function buildTemplateParts<T extends TemplateValue = TemplateValue>(
|
|
10
|
+
part: 'key' | 'sort',
|
|
11
|
+
template: Record<string, unknown>,
|
|
12
|
+
prefix: string[] = [],
|
|
13
|
+
): TemplatePart<T>[] {
|
|
14
|
+
const out: TemplatePart<T>[] = [];
|
|
15
|
+
for (const [key, value] of Object.entries(template)) {
|
|
16
|
+
const path = prefix.length ? [...prefix, key] : [key];
|
|
17
|
+
if (typeof value === 'object' && value !== null) {
|
|
18
|
+
out.push(...buildTemplateParts<T>(part, castTo(value), path));
|
|
19
|
+
} else {
|
|
20
|
+
out.push({ path, value: castTo<T>(value), part });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Defines a keyed index for a model
|
|
28
|
+
*/
|
|
29
|
+
export function keyedIndex<
|
|
30
|
+
T extends ModelType,
|
|
31
|
+
K extends KeyedIndexSelection<T>
|
|
32
|
+
>(cls: Class<T>, config: { name: string, key: K }): KeyedIndex<T, K, {}> {
|
|
33
|
+
const { name, key } = config;
|
|
34
|
+
const keyTemplate = buildTemplateParts<true>('key', key);
|
|
35
|
+
const idx: KeyedIndex<T, K, {}> = {
|
|
36
|
+
type: 'indexed:keyed',
|
|
37
|
+
class: cls, name, unique: false,
|
|
38
|
+
key, keyTemplate, sort: {}, sortTemplate: []
|
|
39
|
+
};
|
|
40
|
+
ModelRegistryIndex.getForRegister(cls).register({ indices: { [idx.name]: idx } });
|
|
41
|
+
return idx;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Defines a unique index for a model
|
|
46
|
+
*/
|
|
47
|
+
export function uniqueIndex<
|
|
48
|
+
T extends ModelType,
|
|
49
|
+
K extends KeyedIndexSelection<T>
|
|
50
|
+
>(cls: Class<T>, config: { name: string, key: K }): KeyedIndex<T, K, {}> {
|
|
51
|
+
const { name, key } = config;
|
|
52
|
+
const keyTemplate = buildTemplateParts<true>('key', key);
|
|
53
|
+
const idx: KeyedIndex<T, K, {}> = {
|
|
54
|
+
type: 'indexed:keyed',
|
|
55
|
+
class: cls, name, unique: true,
|
|
56
|
+
key, keyTemplate, sort: {}, sortTemplate: []
|
|
57
|
+
};
|
|
58
|
+
ModelRegistryIndex.getForRegister(cls).register({ indices: { [idx.name]: idx } });
|
|
59
|
+
return idx;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Defines a sorted index for a model
|
|
64
|
+
*/
|
|
65
|
+
export function sortedIndex<
|
|
66
|
+
T extends ModelType,
|
|
67
|
+
K extends KeyedIndexSelection<T>,
|
|
68
|
+
S extends SortedIndexSelection<T>
|
|
69
|
+
>(cls: Class<T>, config: { name: string, key: K, sort: S }): SortedIndex<T, K, S> {
|
|
70
|
+
const { name, key, sort } = config;
|
|
71
|
+
const keyTemplate = buildTemplateParts<true>('key', key);
|
|
72
|
+
const sortTemplate = buildTemplateParts<1 | -1>('sort', sort);
|
|
73
|
+
const idx: SortedIndex<T, K, S> = {
|
|
74
|
+
type: 'indexed:sorted',
|
|
75
|
+
class: cls, name, key, sort,
|
|
76
|
+
keyTemplate, sortTemplate,
|
|
77
|
+
};
|
|
78
|
+
ModelRegistryIndex.getForRegister(cls).register({ indices: { [idx.name]: idx } });
|
|
79
|
+
return idx;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const isModelIndexedIndex = <T extends ModelType>(idx: Any): idx is AllIndexes<T> =>
|
|
83
|
+
typeof idx === 'object' && idx !== null && 'type' in idx && typeof idx.type === 'string' && idx.type.startsWith('indexed:');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ModelType, IndexConfig } from '@travetto/model';
|
|
2
|
+
import { type IntrinsicType, type Any, type DeepPartial, RuntimeError, type Class } from '@travetto/runtime';
|
|
3
|
+
|
|
4
|
+
type TypeProjection<T, V> = {
|
|
5
|
+
[P in keyof T]?:
|
|
6
|
+
(T[P] extends (IntrinsicType | undefined) ? (V | undefined) :
|
|
7
|
+
(T[P] extends Any[] ?
|
|
8
|
+
(TypeProjection<T[P][number], V> | null | undefined)[] :
|
|
9
|
+
TypeProjection<T[P], V>)
|
|
10
|
+
);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type KeyedIndexSelection<T> = TypeProjection<T, true>;
|
|
14
|
+
export type SortedIndexSelection<T> = TypeProjection<T, 1 | -1>;
|
|
15
|
+
|
|
16
|
+
export type KeyedIndexBody<T, K> = {
|
|
17
|
+
[P in keyof K]: (P extends keyof T ?
|
|
18
|
+
(K[P] extends true | 1 | -1 ? T[P] :
|
|
19
|
+
(T[P] extends Any[] | null | undefined ? T[P] :
|
|
20
|
+
KeyedIndexBody<T[P], NonNullable<K[P]>>))
|
|
21
|
+
: never);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Merge<A, B> = {
|
|
25
|
+
[K in keyof A | keyof B]: (K extends keyof A & keyof B ?
|
|
26
|
+
A[K] | B[K] : (K extends keyof B ?
|
|
27
|
+
B[K] : (K extends keyof A ?
|
|
28
|
+
A[K] : never
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type KeyedIndexWithPartialBody<T, K> = {
|
|
35
|
+
[P in keyof K]: (P extends keyof T ?
|
|
36
|
+
(K[P] extends true | 1 | -1 ? T[P] :
|
|
37
|
+
(T[P] extends Any[] | null | undefined ? T[P] :
|
|
38
|
+
// Recurse for nested objects
|
|
39
|
+
KeyedIndexWithPartialBody<T[P], NonNullable<K[P]>>)
|
|
40
|
+
)
|
|
41
|
+
: never);
|
|
42
|
+
} &
|
|
43
|
+
// 2. All other fields in T (not in K) become OPTIONAL
|
|
44
|
+
DeepPartial<Omit<T, keyof K>>;
|
|
45
|
+
|
|
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>>;
|
|
49
|
+
|
|
50
|
+
export type TemplateValue = 1 | -1 | true;
|
|
51
|
+
export type TemplatePart<T extends TemplateValue = TemplateValue> = { path: string[], value: T, part: 'key' | 'sort' };
|
|
52
|
+
|
|
53
|
+
export interface KeyedIndex<
|
|
54
|
+
T extends ModelType,
|
|
55
|
+
K extends KeyedIndexSelection<T>,
|
|
56
|
+
S extends SortedIndexSelection<T>
|
|
57
|
+
> extends IndexConfig<'indexed:keyed'> {
|
|
58
|
+
key: K;
|
|
59
|
+
sort: S;
|
|
60
|
+
unique: boolean;
|
|
61
|
+
keyTemplate: TemplatePart<true>[];
|
|
62
|
+
sortTemplate: TemplatePart<1 | -1>[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SortedIndex<
|
|
66
|
+
T extends ModelType,
|
|
67
|
+
K extends KeyedIndexSelection<T>,
|
|
68
|
+
S extends SortedIndexSelection<T>
|
|
69
|
+
> extends IndexConfig<'indexed:sorted'> {
|
|
70
|
+
key: K;
|
|
71
|
+
sort: S;
|
|
72
|
+
keyTemplate: TemplatePart<true>[];
|
|
73
|
+
sortTemplate: TemplatePart<1 | -1>[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type SingleItemIndex<
|
|
77
|
+
T extends ModelType,
|
|
78
|
+
K extends KeyedIndexSelection<T> = Any,
|
|
79
|
+
S extends SortedIndexSelection<T> = Any
|
|
80
|
+
> = KeyedIndex<T, K, S> | SortedIndex<T, K, S>;
|
|
81
|
+
|
|
82
|
+
export type AllIndexes<
|
|
83
|
+
T extends ModelType,
|
|
84
|
+
K extends KeyedIndexSelection<T> = Any,
|
|
85
|
+
S extends SortedIndexSelection<T> = Any
|
|
86
|
+
> = KeyedIndex<T, K, S> | SortedIndex<T, K, S>;
|
|
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,96 @@
|
|
|
1
|
+
import type { ModelType, ModelBasicSupport, OptionalId } from '@travetto/model';
|
|
2
|
+
import type { Class } from '@travetto/runtime';
|
|
3
|
+
import type {
|
|
4
|
+
KeyedIndexSelection, KeyedIndexBody, SortedIndexSelection, SortedIndex,
|
|
5
|
+
SingleItemIndex, FullKeyedIndexBody, FullKeyedIndexWithPartialBody
|
|
6
|
+
} 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
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Support for simple indexed activity
|
|
20
|
+
*
|
|
21
|
+
* @concrete
|
|
22
|
+
*/
|
|
23
|
+
export interface ModelIndexedSupport extends ModelBasicSupport {
|
|
24
|
+
/**
|
|
25
|
+
* Get entity by index as defined by fields of idx and the body fields
|
|
26
|
+
* @param cls The type to search by
|
|
27
|
+
* @param idx The index to search against
|
|
28
|
+
* @param body The payload of fields needed to search
|
|
29
|
+
*/
|
|
30
|
+
getByIndex<
|
|
31
|
+
T extends ModelType,
|
|
32
|
+
K extends KeyedIndexSelection<T>,
|
|
33
|
+
S extends SortedIndexSelection<T>
|
|
34
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delete entity by index as defined by fields of idx and the body fields
|
|
38
|
+
* @param cls The type to search by
|
|
39
|
+
* @param idx The index to search against
|
|
40
|
+
* @param body The payload of fields needed to search
|
|
41
|
+
*/
|
|
42
|
+
deleteByIndex<
|
|
43
|
+
T extends ModelType,
|
|
44
|
+
K extends KeyedIndexSelection<T>,
|
|
45
|
+
S extends SortedIndexSelection<T>
|
|
46
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Upsert by index, allowing the index to act as a primary key
|
|
50
|
+
* @param cls The type to create for
|
|
51
|
+
* @param idx The index to use
|
|
52
|
+
* @param body The document to potentially store
|
|
53
|
+
*/
|
|
54
|
+
upsertByIndex<
|
|
55
|
+
T extends ModelType,
|
|
56
|
+
K extends KeyedIndexSelection<T>,
|
|
57
|
+
S extends SortedIndexSelection<T>
|
|
58
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update by index
|
|
62
|
+
* @param cls The type to update for
|
|
63
|
+
* @param idx The index to update by
|
|
64
|
+
* @param body The document to update
|
|
65
|
+
*/
|
|
66
|
+
updateByIndex<
|
|
67
|
+
T extends ModelType,
|
|
68
|
+
K extends KeyedIndexSelection<T>,
|
|
69
|
+
S extends SortedIndexSelection<T>
|
|
70
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Update partial by index
|
|
74
|
+
* @param cls The type to update for
|
|
75
|
+
* @param idx The index to update by
|
|
76
|
+
* @param body The partial document to update
|
|
77
|
+
*/
|
|
78
|
+
updatePartialByIndex<
|
|
79
|
+
T extends ModelType,
|
|
80
|
+
K extends KeyedIndexSelection<T>,
|
|
81
|
+
S extends SortedIndexSelection<T>
|
|
82
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List entity by ranged index as defined by fields of idx
|
|
86
|
+
* @param cls The type to search by
|
|
87
|
+
* @param idx The index to search against
|
|
88
|
+
* @param body The payload of fields needed to search
|
|
89
|
+
* @param options The configuration for listing
|
|
90
|
+
*/
|
|
91
|
+
listByIndex<
|
|
92
|
+
T extends ModelType,
|
|
93
|
+
S extends SortedIndexSelection<T>,
|
|
94
|
+
K extends KeyedIndexSelection<T>
|
|
95
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, options?: ListPageOptions): Promise<ListPageResult<T>>;
|
|
96
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { castTo, type Class, hasFunction } from '@travetto/runtime';
|
|
2
|
+
import { type ModelType, type ModelCrudSupport, type OptionalId, NotFoundError } from '@travetto/model';
|
|
3
|
+
|
|
4
|
+
import type { ModelIndexedSupport } from './types/service.ts';
|
|
5
|
+
import type { KeyedIndexSelection, SingleItemIndex, SortedIndexSelection } from './types/indexes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utils for working with indexed model services
|
|
9
|
+
*/
|
|
10
|
+
export class ModelIndexedUtil {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for determining if service supports indexed operation
|
|
14
|
+
*/
|
|
15
|
+
static isSupported = hasFunction<ModelIndexedSupport>('getByIndex');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Naive upsert by index
|
|
19
|
+
* @param service
|
|
20
|
+
* @param cls
|
|
21
|
+
* @param idx
|
|
22
|
+
* @param body
|
|
23
|
+
*/
|
|
24
|
+
static async naiveUpsert<
|
|
25
|
+
T extends ModelType,
|
|
26
|
+
K extends KeyedIndexSelection<T>,
|
|
27
|
+
S extends SortedIndexSelection<T>
|
|
28
|
+
>(
|
|
29
|
+
service: ModelIndexedSupport & ModelCrudSupport,
|
|
30
|
+
cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
try {
|
|
33
|
+
return await this.naiveUpdate(service, cls, idx, body);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof NotFoundError) {
|
|
36
|
+
return await service.create(cls, body);
|
|
37
|
+
} else {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Naive update by index
|
|
45
|
+
* @param service
|
|
46
|
+
* @param cls
|
|
47
|
+
* @param idx
|
|
48
|
+
* @param body
|
|
49
|
+
*/
|
|
50
|
+
static async naiveUpdate<
|
|
51
|
+
T extends ModelType,
|
|
52
|
+
K extends KeyedIndexSelection<T>,
|
|
53
|
+
S extends SortedIndexSelection<T>
|
|
54
|
+
>(
|
|
55
|
+
service: ModelIndexedSupport & ModelCrudSupport,
|
|
56
|
+
cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
const { id } = await service.getByIndex(cls, idx, castTo(body));
|
|
59
|
+
body.id = id;
|
|
60
|
+
return await service.update(cls, castTo(body));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @jsxImportSource @travetto/doc/support */
|
|
2
|
+
import { d, type DocJSXElementByFn, type DocJSXElement, DocFileUtil } from '@travetto/doc';
|
|
3
|
+
import { Runtime, toConcrete } from '@travetto/runtime';
|
|
4
|
+
|
|
5
|
+
import type { ModelIndexedSupport } from '../src/types/service.ts';
|
|
6
|
+
|
|
7
|
+
const toLink = (title: string, target: Function): DocJSXElementByFn<'CodeLink'> =>
|
|
8
|
+
d.codeLink(title, Runtime.getSourceFile(target), new RegExp(`\\binterface\\s+${target.name}`));
|
|
9
|
+
|
|
10
|
+
export const Links = {
|
|
11
|
+
Indexed: toLink('Indexed', toConcrete<ModelIndexedSupport>()),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ModelIndexedTypes = (fn: Function): DocJSXElement[] => {
|
|
15
|
+
const { content } = DocFileUtil.readSource(fn);
|
|
16
|
+
const found: DocJSXElementByFn<'CodeLink'>[] = [];
|
|
17
|
+
const seen = new Set<string>();
|
|
18
|
+
for (const [, key] of content.matchAll(/Model(Indexed)Support/g)) {
|
|
19
|
+
if (!seen.has(key) && key in Links) {
|
|
20
|
+
seen.add(key);
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
22
|
+
const link = Links[key as keyof typeof Links];
|
|
23
|
+
found.push(link);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return found.map(type => <li>{type}</li>);
|
|
27
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Suite, Test } from '@travetto/test';
|
|
4
|
+
import { Schema } from '@travetto/schema';
|
|
5
|
+
import { castTo, TimeUtil } from '@travetto/runtime';
|
|
6
|
+
import { Model, NotFoundError } from '@travetto/model';
|
|
7
|
+
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
8
|
+
|
|
9
|
+
import type { ModelIndexedSupport } from '../../src/types/service.ts';
|
|
10
|
+
import { keyedIndex, sortedIndex } from '../../src/indexes.ts';
|
|
11
|
+
import { IndexedFieldError } from '../../__index__.ts';
|
|
12
|
+
|
|
13
|
+
@Model('index_user')
|
|
14
|
+
class User {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userNameIndex = keyedIndex(User, {
|
|
20
|
+
name: 'userName',
|
|
21
|
+
key: { name: true }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
@Model('index_user_2')
|
|
25
|
+
class User2 {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Model()
|
|
31
|
+
class User3 {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
age: number;
|
|
35
|
+
color?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const userAgeIndex = sortedIndex(User3, {
|
|
39
|
+
name: 'userAge',
|
|
40
|
+
key: { name: true },
|
|
41
|
+
sort: { age: 1 }
|
|
42
|
+
});
|
|
43
|
+
const userAgeReversedIndex = sortedIndex(User3, {
|
|
44
|
+
name: 'userAgeReverse',
|
|
45
|
+
key: { name: true },
|
|
46
|
+
sort: { age: -1 }
|
|
47
|
+
});
|
|
48
|
+
const userAgeNoKeyIndex = sortedIndex(User3, {
|
|
49
|
+
name: 'userAgeNoKey',
|
|
50
|
+
key: {},
|
|
51
|
+
sort: { age: 1 }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
@Schema()
|
|
55
|
+
class Child {
|
|
56
|
+
name: string;
|
|
57
|
+
age: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Model()
|
|
61
|
+
class User4 {
|
|
62
|
+
id: string;
|
|
63
|
+
createdDate?: Date = new Date();
|
|
64
|
+
color: string;
|
|
65
|
+
child: Child;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const childAgeIndex = sortedIndex(User4, {
|
|
69
|
+
name: 'childAge',
|
|
70
|
+
key: { child: { name: true } },
|
|
71
|
+
sort: { child: { age: 1 } }
|
|
72
|
+
});
|
|
73
|
+
const nameCreatedIndex = sortedIndex(User4, {
|
|
74
|
+
name: 'nameCreated',
|
|
75
|
+
key: { child: { name: true } },
|
|
76
|
+
sort: { createdDate: 1 }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
@Suite()
|
|
80
|
+
export abstract class ModelIndexedSuite extends BaseModelSuite<ModelIndexedSupport> {
|
|
81
|
+
|
|
82
|
+
indexLimitSkew = 0;
|
|
83
|
+
supportsDeepIndexes = true;
|
|
84
|
+
|
|
85
|
+
@Test()
|
|
86
|
+
async writeAndRead() {
|
|
87
|
+
const service = await this.service;
|
|
88
|
+
|
|
89
|
+
await service.create(User, User.from({ name: 'bob1' }));
|
|
90
|
+
await service.create(User, User.from({ name: 'bob2' }));
|
|
91
|
+
|
|
92
|
+
const found1 = await service.getByIndex(User, userNameIndex, {
|
|
93
|
+
name: 'bob1'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
assert(found1.name === 'bob1');
|
|
97
|
+
|
|
98
|
+
const found2 = await service.getByIndex(User, userNameIndex, {
|
|
99
|
+
name: 'bob2'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert(found2.name === 'bob2');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test()
|
|
106
|
+
async readMissingValue() {
|
|
107
|
+
const service = await this.service;
|
|
108
|
+
await assert.rejects(() => service.getByIndex(User, userNameIndex, { name: 'jim' }), NotFoundError);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test()
|
|
112
|
+
async readDifferentType() {
|
|
113
|
+
const service = await this.service;
|
|
114
|
+
await assert.rejects(() => service.getByIndex(User2, userNameIndex, { name: 'jim' }), NotFoundError);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test()
|
|
118
|
+
async queryMultiple() {
|
|
119
|
+
const service = await this.service;
|
|
120
|
+
|
|
121
|
+
await service.create(User3, User3.from({ name: 'bob', age: 20 }));
|
|
122
|
+
await service.create(User3, User3.from({ name: 'bob', age: 30, color: 'green' }));
|
|
123
|
+
|
|
124
|
+
const found = await service.getByIndex(User3, userAgeIndex, { name: 'bob', age: 30 });
|
|
125
|
+
|
|
126
|
+
assert(found.color === 'green');
|
|
127
|
+
|
|
128
|
+
const found2 = await service.getByIndex(User3, userAgeIndex, { name: 'bob', age: 20 });
|
|
129
|
+
|
|
130
|
+
assert(!found2.color);
|
|
131
|
+
|
|
132
|
+
// @ts-expect-error
|
|
133
|
+
await assert.rejects(() => service.getByIndex(User3, userAgeIndex, { name: 'bob' }), IndexedFieldError);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test()
|
|
137
|
+
async queryList() {
|
|
138
|
+
const service = await this.service;
|
|
139
|
+
|
|
140
|
+
await service.create(User3, User3.from({ name: 'bob', age: 40, color: 'blue' }));
|
|
141
|
+
await service.create(User3, User3.from({ name: 'bob', age: 30, color: 'red' }));
|
|
142
|
+
await service.create(User3, User3.from({ name: 'bob', age: 50, color: 'green' }));
|
|
143
|
+
|
|
144
|
+
const { items: arr } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
|
|
145
|
+
|
|
146
|
+
console.error(arr);
|
|
147
|
+
|
|
148
|
+
assert(arr[0].color === 'red');
|
|
149
|
+
assert(arr[0].name === 'bob');
|
|
150
|
+
assert(arr[1].color === 'blue');
|
|
151
|
+
assert(arr[1].name === 'bob');
|
|
152
|
+
assert(arr[2].color === 'green');
|
|
153
|
+
assert(arr[2].name === 'bob');
|
|
154
|
+
|
|
155
|
+
// @ts-expect-error
|
|
156
|
+
await assert.rejects(() => service.listByIndex(User3, userAgeIndex, {}), IndexedFieldError);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test()
|
|
160
|
+
async queryListNoSelectedKeys() {
|
|
161
|
+
const service = await this.service;
|
|
162
|
+
|
|
163
|
+
await service.create(User3, User3.from({ name: 'charlie', age: 40, color: 'blue' }));
|
|
164
|
+
await service.create(User3, User3.from({ name: 'alice', age: 30, color: 'red' }));
|
|
165
|
+
await service.create(User3, User3.from({ name: 'bob', age: 50, color: 'green' }));
|
|
166
|
+
|
|
167
|
+
const { items: arr } = await service.listByIndex(User3, userAgeNoKeyIndex, {});
|
|
168
|
+
|
|
169
|
+
assert(arr[0].name === 'alice' && arr[0].age === 30);
|
|
170
|
+
assert(arr[1].name === 'charlie' && arr[1].age === 40);
|
|
171
|
+
assert(arr[2].name === 'bob' && arr[2].age === 50);
|
|
172
|
+
|
|
173
|
+
const found = await service.getByIndex(User3, userAgeNoKeyIndex, { age: 40 });
|
|
174
|
+
assert(found.name === 'charlie');
|
|
175
|
+
|
|
176
|
+
// @ts-expect-error
|
|
177
|
+
await assert.rejects(() => service.getByIndex(User3, userAgeNoKeyIndex, {}), IndexedFieldError);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test({ skip: (self) => !castTo<ModelIndexedSuite>(self).supportsDeepIndexes })
|
|
181
|
+
async queryDeepList() {
|
|
182
|
+
const service = await this.service;
|
|
183
|
+
|
|
184
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, color: 'blue' }));
|
|
185
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, color: 'red' }));
|
|
186
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, color: 'green' }));
|
|
187
|
+
|
|
188
|
+
const { items: arr } = await service.listByIndex(User4, childAgeIndex, { child: { name: 'bob' } });
|
|
189
|
+
assert(arr[0].color === 'red' && arr[0].child.name === 'bob' && arr[0].child.age === 30);
|
|
190
|
+
assert(arr[1].color === 'blue' && arr[1].child.name === 'bob' && arr[1].child.age === 40);
|
|
191
|
+
assert(arr[2].color === 'green' && arr[2].child.name === 'bob' && arr[2].child.age === 50);
|
|
192
|
+
|
|
193
|
+
// @ts-expect-error
|
|
194
|
+
await assert.rejects(() => service.listByIndex(User4, childAgeIndex, {}), IndexedFieldError);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Test({ skip: (self) => !castTo<ModelIndexedSuite>(self).supportsDeepIndexes })
|
|
198
|
+
async queryComplexDateList() {
|
|
199
|
+
const service = await this.service;
|
|
200
|
+
|
|
201
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 40 }, createdDate: TimeUtil.fromNow('3d'), color: 'blue' }));
|
|
202
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 30 }, createdDate: TimeUtil.fromNow('2d'), color: 'red' }));
|
|
203
|
+
await service.create(User4, User4.from({ child: { name: 'bob', age: 50 }, createdDate: TimeUtil.fromNow('-1d'), color: 'green' }));
|
|
204
|
+
|
|
205
|
+
const { items: arr } = await service.listByIndex(User4, nameCreatedIndex, { child: { name: 'bob' } });
|
|
206
|
+
|
|
207
|
+
assert(arr[0].color === 'green' && arr[0].child.name === 'bob' && arr[0].child.age === 50);
|
|
208
|
+
assert(arr[1].color === 'red' && arr[1].child.name === 'bob' && arr[1].child.age === 30);
|
|
209
|
+
assert(arr[2].color === 'blue' && arr[2].child.name === 'bob' && arr[2].child.age === 40);
|
|
210
|
+
|
|
211
|
+
// @ts-expect-error
|
|
212
|
+
await assert.rejects(() => service.listByIndex(User4, nameCreatedIndex, {}), IndexedFieldError);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Test()
|
|
216
|
+
async upsertByIndex() {
|
|
217
|
+
const service = await this.service;
|
|
218
|
+
|
|
219
|
+
const user1 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 40, color: 'blue' });
|
|
220
|
+
const user2 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 40, color: 'green' });
|
|
221
|
+
const user3 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 40, color: 'red' });
|
|
222
|
+
|
|
223
|
+
const { items: arr } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
|
|
224
|
+
assert(arr.length === 1);
|
|
225
|
+
|
|
226
|
+
assert(user1.id === user2.id);
|
|
227
|
+
assert(user2.id === user3.id);
|
|
228
|
+
assert(user1.color === 'blue');
|
|
229
|
+
assert(user3.color === 'red');
|
|
230
|
+
|
|
231
|
+
const user4 = await service.upsertByIndex(User3, userAgeIndex, { name: 'bob', age: 30, color: 'red' });
|
|
232
|
+
const { items: arr2 } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
|
|
233
|
+
assert(arr2.length === 2);
|
|
234
|
+
|
|
235
|
+
await service.deleteByIndex(User3, userAgeIndex, user1);
|
|
236
|
+
|
|
237
|
+
const { items: arr3 } = await service.listByIndex(User3, userAgeIndex, { name: 'bob' });
|
|
238
|
+
assert(arr3.length === 1);
|
|
239
|
+
assert(arr3[0].id === user4.id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Test()
|
|
243
|
+
async updateByIndex() {
|
|
244
|
+
const service = await this.service;
|
|
245
|
+
|
|
246
|
+
const created = await service.create(User3, User3.from({ name: 'alice', age: 25, color: 'blue' }));
|
|
247
|
+
|
|
248
|
+
const updated = await service.updateByIndex(User3, userAgeIndex, { ...created, color: 'red' });
|
|
249
|
+
|
|
250
|
+
assert(updated.id === created.id);
|
|
251
|
+
assert(updated.name === 'alice');
|
|
252
|
+
assert(updated.age === 25);
|
|
253
|
+
assert(updated.color === 'red');
|
|
254
|
+
|
|
255
|
+
const found = await service.getByIndex(User3, userAgeIndex, { name: 'alice', age: 25 });
|
|
256
|
+
assert(found.color === 'red');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Test()
|
|
260
|
+
async updatePartialByIndex() {
|
|
261
|
+
const service = await this.service;
|
|
262
|
+
|
|
263
|
+
const created = await service.create(User3, User3.from({ name: 'carol', age: 35, color: 'green' }));
|
|
264
|
+
|
|
265
|
+
const updated = await service.updatePartialByIndex(User3, userAgeIndex, { name: 'carol', age: 35, color: 'yellow' });
|
|
266
|
+
|
|
267
|
+
assert(updated.id === created.id);
|
|
268
|
+
assert(updated.name === 'carol');
|
|
269
|
+
assert(updated.age === 35);
|
|
270
|
+
assert(updated.color === 'yellow');
|
|
271
|
+
|
|
272
|
+
const found = await service.getByIndex(User3, userAgeIndex, { name: 'carol', age: 35 });
|
|
273
|
+
assert(found.color === 'yellow');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@Test()
|
|
277
|
+
async paginateByIndex() {
|
|
278
|
+
const service = await this.service;
|
|
279
|
+
|
|
280
|
+
const allColors = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+,<.>/?;:\'"[{]}|`~'.repeat(2).split('');
|
|
281
|
+
|
|
282
|
+
for (const [i, color] of allColors.entries()) {
|
|
283
|
+
await service.create(User3, User3.from({ name: 'page', age: (i + 1) * 10, color }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const limit = 7;
|
|
287
|
+
const items: string[] = [];
|
|
288
|
+
let offset: string | undefined;
|
|
289
|
+
|
|
290
|
+
do {
|
|
291
|
+
const page = await service.listByIndex(User3, userAgeIndex, { name: 'page' }, { limit, offset });
|
|
292
|
+
items.push(...page.items.map(u => u.color!));
|
|
293
|
+
offset = page.nextOffset;
|
|
294
|
+
} while (offset);
|
|
295
|
+
|
|
296
|
+
assert(items.length === allColors.length);
|
|
297
|
+
assert.deepEqual(items, allColors);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@Test()
|
|
301
|
+
async paginateByIndexReverse() {
|
|
302
|
+
const service = await this.service;
|
|
303
|
+
|
|
304
|
+
const allColors = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+,<.>/?;:\'"[{]}|`~'.repeat(2).split('');
|
|
305
|
+
|
|
306
|
+
for (const [i, color] of allColors.entries()) {
|
|
307
|
+
await service.create(User3, User3.from({ name: 'page', age: (i + 1) * 10, color }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const limit = 7;
|
|
311
|
+
const items: string[] = [];
|
|
312
|
+
let offset: string | undefined;
|
|
313
|
+
|
|
314
|
+
do {
|
|
315
|
+
const page = await service.listByIndex(User3, userAgeReversedIndex, { name: 'page' }, { limit, offset });
|
|
316
|
+
items.push(...page.items.map(u => u.color!));
|
|
317
|
+
offset = page.nextOffset;
|
|
318
|
+
} while (offset);
|
|
319
|
+
|
|
320
|
+
assert(items.length === allColors.length);
|
|
321
|
+
assert.deepEqual(items, allColors.toReversed());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
|
|
3
|
+
import { Suite, Test } from '@travetto/test';
|
|
4
|
+
import { castTo } from '@travetto/runtime';
|
|
5
|
+
import { Discriminated } from '@travetto/schema';
|
|
6
|
+
import { Model, NotFoundError, SubTypeNotSupportedError } from '@travetto/model';
|
|
7
|
+
|
|
8
|
+
import { BaseModelSuite } from '@travetto/model/support/test/base.ts';
|
|
9
|
+
|
|
10
|
+
import type { ModelIndexedSupport } from '../../src/types/service.ts';
|
|
11
|
+
import { ModelIndexedUtil } from '../../src/util.ts';
|
|
12
|
+
import { keyedIndex } from '../../src/indexes.ts';
|
|
13
|
+
|
|
14
|
+
@Model()
|
|
15
|
+
@Discriminated('type')
|
|
16
|
+
export class IndexedWorker {
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
name: string;
|
|
20
|
+
age?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const workerNameIndex = keyedIndex(IndexedWorker, {
|
|
24
|
+
name: 'worker-name',
|
|
25
|
+
key: { name: true, age: true }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
@Model()
|
|
29
|
+
export class IndexedDoctor extends IndexedWorker {
|
|
30
|
+
specialty: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Model()
|
|
34
|
+
export class IndexedFirefighter extends IndexedWorker {
|
|
35
|
+
firehouse: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Model()
|
|
39
|
+
export class IndexedEngineer extends IndexedWorker {
|
|
40
|
+
major: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Suite()
|
|
44
|
+
export abstract class ModelIndexedPolymorphismSuite extends BaseModelSuite<ModelIndexedSupport> {
|
|
45
|
+
|
|
46
|
+
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
47
|
+
async polymorphicIndexGet() {
|
|
48
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
49
|
+
const now = 30;
|
|
50
|
+
const [doc, fire, eng] = [
|
|
51
|
+
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
52
|
+
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
53
|
+
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
57
|
+
|
|
58
|
+
const result = await service.getByIndex(IndexedWorker, workerNameIndex, {
|
|
59
|
+
age: now,
|
|
60
|
+
name: 'rob'
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert(result instanceof IndexedFirefighter);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const res2 = await service.getByIndex(IndexedFirefighter, workerNameIndex, {
|
|
67
|
+
age: now,
|
|
68
|
+
name: 'rob'
|
|
69
|
+
});
|
|
70
|
+
assert(res2 instanceof IndexedFirefighter); // If service allows for get by subtype
|
|
71
|
+
} catch (err) {
|
|
72
|
+
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError); // If it does not
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
|
|
77
|
+
async polymorphicIndexDelete() {
|
|
78
|
+
const service: ModelIndexedSupport = castTo(await this.service);
|
|
79
|
+
const now = 30;
|
|
80
|
+
const [doc, fire, eng] = [
|
|
81
|
+
IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
|
|
82
|
+
IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
|
|
83
|
+
IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
await this.saveAll(IndexedWorker, [doc, fire, eng]);
|
|
87
|
+
|
|
88
|
+
assert(await this.getSize(IndexedWorker) === 3);
|
|
89
|
+
|
|
90
|
+
await service.deleteByIndex(IndexedWorker, workerNameIndex, {
|
|
91
|
+
age: now,
|
|
92
|
+
name: 'bob'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
assert(await this.getSize(IndexedWorker) === 2);
|
|
96
|
+
assert(await this.getSize(IndexedDoctor) === 0);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await service.deleteByIndex(IndexedFirefighter, workerNameIndex, {
|
|
100
|
+
age: now,
|
|
101
|
+
name: 'rob'
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await service.deleteByIndex(IndexedEngineer, workerNameIndex, {
|
|
109
|
+
age: now,
|
|
110
|
+
name: 'bob'
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|