@travetto/model-elasticsearch 8.0.0-alpha.2 → 8.0.0-alpha.21
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 +7 -6
- package/src/index-manager.ts +6 -3
- package/src/internal/query.ts +25 -12
- package/src/internal/schema.ts +15 -9
- package/src/service.ts +222 -99
- package/support/service.elasticsearch.ts +1 -1
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#L41) 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#L16)
|
|
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.21",
|
|
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": [
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"directory": "module/model-elasticsearch"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@elastic/elasticsearch": "^9.
|
|
32
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-
|
|
31
|
+
"@elastic/elasticsearch": "^9.4.2",
|
|
32
|
+
"@travetto/config": "^8.0.0-alpha.18",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.19",
|
|
34
|
+
"@travetto/model-indexed": "^8.0.0-alpha.21",
|
|
35
|
+
"@travetto/model-query": "^8.0.0-alpha.20"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.24"
|
|
38
39
|
},
|
|
39
40
|
"peerDependenciesMeta": {
|
|
40
41
|
"@travetto/cli": {
|
package/src/index-manager.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type * as estypes from '@elastic/elasticsearch/api/types';
|
|
|
3
3
|
|
|
4
4
|
import { JSONUtil, type Class } from '@travetto/runtime';
|
|
5
5
|
import { ModelRegistryIndex, type ModelType, type ModelStorageSupport } from '@travetto/model';
|
|
6
|
+
import { warnIfIndexedUniqueIndex } from '@travetto/model-indexed';
|
|
6
7
|
|
|
7
8
|
import type { ElasticsearchModelConfig } from './config.ts';
|
|
8
9
|
import { ElasticsearchSchemaUtil } from './internal/schema.ts';
|
|
@@ -103,6 +104,8 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
103
104
|
const { index } = this.getIdentity(cls);
|
|
104
105
|
const resolvedAlias = await this.#client.indices.getMapping({ index }).catch(() => undefined);
|
|
105
106
|
|
|
107
|
+
warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
108
|
+
|
|
106
109
|
if (resolvedAlias) {
|
|
107
110
|
const [currentIndex] = Object.keys(resolvedAlias ?? {});
|
|
108
111
|
const pendingMapping = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
@@ -141,8 +144,8 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
141
144
|
|
|
142
145
|
async deleteStorage(): Promise<void> {
|
|
143
146
|
console.debug('Deleting storage', { idx: this.getNamespacedIndex('*') });
|
|
144
|
-
await this.#client.indices.delete({
|
|
145
|
-
|
|
146
|
-
});
|
|
147
|
+
// await this.#client.indices.delete({
|
|
148
|
+
// index: this.getNamespacedIndex('*')
|
|
149
|
+
// });
|
|
147
150
|
}
|
|
148
151
|
}
|
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
|
/**
|
|
@@ -134,7 +141,13 @@ export class ElasticsearchQueryUtil {
|
|
|
134
141
|
}
|
|
135
142
|
case '$regex': {
|
|
136
143
|
const pattern = DataUtil.toRegex(castTo(value));
|
|
137
|
-
if (pattern.source.startsWith('
|
|
144
|
+
if (pattern.source.startsWith('^')) { // We have a prefix query
|
|
145
|
+
if (/^\^[A-Za-z0-9_\-]+/.test(pattern.source)) {
|
|
146
|
+
items.push({ prefix: { [subPath]: pattern.source.substring(1) } });
|
|
147
|
+
} else {
|
|
148
|
+
items.push({ regexp: { [subPath]: pattern.source.substring(1) } });
|
|
149
|
+
}
|
|
150
|
+
} else if (pattern.source.startsWith('\\b') && pattern.source.endsWith('.*') && declaredSchema.specifiers?.includes('text')) {
|
|
138
151
|
const textField = !pattern.flags.includes('i') && config && config.caseSensitive ?
|
|
139
152
|
`${subPath}.text_cs` :
|
|
140
153
|
`${subPath}.text`;
|
|
@@ -191,7 +204,7 @@ export class ElasticsearchQueryUtil {
|
|
|
191
204
|
if (ModelQueryUtil.has$And(clause)) {
|
|
192
205
|
return { bool: { must: clause.$and.map(item => this.extractWhereQuery<T>(cls, item, config)) } };
|
|
193
206
|
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
194
|
-
return { bool: { should: clause.$or.map(item => this.extractWhereQuery<T>(cls, item, config)),
|
|
207
|
+
return { bool: { should: clause.$or.map(item => this.extractWhereQuery<T>(cls, item, config)), minimum_should_match: 1 } };
|
|
195
208
|
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
196
209
|
return { bool: { ['must_not']: this.extractWhereQuery<T>(cls, clause.$not, config) } };
|
|
197
210
|
} else {
|
package/src/internal/schema.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { EsSchemaConfig } from './types.ts';
|
|
|
7
7
|
|
|
8
8
|
const PointConcrete = toConcrete<Point>();
|
|
9
9
|
|
|
10
|
-
const isMappingType = (input: estypes.MappingProperty): input is estypes.MappingTypeMapping =>
|
|
11
|
-
(input.type === 'object' || input.type === 'nested')
|
|
10
|
+
const isMappingType = (input: estypes.MappingProperty): input is estypes.MappingTypeMapping & estypes.MappingProperty =>
|
|
11
|
+
!!input && !!input.type && (input.type === 'object' || input.type === 'nested');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Utils for ES Schema management
|
|
@@ -163,14 +163,20 @@ export class ElasticsearchSchemaUtil {
|
|
|
163
163
|
const currentProperty = currentProperties[key];
|
|
164
164
|
const neededProperty = neededProperties[key];
|
|
165
165
|
|
|
166
|
-
if (
|
|
167
|
-
|
|
166
|
+
if (isMappingType(currentProperty) && isMappingType(neededProperty)) {
|
|
167
|
+
if (currentProperty.type !== neededProperty.type) {
|
|
168
|
+
changed.push(path);
|
|
169
|
+
} else {
|
|
170
|
+
changed.push(...this.getChangedFields(
|
|
171
|
+
'properties' in currentProperty ? currentProperty : { properties: {} },
|
|
172
|
+
'properties' in neededProperty ? neededProperty : { properties: {} },
|
|
173
|
+
path
|
|
174
|
+
));
|
|
175
|
+
}
|
|
168
176
|
} else if (isMappingType(currentProperty) || isMappingType(neededProperty)) {
|
|
169
|
-
changed.push(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
path
|
|
173
|
-
));
|
|
177
|
+
changed.push(path);
|
|
178
|
+
} else if (!currentProperty || !neededProperty || currentProperty.type !== neededProperty.type) {
|
|
179
|
+
changed.push(path);
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
return changed;
|
package/src/service.ts
CHANGED
|
@@ -3,19 +3,24 @@ 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
|
+
type ModelListOptions
|
|
8
9
|
} from '@travetto/model';
|
|
9
|
-
import { ShutdownManager, type
|
|
10
|
+
import { ShutdownManager, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
|
|
10
11
|
import { BindUtil } from '@travetto/schema';
|
|
11
12
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
12
13
|
import {
|
|
13
|
-
type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport,
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type ModelQueryFacet,
|
|
14
|
+
type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport, type PageableModelQuery,
|
|
15
|
+
type Query, type ValidStringFields, QueryVerifier, type ModelQuerySuggestSupport, ModelQueryUtil, ModelQuerySuggestUtil,
|
|
16
|
+
ModelQueryCrudUtil, type ModelQueryFacet,
|
|
17
|
+
type WhereClause,
|
|
18
18
|
} from '@travetto/model-query';
|
|
19
|
+
import {
|
|
20
|
+
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
|
|
21
|
+
type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, type FullKeyedIndexBody,
|
|
22
|
+
type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType
|
|
23
|
+
} from '@travetto/model-indexed';
|
|
19
24
|
|
|
20
25
|
import type { ElasticsearchModelConfig } from './config.ts';
|
|
21
26
|
import type { EsBulkError } from './internal/types.ts';
|
|
@@ -48,6 +53,101 @@ export class ElasticsearchModelService implements
|
|
|
48
53
|
|
|
49
54
|
constructor(config: ElasticsearchModelConfig) { this.config = config; }
|
|
50
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Convert _id to id
|
|
58
|
+
*/
|
|
59
|
+
async #postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
|
|
60
|
+
let item = {
|
|
61
|
+
...(input._id ? { id: input._id } : {}),
|
|
62
|
+
...input._source!,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
item = await ModelCrudUtil.load(cls, item);
|
|
66
|
+
|
|
67
|
+
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
68
|
+
|
|
69
|
+
if (expiresAt) {
|
|
70
|
+
const expiry = ModelExpiryUtil.getExpiryState(cls, item);
|
|
71
|
+
if (!expiry.expired) {
|
|
72
|
+
return item;
|
|
73
|
+
}
|
|
74
|
+
throw new NotFoundError(cls, item.id);
|
|
75
|
+
} else {
|
|
76
|
+
return item;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async * #scrollCollection<T extends ModelType>(
|
|
81
|
+
cls: Class<T>,
|
|
82
|
+
buildSearch: (offset?: estypes.SortResults) => estypes.SearchRequest,
|
|
83
|
+
options?: ModelPageOptions<estypes.SortResults> & ModelListOptions
|
|
84
|
+
): AsyncIterable<{ items: T[], nextOffset?: estypes.SortResults | undefined }> {
|
|
85
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
86
|
+
const batchSize = Math.min(limit, options?.batchSizeHint ?? 100);
|
|
87
|
+
|
|
88
|
+
let search: estypes.SearchResponse<T> = await this.execSearch<T>(cls, {
|
|
89
|
+
size: batchSize,
|
|
90
|
+
...buildSearch(options?.offset),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let hits = search.hits.hits;
|
|
94
|
+
let produced = 0;
|
|
95
|
+
|
|
96
|
+
while (produced < limit && hits.length && !(options?.abort?.aborted)) {
|
|
97
|
+
hits = search.hits.hits;
|
|
98
|
+
produced += hits.length;
|
|
99
|
+
if (produced > limit) {
|
|
100
|
+
hits = hits.slice(0, limit - (produced - hits.length));
|
|
101
|
+
}
|
|
102
|
+
const items = await ModelCrudUtil.filterOutNotFound(
|
|
103
|
+
hits.map(hit => this.#postLoad(cls, hit))
|
|
104
|
+
);
|
|
105
|
+
const nextOffset = hits.at(-1)?.sort;
|
|
106
|
+
yield { items, nextOffset };
|
|
107
|
+
if (search._scroll_id) {
|
|
108
|
+
search = await this.client.scroll({ scroll_id: search._scroll_id });
|
|
109
|
+
} else if (nextOffset) {
|
|
110
|
+
search = await this.execSearch<T>(cls, {
|
|
111
|
+
size: batchSize,
|
|
112
|
+
...buildSearch(nextOffset),
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
hits = [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (search._scroll_id) {
|
|
119
|
+
await this.client.clearScroll({ scroll_id: search._scroll_id }).catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async * #scrollIndex<T extends ModelType>(
|
|
124
|
+
cls: Class<T>,
|
|
125
|
+
idx: SortedIndex<T>,
|
|
126
|
+
body: KeyedIndexBody<T>,
|
|
127
|
+
options?: ModelPageOptions<estypes.SortResults> & ModelListOptions,
|
|
128
|
+
transformWhere?: (where: WhereClause<T>) => WhereClause<T>
|
|
129
|
+
): AsyncIterable<{
|
|
130
|
+
items: T[];
|
|
131
|
+
nextOffset?: estypes.SortResults | undefined;
|
|
132
|
+
}> {
|
|
133
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
134
|
+
let whereClause: WhereClause<T> = castTo(computed.project());
|
|
135
|
+
if (transformWhere) {
|
|
136
|
+
whereClause = transformWhere(whereClause);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
yield* this.#scrollCollection(cls, (offset) => {
|
|
140
|
+
const result = {
|
|
141
|
+
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
142
|
+
ElasticsearchQueryUtil.extractWhereQuery(cls, whereClause)
|
|
143
|
+
),
|
|
144
|
+
...(offset ? { search_after: offset } : {}),
|
|
145
|
+
sort: ElasticsearchQueryUtil.getSort(idx)
|
|
146
|
+
};
|
|
147
|
+
return result;
|
|
148
|
+
}, options);
|
|
149
|
+
}
|
|
150
|
+
|
|
51
151
|
@PostConstruct()
|
|
52
152
|
async initializeClient(this: ElasticsearchModelService): Promise<void> {
|
|
53
153
|
this.client = new Client({
|
|
@@ -109,30 +209,6 @@ export class ElasticsearchModelService implements
|
|
|
109
209
|
return item;
|
|
110
210
|
}
|
|
111
211
|
|
|
112
|
-
/**
|
|
113
|
-
* Convert _id to id
|
|
114
|
-
*/
|
|
115
|
-
async postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
|
|
116
|
-
let item = {
|
|
117
|
-
...(input._id ? { id: input._id } : {}),
|
|
118
|
-
...input._source!,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
item = await ModelCrudUtil.load(cls, item);
|
|
122
|
-
|
|
123
|
-
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
124
|
-
|
|
125
|
-
if (expiresAt) {
|
|
126
|
-
const expiry = ModelExpiryUtil.getExpiryState(cls, item);
|
|
127
|
-
if (!expiry.expired) {
|
|
128
|
-
return item;
|
|
129
|
-
}
|
|
130
|
-
throw new NotFoundError(cls, item.id);
|
|
131
|
-
} else {
|
|
132
|
-
return item;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
212
|
createStorage(): Promise<void> { return this.manager.createStorage(); }
|
|
137
213
|
deleteStorage(): Promise<void> { return this.manager.deleteStorage(); }
|
|
138
214
|
upsertModel(cls: Class): Promise<void> { return this.manager.upsertModel(cls); }
|
|
@@ -143,7 +219,7 @@ export class ElasticsearchModelService implements
|
|
|
143
219
|
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
144
220
|
try {
|
|
145
221
|
const result = await this.client.get<T>({ ...this.manager.getIdentity(cls), id });
|
|
146
|
-
return this
|
|
222
|
+
return this.#postLoad(cls, result);
|
|
147
223
|
} catch {
|
|
148
224
|
throw new NotFoundError(cls, id);
|
|
149
225
|
}
|
|
@@ -246,27 +322,12 @@ export class ElasticsearchModelService implements
|
|
|
246
322
|
return this.get(cls, id);
|
|
247
323
|
}
|
|
248
324
|
|
|
249
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
while (search.hits.hits.length > 0) {
|
|
257
|
-
for (const hit of search.hits.hits) {
|
|
258
|
-
try {
|
|
259
|
-
yield this.postLoad(cls, hit);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
if (!(error instanceof NotFoundError)) {
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
search = await this.client.scroll({
|
|
266
|
-
scroll_id: search._scroll_id,
|
|
267
|
-
scroll: '2m'
|
|
268
|
-
});
|
|
269
|
-
}
|
|
325
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
326
|
+
for await (const batch of this.#scrollCollection(cls, () => ({
|
|
327
|
+
query: ElasticsearchQueryUtil.getSearchQuery(cls, {}),
|
|
328
|
+
scroll: '2m'
|
|
329
|
+
}), options)) {
|
|
330
|
+
yield batch.items;
|
|
270
331
|
}
|
|
271
332
|
}
|
|
272
333
|
|
|
@@ -354,67 +415,129 @@ export class ElasticsearchModelService implements
|
|
|
354
415
|
}
|
|
355
416
|
|
|
356
417
|
// Indexed
|
|
357
|
-
async getByIndex<
|
|
358
|
-
|
|
418
|
+
async getByIndex<
|
|
419
|
+
T extends ModelType,
|
|
420
|
+
K extends KeyedIndexSelection<T>,
|
|
421
|
+
S extends SortedIndexSelection<T>
|
|
422
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
423
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
424
|
+
const projected = computed.project({
|
|
425
|
+
sort: true,
|
|
426
|
+
includeId: true,
|
|
427
|
+
emptyValue: { $exists: true }
|
|
428
|
+
});
|
|
429
|
+
|
|
359
430
|
const result = await this.execSearch<T>(cls, {
|
|
360
431
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
361
|
-
ElasticsearchQueryUtil.extractWhereTermQuery(cls,
|
|
362
|
-
ModelIndexedUtil.projectIndex(cls, idx, body))
|
|
432
|
+
ElasticsearchQueryUtil.extractWhereTermQuery(cls, projected)
|
|
363
433
|
)
|
|
364
434
|
});
|
|
365
435
|
if (!result.hits.hits.length) {
|
|
366
|
-
throw new NotFoundError(`${cls.name}: ${idx}`,
|
|
436
|
+
throw new NotFoundError(`${cls.name}: ${idx.name}`, computed.getKey({ sort: true }));
|
|
367
437
|
}
|
|
368
|
-
return this
|
|
438
|
+
return this.#postLoad(cls, result.hits.hits[0]);
|
|
439
|
+
|
|
369
440
|
}
|
|
370
441
|
|
|
371
|
-
async deleteByIndex<
|
|
372
|
-
|
|
442
|
+
async deleteByIndex<
|
|
443
|
+
T extends ModelType,
|
|
444
|
+
K extends KeyedIndexSelection<T>,
|
|
445
|
+
S extends SortedIndexSelection<T>
|
|
446
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
447
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
373
448
|
const result = await this.client.deleteByQuery({
|
|
374
449
|
index: this.manager.getIdentity(cls).index,
|
|
375
450
|
query: ElasticsearchQueryUtil.getSearchQuery(cls,
|
|
376
|
-
ElasticsearchQueryUtil.extractWhereTermQuery(cls,
|
|
377
|
-
|
|
451
|
+
ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
|
|
452
|
+
sort: true,
|
|
453
|
+
includeId: true,
|
|
454
|
+
emptyValue: { $exists: true }
|
|
455
|
+
}))
|
|
378
456
|
),
|
|
379
457
|
refresh: true
|
|
380
458
|
});
|
|
381
|
-
if (result.deleted) {
|
|
382
|
-
|
|
459
|
+
if (!result.deleted) {
|
|
460
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
383
461
|
}
|
|
384
|
-
throw new NotFoundError(`${cls.name}: ${idx}`, key);
|
|
385
462
|
}
|
|
386
463
|
|
|
387
|
-
|
|
464
|
+
upsertByIndex<
|
|
465
|
+
T extends ModelType,
|
|
466
|
+
K extends KeyedIndexSelection<T>,
|
|
467
|
+
S extends SortedIndexSelection<T>
|
|
468
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
388
469
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
389
470
|
}
|
|
390
471
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
399
|
-
),
|
|
400
|
-
sort: ElasticsearchQueryUtil.getSort(config.fields)
|
|
401
|
-
});
|
|
472
|
+
updateByIndex<
|
|
473
|
+
T extends ModelType,
|
|
474
|
+
K extends KeyedIndexSelection<T>,
|
|
475
|
+
S extends SortedIndexSelection<T>
|
|
476
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
477
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
478
|
+
}
|
|
402
479
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
480
|
+
async updatePartialByIndex<
|
|
481
|
+
T extends ModelType,
|
|
482
|
+
K extends KeyedIndexSelection<T>,
|
|
483
|
+
S extends SortedIndexSelection<T>
|
|
484
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
485
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
486
|
+
return this.update(cls, item);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async pageByIndex<
|
|
490
|
+
T extends ModelType,
|
|
491
|
+
K extends KeyedIndexSelection<T>,
|
|
492
|
+
S extends SortedIndexSelection<T>
|
|
493
|
+
>(
|
|
494
|
+
cls: Class<T>,
|
|
495
|
+
idx: SortedIndex<T, K, S>,
|
|
496
|
+
body: KeyedIndexBody<T, K>,
|
|
497
|
+
options?: ModelPageOptions,
|
|
498
|
+
): Promise<ModelPageResult<T>> {
|
|
499
|
+
const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
|
|
500
|
+
const items: T[] = [];
|
|
501
|
+
let lastNextOffset: estypes.SortResults | undefined;
|
|
502
|
+
for await (const { items: fetched, nextOffset } of this.#scrollIndex(cls, idx, body, { limit: 100, ...options, offset })) {
|
|
503
|
+
lastNextOffset = nextOffset;
|
|
504
|
+
items.push(...fetched);
|
|
505
|
+
}
|
|
506
|
+
return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async * listByIndex<
|
|
510
|
+
T extends ModelType,
|
|
511
|
+
K extends KeyedIndexSelection<T>,
|
|
512
|
+
S extends SortedIndexSelection<T>
|
|
513
|
+
>(
|
|
514
|
+
cls: Class<T>,
|
|
515
|
+
idx: SortedIndex<T, K, S>,
|
|
516
|
+
body: KeyedIndexBody<T, K>,
|
|
517
|
+
options?: ModelListOptions
|
|
518
|
+
): AsyncIterable<T[]> {
|
|
519
|
+
for await (const { items } of this.#scrollIndex(cls, idx, body, options)) {
|
|
520
|
+
yield items;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async suggestByIndex<
|
|
525
|
+
T extends ModelType,
|
|
526
|
+
S extends SortedIndexSelection<T>,
|
|
527
|
+
K extends KeyedIndexSelection<T>,
|
|
528
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
529
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
530
|
+
const items: T[] = [];
|
|
531
|
+
for await (const { items: fetched } of this.#scrollIndex(cls, idx, body, { limit: 10, ...options },
|
|
532
|
+
where => castTo({
|
|
533
|
+
$and: [where, {
|
|
534
|
+
[idx.sortTemplate[0].path.join('.')]: { $regex: ModelIndexedUtil.getSuggestRegex(prefix) }
|
|
535
|
+
}]
|
|
536
|
+
})
|
|
537
|
+
)) {
|
|
538
|
+
items.push(...fetched);
|
|
417
539
|
}
|
|
540
|
+
return items;
|
|
418
541
|
}
|
|
419
542
|
|
|
420
543
|
// Query
|
|
@@ -424,7 +547,7 @@ export class ElasticsearchModelService implements
|
|
|
424
547
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
|
|
425
548
|
const results = await this.execSearch(cls, search);
|
|
426
549
|
const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
|
|
427
|
-
return Promise.all(results.hits.hits.map(hit => this
|
|
550
|
+
return Promise.all(results.hits.hits.map(hit => this.#postLoad(cls, hit).then(item => {
|
|
428
551
|
if (shouldRemoveIds) {
|
|
429
552
|
delete castTo<OptionalId<T>>(item).id;
|
|
430
553
|
}
|
|
@@ -520,17 +643,17 @@ export class ElasticsearchModelService implements
|
|
|
520
643
|
}
|
|
521
644
|
|
|
522
645
|
// Query Facet
|
|
523
|
-
async
|
|
646
|
+
async suggestByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|
|
524
647
|
await QueryVerifier.verify(cls, query);
|
|
525
648
|
|
|
526
649
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
527
650
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
528
651
|
const result = await this.execSearch(cls, search);
|
|
529
|
-
const all = await Promise.all(result.hits.hits.map(hit => this
|
|
652
|
+
const all = await Promise.all(result.hits.hits.map(hit => this.#postLoad(cls, hit)));
|
|
530
653
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
|
|
531
654
|
}
|
|
532
655
|
|
|
533
|
-
async
|
|
656
|
+
async suggestValuesByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
534
657
|
await QueryVerifier.verify(cls, query);
|
|
535
658
|
|
|
536
659
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, {
|
|
@@ -539,12 +662,12 @@ export class ElasticsearchModelService implements
|
|
|
539
662
|
});
|
|
540
663
|
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
541
664
|
const result = await this.execSearch(cls, search);
|
|
542
|
-
const all = result.hits.hits.map(hit => castTo<T>((
|
|
665
|
+
const all = result.hits.hits.map(hit => castTo<T>((field === 'id' ? { id: hit._id } : hit._source)));
|
|
543
666
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, item => item, query && query.limit);
|
|
544
667
|
}
|
|
545
668
|
|
|
546
669
|
// Facet
|
|
547
|
-
async
|
|
670
|
+
async facetByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
|
|
548
671
|
await QueryVerifier.verify(cls, query);
|
|
549
672
|
|
|
550
673
|
const resolvedSearch = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
|