@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 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#L39) will also modify the [elasticsearch](https://elastic.co) schema in real time to minimize impact to development.
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.11",
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.10",
33
- "@travetto/model": "^8.0.0-alpha.10",
34
- "@travetto/model-indexed": "^8.0.0-alpha.11",
35
- "@travetto/model-query": "^8.0.0-alpha.10"
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.15"
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 ListPageOptions, ModelIndexedUtil,
19
- type SingleItemIndex, type SortedIndexSelection, type ListPageResult, type SortedIndex, type FullKeyedIndexBody,
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?: ListPageOptions<estypes.SortResults>
130
+ options?: ModelPageOptions<estypes.SortResults> & ModelListOptions
63
131
  ): AsyncIterable<{
64
- hits: estypes.SearchResponse<T>['hits']['hits'];
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.postLoad(cls, result);
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>): AsyncIterable<T> {
304
- let search: estypes.SearchResponse<T> = await this.execSearch<T>(cls, {
305
- scroll: '2m',
306
- size: 100,
307
- query: ElasticsearchQueryUtil.getSearchQuery(cls, {})
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.postLoad(cls, result.hits.hits[0]);
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?: ListPageOptions,
491
- ): Promise<ListPageResult<T>> {
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 { hits, nextOffset } of this.#scrollIndex(cls, idx, body, { ...options, offset })) {
496
+ for await (const { items: fetched, nextOffset } of this.#scrollIndex(cls, idx, body, { limit: 100, ...options, offset })) {
496
497
  lastNextOffset = nextOffset;
497
- for (const hit of hits) {
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
- ): AsyncIterable<T> {
519
- for await (const { hits } of this.#scrollIndex(cls, idx, body, { limit: Number.MAX_SAFE_INTEGER })) {
520
- for (const hit of hits) {
521
- try {
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.postLoad(cls, hit).then(item => {
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.postLoad(cls, hit)));
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
 
@@ -1,6 +1,6 @@
1
1
  import type { ServiceDescriptor } from '@travetto/cli';
2
2
 
3
- const version = '9.2.4';
3
+ const version = '9.2.8';
4
4
 
5
5
  const port = 9200;
6
6