@travetto/model-elasticsearch 8.0.0-alpha.1 → 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 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#L36) 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#L39) 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/src/types/indexed.ts#L11)
22
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L23)
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.1",
3
+ "version": "8.0.0-alpha.10",
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,12 +29,13 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@elastic/elasticsearch": "^9.3.4",
32
- "@travetto/config": "^8.0.0-alpha.1",
33
- "@travetto/model": "^8.0.0-alpha.1",
34
- "@travetto/model-query": "^8.0.0-alpha.1"
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"
35
36
  },
36
37
  "peerDependencies": {
37
- "@travetto/cli": "^8.0.0-alpha.1"
38
+ "@travetto/cli": "^8.0.0-alpha.15"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "@travetto/cli": {
@@ -1,9 +1,10 @@
1
1
  import type * as estypes from '@elastic/elasticsearch/api/types';
2
2
 
3
- import { castTo, type Class, TypedObject } from '@travetto/runtime';
3
+ import { type Any, castTo, type Class, TypedObject } from '@travetto/runtime';
4
4
  import { type WhereClause, type SelectClause, type SortClause, type Query, ModelQueryUtil } from '@travetto/model-query';
5
- import { type IndexConfig, type ModelType, ModelRegistryIndex } from '@travetto/model';
5
+ import { type ModelType, ModelRegistryIndex } from '@travetto/model';
6
6
  import { DataUtil, SchemaRegistryIndex } from '@travetto/schema';
7
+ import type { SortedIndex } from '@travetto/model-indexed';
7
8
 
8
9
  import { type EsSchemaConfig } from './types.ts';
9
10
 
@@ -13,7 +14,7 @@ import { type EsSchemaConfig } from './types.ts';
13
14
  export class ElasticsearchQueryUtil {
14
15
 
15
16
  /**
16
- * Convert `a.b.c` to `a : { b : { c : ... }}`
17
+ * Convert `a : { b : { c : ... }}` to `a.b.c`
17
18
  */
18
19
  static extractSimple<T>(input: T, path: string = ''): Record<string, unknown> {
19
20
  const out: Record<string, unknown> = {};
@@ -51,13 +52,19 @@ export class ElasticsearchQueryUtil {
51
52
  /**
52
53
  * Build sort mechanism
53
54
  */
54
- static getSort<T extends ModelType>(sort: SortClause<T>[] | IndexConfig<T>['fields']): estypes.Sort {
55
- return sort.map<estypes.SortOptions>(option => {
56
- const item = this.extractSimple(option);
57
- const key = Object.keys(item)[0];
58
- const value: boolean | -1 | 1 = castTo(item[key]);
59
- return { [key]: { order: value === 1 || value === true ? 'asc' : 'desc' } };
60
- });
55
+ static getSort<T extends ModelType>(sort: SortClause<T>[] | SortedIndex<T, Any, Any>): estypes.Sort {
56
+ if (Array.isArray(sort)) {
57
+ return sort.map<estypes.SortOptions>(option => {
58
+ const item = this.extractSimple(option);
59
+ const key = Object.keys(item)[0];
60
+ const value: boolean | -1 | 1 = castTo(item[key]);
61
+ return { [key]: { order: value === 1 || value === true ? 'asc' : 'desc' } };
62
+ });
63
+ } else {
64
+ return sort.sortTemplate.map<estypes.SortOptions>(field =>
65
+ ({ [field.path.join('.')]: { order: field.value === 1 ? 'asc' : 'desc' } })
66
+ );
67
+ }
61
68
  }
62
69
 
63
70
  /**
package/src/service.ts CHANGED
@@ -3,19 +3,22 @@ import type * as estypes from '@elastic/elasticsearch/api/types';
3
3
 
4
4
  import {
5
5
  type ModelCrudSupport, type BulkOperation, type BulkResponse, type ModelBulkSupport, type ModelExpirySupport,
6
- type ModelIndexedSupport, type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
7
- ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
6
+ type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
7
+ ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
8
8
  } from '@travetto/model';
9
- import { ShutdownManager, type DeepPartial, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
9
+ import { ShutdownManager, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
10
10
  import { BindUtil } from '@travetto/schema';
11
11
  import { Injectable, PostConstruct } from '@travetto/di';
12
12
  import {
13
- type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport,
14
- type ModelQuerySupport, type PageableModelQuery, type Query, type ValidStringFields,
15
- QueryVerifier, type ModelQuerySuggestSupport,
16
- ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil,
17
- type ModelQueryFacet,
13
+ type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport, type PageableModelQuery,
14
+ type Query, type ValidStringFields, QueryVerifier, type ModelQuerySuggestSupport, ModelQueryUtil, ModelQuerySuggestUtil,
15
+ ModelQueryCrudUtil, type ModelQueryFacet,
18
16
  } from '@travetto/model-query';
17
+ import {
18
+ type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ListPageOptions, ModelIndexedUtil,
19
+ type SingleItemIndex, type SortedIndexSelection, type ListPageResult, type SortedIndex, type FullKeyedIndexBody,
20
+ type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex
21
+ } from '@travetto/model-indexed';
19
22
 
20
23
  import type { ElasticsearchModelConfig } from './config.ts';
21
24
  import type { EsBulkError } from './internal/types.ts';
@@ -48,6 +51,57 @@ export class ElasticsearchModelService implements
48
51
 
49
52
  constructor(config: ElasticsearchModelConfig) { this.config = config; }
50
53
 
54
+ async * #scrollIndex<
55
+ T extends ModelType,
56
+ K extends KeyedIndexSelection<T>,
57
+ S extends SortedIndexSelection<T>
58
+ >(
59
+ cls: Class<T>,
60
+ idx: SortedIndex<T, K, S>,
61
+ body: KeyedIndexBody<T, K>,
62
+ options?: ListPageOptions<estypes.SortResults>
63
+ ): AsyncIterable<{
64
+ hits: estypes.SearchResponse<T>['hits']['hits'];
65
+ nextOffset?: estypes.SortResults | undefined;
66
+ }> {
67
+ const limit = options?.limit ?? 100;
68
+ 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
+ ),
75
+ query: ElasticsearchQueryUtil.getSearchQuery(cls,
76
+ ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project())
77
+ ),
78
+ 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
+ }
103
+ }
104
+
51
105
  @PostConstruct()
52
106
  async initializeClient(this: ElasticsearchModelService): Promise<void> {
53
107
  this.client = new Client({
@@ -354,69 +408,104 @@ export class ElasticsearchModelService implements
354
408
  }
355
409
 
356
410
  // Indexed
357
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
358
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
411
+ async getByIndex<
412
+ T extends ModelType,
413
+ K extends KeyedIndexSelection<T>,
414
+ S extends SortedIndexSelection<T>
415
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
416
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
417
+
359
418
  const result = await this.execSearch<T>(cls, {
360
419
  query: ElasticsearchQueryUtil.getSearchQuery(cls,
361
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
362
- ModelIndexedUtil.projectIndex(cls, idx, body))
420
+ ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
421
+ sort: true,
422
+ emptyValue: { $exists: true }
423
+ }))
424
+
363
425
  )
364
426
  });
365
427
  if (!result.hits.hits.length) {
366
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
428
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
367
429
  }
368
430
  return this.postLoad(cls, result.hits.hits[0]);
431
+
369
432
  }
370
433
 
371
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
372
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
434
+ async deleteByIndex<
435
+ T extends ModelType,
436
+ K extends KeyedIndexSelection<T>,
437
+ S extends SortedIndexSelection<T>
438
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
439
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
373
440
  const result = await this.client.deleteByQuery({
374
441
  index: this.manager.getIdentity(cls).index,
375
442
  query: ElasticsearchQueryUtil.getSearchQuery(cls,
376
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
377
- ModelIndexedUtil.projectIndex(cls, idx, body))
443
+ ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
444
+ sort: true,
445
+ emptyValue: { $exists: true }
446
+ }))
378
447
  ),
379
448
  refresh: true
380
449
  });
381
- if (result.deleted) {
382
- return;
450
+ if (!result.deleted) {
451
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
383
452
  }
384
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
385
453
  }
386
454
 
387
- async upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
455
+ upsertByIndex<
456
+ T extends ModelType,
457
+ K extends KeyedIndexSelection<T>,
458
+ S extends SortedIndexSelection<T>
459
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
388
460
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
389
461
  }
390
462
 
391
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
392
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
393
- let search = await this.execSearch<T>(cls, {
394
- scroll: '2m',
395
- size: 100,
396
- query: ElasticsearchQueryUtil.getSearchQuery(cls,
397
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
398
- ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
399
- ),
400
- sort: ElasticsearchQueryUtil.getSort(config.fields)
401
- });
463
+ updateByIndex<
464
+ T extends ModelType,
465
+ K extends KeyedIndexSelection<T>,
466
+ S extends SortedIndexSelection<T>
467
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
468
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
469
+ }
402
470
 
403
- while (search.hits.hits.length > 0) {
404
- for (const hit of search.hits.hits) {
471
+ async updatePartialByIndex<
472
+ T extends ModelType,
473
+ K extends KeyedIndexSelection<T>,
474
+ S extends SortedIndexSelection<T>
475
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
476
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
477
+ return this.update(cls, item);
478
+ }
479
+
480
+ async listByIndex<
481
+ T extends ModelType,
482
+ K extends KeyedIndexSelection<T>,
483
+ S extends SortedIndexSelection<T>
484
+ >(
485
+ cls: Class<T>,
486
+ idx: SortedIndex<T, K, S>,
487
+ body: KeyedIndexBody<T, K>,
488
+ options?: ListPageOptions,
489
+ ): Promise<ListPageResult<T>> {
490
+ const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
491
+ const items: T[] = [];
492
+ let lastNextOffset: estypes.SortResults | undefined;
493
+ for await (const { hits, nextOffset } of this.#scrollIndex(cls, idx, body, { ...options, offset })) {
494
+ lastNextOffset = nextOffset;
495
+ for (const hit of hits) {
405
496
  try {
406
- yield this.postLoad(cls, hit);
497
+ items.push(await this.postLoad(cls, hit));
407
498
  } catch (error) {
408
499
  if (!(error instanceof NotFoundError)) {
409
500
  throw error;
410
501
  }
411
502
  }
412
- search = await this.client.scroll({
413
- scroll_id: search._scroll_id,
414
- scroll: '2m'
415
- });
416
503
  }
417
504
  }
505
+ return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
418
506
  }
419
507
 
508
+
420
509
  // Query
421
510
  async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
422
511
  await QueryVerifier.verify(cls, query);