@travetto/model-elasticsearch 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 +2 -2
- package/package.json +6 -6
- package/src/service.ts +107 -98
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ npm install @travetto/model-elasticsearch
|
|
|
13
13
|
yarn add @travetto/model-elasticsearch
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides an [elasticsearch](https://elastic.co)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [elasticsearch](https://elastic.co). In development mode, [ElasticsearchModelService](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch/src/service.ts#
|
|
16
|
+
This module provides an [elasticsearch](https://elastic.co)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [elasticsearch](https://elastic.co). In development mode, [ElasticsearchModelService](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch/src/service.ts#L40) will also modify the [elasticsearch](https://elastic.co) schema in real time to minimize impact to development.
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
19
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
20
|
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L64)
|
|
21
21
|
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
22
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#
|
|
22
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L15)
|
|
23
23
|
* [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
|
|
24
24
|
* [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
|
|
25
25
|
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-elasticsearch",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@elastic/elasticsearch": "^9.3.4",
|
|
32
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-indexed": "^8.0.0-alpha.
|
|
35
|
-
"@travetto/model-query": "^8.0.0-alpha.
|
|
32
|
+
"@travetto/config": "^8.0.0-alpha.11",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.11",
|
|
34
|
+
"@travetto/model-indexed": "^8.0.0-alpha.12",
|
|
35
|
+
"@travetto/model-query": "^8.0.0-alpha.11"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.16"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type * as estypes from '@elastic/elasticsearch/api/types';
|
|
|
4
4
|
import {
|
|
5
5
|
type ModelCrudSupport, type BulkOperation, type BulkResponse, type ModelBulkSupport, type ModelExpirySupport,
|
|
6
6
|
type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
|
|
7
|
-
ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
|
|
7
|
+
ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
|
|
8
|
+
type ModelListOptions
|
|
8
9
|
} from '@travetto/model';
|
|
9
10
|
import { ShutdownManager, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
|
|
10
11
|
import { BindUtil } from '@travetto/schema';
|
|
@@ -15,8 +16,8 @@ import {
|
|
|
15
16
|
ModelQueryCrudUtil, type ModelQueryFacet,
|
|
16
17
|
} from '@travetto/model-query';
|
|
17
18
|
import {
|
|
18
|
-
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type
|
|
19
|
-
type SingleItemIndex, type SortedIndexSelection, type
|
|
19
|
+
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
|
|
20
|
+
type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, type FullKeyedIndexBody,
|
|
20
21
|
type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex
|
|
21
22
|
} from '@travetto/model-indexed';
|
|
22
23
|
|
|
@@ -51,6 +52,73 @@ export class ElasticsearchModelService implements
|
|
|
51
52
|
|
|
52
53
|
constructor(config: ElasticsearchModelConfig) { this.config = config; }
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Convert _id to id
|
|
57
|
+
*/
|
|
58
|
+
async #postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
|
|
59
|
+
let item = {
|
|
60
|
+
...(input._id ? { id: input._id } : {}),
|
|
61
|
+
...input._source!,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
item = await ModelCrudUtil.load(cls, item);
|
|
65
|
+
|
|
66
|
+
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
67
|
+
|
|
68
|
+
if (expiresAt) {
|
|
69
|
+
const expiry = ModelExpiryUtil.getExpiryState(cls, item);
|
|
70
|
+
if (!expiry.expired) {
|
|
71
|
+
return item;
|
|
72
|
+
}
|
|
73
|
+
throw new NotFoundError(cls, item.id);
|
|
74
|
+
} else {
|
|
75
|
+
return item;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async * #scrollCollection<T extends ModelType>(
|
|
80
|
+
cls: Class<T>,
|
|
81
|
+
buildSearch: (offset?: estypes.SortResults) => estypes.SearchRequest,
|
|
82
|
+
options?: ModelPageOptions<estypes.SortResults> & ModelListOptions
|
|
83
|
+
): AsyncIterable<{ items: T[], nextOffset?: estypes.SortResults | undefined }> {
|
|
84
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
85
|
+
const batchSize = Math.min(limit, options?.batchSizeHint ?? 100);
|
|
86
|
+
|
|
87
|
+
let search: estypes.SearchResponse<T> = await this.execSearch<T>(cls, {
|
|
88
|
+
size: batchSize,
|
|
89
|
+
...buildSearch(options?.offset),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let hits = search.hits.hits;
|
|
93
|
+
let produced = 0;
|
|
94
|
+
|
|
95
|
+
while (produced < limit && hits.length && !(options?.abort?.aborted)) {
|
|
96
|
+
hits = search.hits.hits;
|
|
97
|
+
produced += hits.length;
|
|
98
|
+
if (produced > limit) {
|
|
99
|
+
hits = hits.slice(0, limit - (produced - hits.length));
|
|
100
|
+
}
|
|
101
|
+
const items = await ModelCrudUtil.filterOutNotFound(
|
|
102
|
+
hits.map(hit => this.#postLoad(cls, hit))
|
|
103
|
+
);
|
|
104
|
+
const nextOffset = hits.at(-1)?.sort;
|
|
105
|
+
yield { items, nextOffset };
|
|
106
|
+
if (search._scroll_id) {
|
|
107
|
+
search = await this.client.scroll({ scroll_id: search._scroll_id });
|
|
108
|
+
} else if (nextOffset) {
|
|
109
|
+
search = await this.execSearch<T>(cls, {
|
|
110
|
+
size: batchSize,
|
|
111
|
+
...buildSearch(nextOffset),
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
hits = [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (search._scroll_id) {
|
|
118
|
+
await this.client.clearScroll({ scroll_id: search._scroll_id }).catch(() => { });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
54
122
|
async * #scrollIndex<
|
|
55
123
|
T extends ModelType,
|
|
56
124
|
K extends KeyedIndexSelection<T>,
|
|
@@ -59,47 +127,19 @@ export class ElasticsearchModelService implements
|
|
|
59
127
|
cls: Class<T>,
|
|
60
128
|
idx: SortedIndex<T, K, S>,
|
|
61
129
|
body: KeyedIndexBody<T, K>,
|
|
62
|
-
options?:
|
|
130
|
+
options?: ModelPageOptions<estypes.SortResults> & ModelListOptions
|
|
63
131
|
): AsyncIterable<{
|
|
64
|
-
|
|
132
|
+
items: T[];
|
|
65
133
|
nextOffset?: estypes.SortResults | undefined;
|
|
66
134
|
}> {
|
|
67
|
-
const limit = options?.limit ?? 100;
|
|
68
135
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
69
|
-
|
|
70
|
-
let search = await this.execSearch<T>(cls, {
|
|
71
|
-
...(options?.offset ?
|
|
72
|
-
{ size: limit, search_after: options.offset } :
|
|
73
|
-
{ scroll: '2m', size: 100 }
|
|
74
|
-
),
|
|
136
|
+
yield* this.#scrollCollection(cls, (offset) => ({
|
|
75
137
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
76
138
|
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project())
|
|
77
139
|
),
|
|
140
|
+
search_after: offset,
|
|
78
141
|
sort: ElasticsearchQueryUtil.getSort(idx)
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
let hits = search.hits.hits.slice(0, limit);
|
|
82
|
-
let produced = hits.length;
|
|
83
|
-
|
|
84
|
-
if (hits.length) {
|
|
85
|
-
yield { hits, nextOffset: hits.at(-1)?.sort };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
while (produced < limit && search._scroll_id && hits.length) {
|
|
89
|
-
search = await this.client.scroll({ scroll_id: search._scroll_id, scroll: '2m' });
|
|
90
|
-
hits = search.hits.hits;
|
|
91
|
-
produced += hits.length;
|
|
92
|
-
if (produced > limit) {
|
|
93
|
-
hits = hits.slice(0, limit - (produced - hits.length));
|
|
94
|
-
}
|
|
95
|
-
if (hits.length) {
|
|
96
|
-
yield { hits, nextOffset: hits.at(-1)?.sort };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (search._scroll_id) {
|
|
101
|
-
await this.client.clearScroll({ scroll_id: search._scroll_id }).catch(() => { });
|
|
102
|
-
}
|
|
142
|
+
}), options);
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
@PostConstruct()
|
|
@@ -163,30 +203,6 @@ export class ElasticsearchModelService implements
|
|
|
163
203
|
return item;
|
|
164
204
|
}
|
|
165
205
|
|
|
166
|
-
/**
|
|
167
|
-
* Convert _id to id
|
|
168
|
-
*/
|
|
169
|
-
async postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
|
|
170
|
-
let item = {
|
|
171
|
-
...(input._id ? { id: input._id } : {}),
|
|
172
|
-
...input._source!,
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
item = await ModelCrudUtil.load(cls, item);
|
|
176
|
-
|
|
177
|
-
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
178
|
-
|
|
179
|
-
if (expiresAt) {
|
|
180
|
-
const expiry = ModelExpiryUtil.getExpiryState(cls, item);
|
|
181
|
-
if (!expiry.expired) {
|
|
182
|
-
return item;
|
|
183
|
-
}
|
|
184
|
-
throw new NotFoundError(cls, item.id);
|
|
185
|
-
} else {
|
|
186
|
-
return item;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
206
|
createStorage(): Promise<void> { return this.manager.createStorage(); }
|
|
191
207
|
deleteStorage(): Promise<void> { return this.manager.deleteStorage(); }
|
|
192
208
|
upsertModel(cls: Class): Promise<void> { return this.manager.upsertModel(cls); }
|
|
@@ -197,7 +213,7 @@ export class ElasticsearchModelService implements
|
|
|
197
213
|
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
198
214
|
try {
|
|
199
215
|
const result = await this.client.get<T>({ ...this.manager.getIdentity(cls), id });
|
|
200
|
-
return this
|
|
216
|
+
return this.#postLoad(cls, result);
|
|
201
217
|
} catch {
|
|
202
218
|
throw new NotFoundError(cls, id);
|
|
203
219
|
}
|
|
@@ -300,27 +316,12 @@ export class ElasticsearchModelService implements
|
|
|
300
316
|
return this.get(cls, id);
|
|
301
317
|
}
|
|
302
318
|
|
|
303
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
while (search.hits.hits.length > 0) {
|
|
311
|
-
for (const hit of search.hits.hits) {
|
|
312
|
-
try {
|
|
313
|
-
yield this.postLoad(cls, hit);
|
|
314
|
-
} catch (error) {
|
|
315
|
-
if (!(error instanceof NotFoundError)) {
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
search = await this.client.scroll({
|
|
320
|
-
scroll_id: search._scroll_id,
|
|
321
|
-
scroll: '2m'
|
|
322
|
-
});
|
|
323
|
-
}
|
|
319
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
320
|
+
for await (const batch of this.#scrollCollection(cls, () => ({
|
|
321
|
+
query: ElasticsearchQueryUtil.getSearchQuery(cls, {}),
|
|
322
|
+
scroll: '2m'
|
|
323
|
+
}), options)) {
|
|
324
|
+
yield batch.items;
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
327
|
|
|
@@ -419,6 +420,7 @@ export class ElasticsearchModelService implements
|
|
|
419
420
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
420
421
|
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
|
|
421
422
|
sort: true,
|
|
423
|
+
includeId: true,
|
|
422
424
|
emptyValue: { $exists: true }
|
|
423
425
|
}))
|
|
424
426
|
|
|
@@ -427,7 +429,7 @@ export class ElasticsearchModelService implements
|
|
|
427
429
|
if (!result.hits.hits.length) {
|
|
428
430
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
429
431
|
}
|
|
430
|
-
return this
|
|
432
|
+
return this.#postLoad(cls, result.hits.hits[0]);
|
|
431
433
|
|
|
432
434
|
}
|
|
433
435
|
|
|
@@ -442,6 +444,7 @@ export class ElasticsearchModelService implements
|
|
|
442
444
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
443
445
|
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
|
|
444
446
|
sort: true,
|
|
447
|
+
includeId: true,
|
|
445
448
|
emptyValue: { $exists: true }
|
|
446
449
|
}))
|
|
447
450
|
),
|
|
@@ -477,7 +480,7 @@ export class ElasticsearchModelService implements
|
|
|
477
480
|
return this.update(cls, item);
|
|
478
481
|
}
|
|
479
482
|
|
|
480
|
-
async
|
|
483
|
+
async pageByIndex<
|
|
481
484
|
T extends ModelType,
|
|
482
485
|
K extends KeyedIndexSelection<T>,
|
|
483
486
|
S extends SortedIndexSelection<T>
|
|
@@ -485,26 +488,32 @@ export class ElasticsearchModelService implements
|
|
|
485
488
|
cls: Class<T>,
|
|
486
489
|
idx: SortedIndex<T, K, S>,
|
|
487
490
|
body: KeyedIndexBody<T, K>,
|
|
488
|
-
options?:
|
|
489
|
-
): Promise<
|
|
491
|
+
options?: ModelPageOptions,
|
|
492
|
+
): Promise<ModelPageResult<T>> {
|
|
490
493
|
const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
|
|
491
494
|
const items: T[] = [];
|
|
492
495
|
let lastNextOffset: estypes.SortResults | undefined;
|
|
493
|
-
for await (const {
|
|
496
|
+
for await (const { items: fetched, nextOffset } of this.#scrollIndex(cls, idx, body, { limit: 100, ...options, offset })) {
|
|
494
497
|
lastNextOffset = nextOffset;
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
items.push(await this.postLoad(cls, hit));
|
|
498
|
-
} catch (error) {
|
|
499
|
-
if (!(error instanceof NotFoundError)) {
|
|
500
|
-
throw error;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
498
|
+
items.push(...fetched);
|
|
504
499
|
}
|
|
505
500
|
return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
|
|
506
501
|
}
|
|
507
502
|
|
|
503
|
+
async * listByIndex<
|
|
504
|
+
T extends ModelType,
|
|
505
|
+
K extends KeyedIndexSelection<T>,
|
|
506
|
+
S extends SortedIndexSelection<T>
|
|
507
|
+
>(
|
|
508
|
+
cls: Class<T>,
|
|
509
|
+
idx: SortedIndex<T, K, S>,
|
|
510
|
+
body: KeyedIndexBody<T, K>,
|
|
511
|
+
options?: ModelListOptions
|
|
512
|
+
): AsyncIterable<T[]> {
|
|
513
|
+
for await (const { items } of this.#scrollIndex(cls, idx, body, options)) {
|
|
514
|
+
yield items;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
508
517
|
|
|
509
518
|
// Query
|
|
510
519
|
async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
@@ -513,7 +522,7 @@ export class ElasticsearchModelService implements
|
|
|
513
522
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
|
|
514
523
|
const results = await this.execSearch(cls, search);
|
|
515
524
|
const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
|
|
516
|
-
return Promise.all(results.hits.hits.map(hit => this
|
|
525
|
+
return Promise.all(results.hits.hits.map(hit => this.#postLoad(cls, hit).then(item => {
|
|
517
526
|
if (shouldRemoveIds) {
|
|
518
527
|
delete castTo<OptionalId<T>>(item).id;
|
|
519
528
|
}
|
|
@@ -615,7 +624,7 @@ export class ElasticsearchModelService implements
|
|
|
615
624
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
616
625
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
617
626
|
const result = await this.execSearch(cls, search);
|
|
618
|
-
const all = await Promise.all(result.hits.hits.map(hit => this
|
|
627
|
+
const all = await Promise.all(result.hits.hits.map(hit => this.#postLoad(cls, hit)));
|
|
619
628
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
|
|
620
629
|
}
|
|
621
630
|
|