@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -6
  3. 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#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)
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#L23)
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.10",
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.10",
33
- "@travetto/model": "^8.0.0-alpha.10",
34
- "@travetto/model-indexed": "^8.0.0-alpha.10",
35
- "@travetto/model-query": "^8.0.0-alpha.10"
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.15"
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 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
 
@@ -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.postLoad(cls, result.hits.hits[0]);
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 listByIndex<
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?: ListPageOptions,
489
- ): Promise<ListPageResult<T>> {
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 { 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 })) {
494
497
  lastNextOffset = nextOffset;
495
- for (const hit of hits) {
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.postLoad(cls, hit).then(item => {
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.postLoad(cls, hit)));
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