@travetto/model-elasticsearch 8.0.0-alpha.1 → 8.0.0-alpha.11
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 -5
- package/src/internal/query.ts +17 -10
- package/src/service.ts +152 -39
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#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/
|
|
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.11",
|
|
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.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-
|
|
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"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.15"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"@travetto/cli": {
|
package/src/internal/query.ts
CHANGED
|
@@ -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
|
|
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
|
|
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>[] |
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
7
|
-
ModelCrudUtil,
|
|
6
|
+
type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
|
|
7
|
+
ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
|
|
8
8
|
} from '@travetto/model';
|
|
9
|
-
import { ShutdownManager, type
|
|
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
|
|
15
|
-
|
|
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,128 @@ export class ElasticsearchModelService implements
|
|
|
354
408
|
}
|
|
355
409
|
|
|
356
410
|
// Indexed
|
|
357
|
-
async getByIndex<
|
|
358
|
-
|
|
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
|
-
|
|
420
|
+
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
|
|
421
|
+
sort: true,
|
|
422
|
+
includeId: true,
|
|
423
|
+
emptyValue: { $exists: true }
|
|
424
|
+
}))
|
|
425
|
+
|
|
363
426
|
)
|
|
364
427
|
});
|
|
365
428
|
if (!result.hits.hits.length) {
|
|
366
|
-
throw new NotFoundError(`${cls.name}: ${idx}`,
|
|
429
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
367
430
|
}
|
|
368
431
|
return this.postLoad(cls, result.hits.hits[0]);
|
|
432
|
+
|
|
369
433
|
}
|
|
370
434
|
|
|
371
|
-
async deleteByIndex<
|
|
372
|
-
|
|
435
|
+
async deleteByIndex<
|
|
436
|
+
T extends ModelType,
|
|
437
|
+
K extends KeyedIndexSelection<T>,
|
|
438
|
+
S extends SortedIndexSelection<T>
|
|
439
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
440
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
373
441
|
const result = await this.client.deleteByQuery({
|
|
374
442
|
index: this.manager.getIdentity(cls).index,
|
|
375
443
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
376
|
-
ElasticsearchQueryUtil.extractWhereTermQuery(cls,
|
|
377
|
-
|
|
444
|
+
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
|
|
445
|
+
sort: true,
|
|
446
|
+
includeId: true,
|
|
447
|
+
emptyValue: { $exists: true }
|
|
448
|
+
}))
|
|
378
449
|
),
|
|
379
450
|
refresh: true
|
|
380
451
|
});
|
|
381
|
-
if (result.deleted) {
|
|
382
|
-
|
|
452
|
+
if (!result.deleted) {
|
|
453
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
383
454
|
}
|
|
384
|
-
throw new NotFoundError(`${cls.name}: ${idx}`, key);
|
|
385
455
|
}
|
|
386
456
|
|
|
387
|
-
|
|
457
|
+
upsertByIndex<
|
|
458
|
+
T extends ModelType,
|
|
459
|
+
K extends KeyedIndexSelection<T>,
|
|
460
|
+
S extends SortedIndexSelection<T>
|
|
461
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
388
462
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
389
463
|
}
|
|
390
464
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
399
|
-
),
|
|
400
|
-
sort: ElasticsearchQueryUtil.getSort(config.fields)
|
|
401
|
-
});
|
|
465
|
+
updateByIndex<
|
|
466
|
+
T extends ModelType,
|
|
467
|
+
K extends KeyedIndexSelection<T>,
|
|
468
|
+
S extends SortedIndexSelection<T>
|
|
469
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
470
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
471
|
+
}
|
|
402
472
|
|
|
403
|
-
|
|
404
|
-
|
|
473
|
+
async updatePartialByIndex<
|
|
474
|
+
T extends ModelType,
|
|
475
|
+
K extends KeyedIndexSelection<T>,
|
|
476
|
+
S extends SortedIndexSelection<T>
|
|
477
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
478
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
479
|
+
return this.update(cls, item);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async pageByIndex<
|
|
483
|
+
T extends ModelType,
|
|
484
|
+
K extends KeyedIndexSelection<T>,
|
|
485
|
+
S extends SortedIndexSelection<T>
|
|
486
|
+
>(
|
|
487
|
+
cls: Class<T>,
|
|
488
|
+
idx: SortedIndex<T, K, S>,
|
|
489
|
+
body: KeyedIndexBody<T, K>,
|
|
490
|
+
options?: ListPageOptions,
|
|
491
|
+
): Promise<ListPageResult<T>> {
|
|
492
|
+
const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
|
|
493
|
+
const items: T[] = [];
|
|
494
|
+
let lastNextOffset: estypes.SortResults | undefined;
|
|
495
|
+
for await (const { hits, nextOffset } of this.#scrollIndex(cls, idx, body, { ...options, offset })) {
|
|
496
|
+
lastNextOffset = nextOffset;
|
|
497
|
+
for (const hit of hits) {
|
|
405
498
|
try {
|
|
406
|
-
|
|
499
|
+
items.push(await this.postLoad(cls, hit));
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (!(error instanceof NotFoundError)) {
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async * listByIndex<
|
|
511
|
+
T extends ModelType,
|
|
512
|
+
K extends KeyedIndexSelection<T>,
|
|
513
|
+
S extends SortedIndexSelection<T>
|
|
514
|
+
>(
|
|
515
|
+
cls: Class<T>,
|
|
516
|
+
idx: SortedIndex<T, K, S>,
|
|
517
|
+
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);
|
|
407
523
|
} catch (error) {
|
|
408
524
|
if (!(error instanceof NotFoundError)) {
|
|
409
525
|
throw error;
|
|
410
526
|
}
|
|
411
527
|
}
|
|
412
|
-
search = await this.client.scroll({
|
|
413
|
-
scroll_id: search._scroll_id,
|
|
414
|
-
scroll: '2m'
|
|
415
|
-
});
|
|
416
528
|
}
|
|
417
529
|
}
|
|
418
530
|
}
|
|
419
531
|
|
|
532
|
+
|
|
420
533
|
// Query
|
|
421
534
|
async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
422
535
|
await QueryVerifier.verify(cls, query);
|