@travetto/model-elasticsearch 8.0.0-alpha.11 → 8.0.0-alpha.13
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 +1 -1
- package/package.json +6 -6
- package/src/service.ts +94 -109
- package/support/service.elasticsearch.ts +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ 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)
|
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.13",
|
|
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.12",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.12",
|
|
34
|
+
"@travetto/model-indexed": "^8.0.0-alpha.13",
|
|
35
|
+
"@travetto/model-query": "^8.0.0-alpha.12"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.17"
|
|
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
|
|
|
@@ -428,7 +429,7 @@ export class ElasticsearchModelService implements
|
|
|
428
429
|
if (!result.hits.hits.length) {
|
|
429
430
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
430
431
|
}
|
|
431
|
-
return this
|
|
432
|
+
return this.#postLoad(cls, result.hits.hits[0]);
|
|
432
433
|
|
|
433
434
|
}
|
|
434
435
|
|
|
@@ -487,22 +488,14 @@ export class ElasticsearchModelService implements
|
|
|
487
488
|
cls: Class<T>,
|
|
488
489
|
idx: SortedIndex<T, K, S>,
|
|
489
490
|
body: KeyedIndexBody<T, K>,
|
|
490
|
-
options?:
|
|
491
|
-
): Promise<
|
|
491
|
+
options?: ModelPageOptions,
|
|
492
|
+
): Promise<ModelPageResult<T>> {
|
|
492
493
|
const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
|
|
493
494
|
const items: T[] = [];
|
|
494
495
|
let lastNextOffset: estypes.SortResults | undefined;
|
|
495
|
-
for await (const {
|
|
496
|
+
for await (const { items: fetched, nextOffset } of this.#scrollIndex(cls, idx, body, { limit: 100, ...options, offset })) {
|
|
496
497
|
lastNextOffset = nextOffset;
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
items.push(await this.postLoad(cls, hit));
|
|
500
|
-
} catch (error) {
|
|
501
|
-
if (!(error instanceof NotFoundError)) {
|
|
502
|
-
throw error;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
498
|
+
items.push(...fetched);
|
|
506
499
|
}
|
|
507
500
|
return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
|
|
508
501
|
}
|
|
@@ -515,21 +508,13 @@ export class ElasticsearchModelService implements
|
|
|
515
508
|
cls: Class<T>,
|
|
516
509
|
idx: SortedIndex<T, K, S>,
|
|
517
510
|
body: KeyedIndexBody<T, K>,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
yield await this.postLoad(cls, hit);
|
|
523
|
-
} catch (error) {
|
|
524
|
-
if (!(error instanceof NotFoundError)) {
|
|
525
|
-
throw error;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
511
|
+
options?: ModelListOptions
|
|
512
|
+
): AsyncIterable<T[]> {
|
|
513
|
+
for await (const { items } of this.#scrollIndex(cls, idx, body, options)) {
|
|
514
|
+
yield items;
|
|
529
515
|
}
|
|
530
516
|
}
|
|
531
517
|
|
|
532
|
-
|
|
533
518
|
// Query
|
|
534
519
|
async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
535
520
|
await QueryVerifier.verify(cls, query);
|
|
@@ -537,7 +522,7 @@ export class ElasticsearchModelService implements
|
|
|
537
522
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
|
|
538
523
|
const results = await this.execSearch(cls, search);
|
|
539
524
|
const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
|
|
540
|
-
return Promise.all(results.hits.hits.map(hit => this
|
|
525
|
+
return Promise.all(results.hits.hits.map(hit => this.#postLoad(cls, hit).then(item => {
|
|
541
526
|
if (shouldRemoveIds) {
|
|
542
527
|
delete castTo<OptionalId<T>>(item).id;
|
|
543
528
|
}
|
|
@@ -639,7 +624,7 @@ export class ElasticsearchModelService implements
|
|
|
639
624
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
640
625
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
641
626
|
const result = await this.execSearch(cls, search);
|
|
642
|
-
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)));
|
|
643
628
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
|
|
644
629
|
}
|
|
645
630
|
|