@travetto/model-elasticsearch 7.0.0-rc.1 → 7.0.0-rc.2
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 +4 -4
- package/package.json +5 -5
- package/src/config.ts +2 -2
- package/src/index-manager.ts +25 -25
- package/src/internal/query.ts +55 -55
- package/src/internal/schema.ts +42 -42
- package/src/service.ts +105 -103
package/README.md
CHANGED
|
@@ -36,8 +36,8 @@ export class Init {
|
|
|
36
36
|
@InjectableFactory({
|
|
37
37
|
primary: true
|
|
38
38
|
})
|
|
39
|
-
static getModelSource(
|
|
40
|
-
return new ElasticsearchModelService(
|
|
39
|
+
static getModelSource(config: ElasticsearchModelConfig) {
|
|
40
|
+
return new ElasticsearchModelService(config);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
```
|
|
@@ -99,8 +99,8 @@ export class ElasticsearchModelConfig {
|
|
|
99
99
|
postConstruct(): void {
|
|
100
100
|
console.debug('Constructed', { config: this });
|
|
101
101
|
this.hosts = this.hosts
|
|
102
|
-
.map(
|
|
103
|
-
.map(
|
|
102
|
+
.map(host => host.includes(':') ? host : `${host}:${this.port}`)
|
|
103
|
+
.map(host => host.startsWith('http') ? host : `http://${host}`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-elasticsearch",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"elasticsearch",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@elastic/elasticsearch": "^9.2.0",
|
|
31
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
32
|
-
"@travetto/config": "^7.0.0-rc.
|
|
33
|
-
"@travetto/model": "^7.0.0-rc.
|
|
34
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
31
|
+
"@travetto/cli": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
33
|
+
"@travetto/model": "^7.0.0-rc.2",
|
|
34
|
+
"@travetto/model-query": "^7.0.0-rc.2"
|
|
35
35
|
},
|
|
36
36
|
"travetto": {
|
|
37
37
|
"displayName": "Elasticsearch Model Source"
|
package/src/config.ts
CHANGED
|
@@ -59,7 +59,7 @@ export class ElasticsearchModelConfig {
|
|
|
59
59
|
postConstruct(): void {
|
|
60
60
|
console.debug('Constructed', { config: this });
|
|
61
61
|
this.hosts = this.hosts
|
|
62
|
-
.map(
|
|
63
|
-
.map(
|
|
62
|
+
.map(host => host.includes(':') ? host : `${host}:${this.port}`)
|
|
63
|
+
.map(host => host.startsWith('http') ? host : `http://${host}`);
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/index-manager.ts
CHANGED
|
@@ -74,22 +74,22 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
74
74
|
*/
|
|
75
75
|
async createIndex(cls: Class, alias = true): Promise<string> {
|
|
76
76
|
const mapping = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
77
|
-
const
|
|
78
|
-
const concreteIndex = `${
|
|
77
|
+
const { index } = this.getIdentity(cls); // Already namespaced
|
|
78
|
+
const concreteIndex = `${index}_${Date.now()}`;
|
|
79
79
|
try {
|
|
80
80
|
await this.#client.indices.create({
|
|
81
81
|
index: concreteIndex,
|
|
82
82
|
mappings: mapping,
|
|
83
83
|
settings: this.config.indexCreate,
|
|
84
|
-
...(alias ? { aliases: { [
|
|
84
|
+
...(alias ? { aliases: { [index]: {} } } : {})
|
|
85
85
|
});
|
|
86
|
-
console.debug('Index created', { index
|
|
86
|
+
console.debug('Index created', { index, concrete: concreteIndex });
|
|
87
87
|
console.debug('Index Config', {
|
|
88
88
|
mappings: mapping,
|
|
89
89
|
settings: this.config.indexCreate
|
|
90
90
|
});
|
|
91
|
-
} catch (
|
|
92
|
-
console.warn('Index already created', { index
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn('Index already created', { index, error });
|
|
93
93
|
}
|
|
94
94
|
return concreteIndex;
|
|
95
95
|
}
|
|
@@ -99,9 +99,9 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
99
99
|
*/
|
|
100
100
|
async createIndexIfMissing(cls: Class): Promise<void> {
|
|
101
101
|
const baseCls = SchemaRegistryIndex.getBaseClass(cls);
|
|
102
|
-
const
|
|
102
|
+
const identity = this.getIdentity(baseCls);
|
|
103
103
|
try {
|
|
104
|
-
await this.#client.search(
|
|
104
|
+
await this.#client.search(identity);
|
|
105
105
|
} catch {
|
|
106
106
|
await this.createIndex(baseCls);
|
|
107
107
|
}
|
|
@@ -114,8 +114,8 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
114
114
|
|
|
115
115
|
async exportModel(cls: Class<ModelType>): Promise<string> {
|
|
116
116
|
const schema = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
117
|
-
const
|
|
118
|
-
return `curl -XPOST $ES_HOST/${
|
|
117
|
+
const { index } = this.getIdentity(cls); // Already namespaced
|
|
118
|
+
return `curl -XPOST $ES_HOST/${index} -d '${JSON.stringify({
|
|
119
119
|
mappings: schema,
|
|
120
120
|
settings: this.config.indexCreate
|
|
121
121
|
})}'`;
|
|
@@ -136,20 +136,20 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
136
136
|
*/
|
|
137
137
|
async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
|
|
138
138
|
// Find which fields are gone
|
|
139
|
-
const removes = change.subs.reduce<string[]>((
|
|
140
|
-
|
|
141
|
-
.filter(
|
|
142
|
-
.map(
|
|
143
|
-
return
|
|
139
|
+
const removes = change.subs.reduce<string[]>((toRemove, subChange) => {
|
|
140
|
+
toRemove.push(...subChange.fields
|
|
141
|
+
.filter(event => event.type === 'removing')
|
|
142
|
+
.map(event => [...subChange.path.map(field => field.name), event.previous!.name].join('.')));
|
|
143
|
+
return toRemove;
|
|
144
144
|
}, []);
|
|
145
145
|
|
|
146
146
|
// Find which types have changed
|
|
147
|
-
const fieldChanges = change.subs.reduce<string[]>((
|
|
148
|
-
|
|
149
|
-
.filter(
|
|
150
|
-
.filter(
|
|
151
|
-
.map(
|
|
152
|
-
return
|
|
147
|
+
const fieldChanges = change.subs.reduce<string[]>((toChange, subChange) => {
|
|
148
|
+
toChange.push(...subChange.fields
|
|
149
|
+
.filter(event => event.type === 'changed')
|
|
150
|
+
.filter(event => event.previous?.type !== event.current?.type)
|
|
151
|
+
.map(event => [...subChange.path.map(field => field.name), event.previous!.name].join('.')));
|
|
152
|
+
return toChange;
|
|
153
153
|
}, []);
|
|
154
154
|
|
|
155
155
|
const { index } = this.getIdentity(cls);
|
|
@@ -159,16 +159,16 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
159
159
|
const next = await this.createIndex(cls, false);
|
|
160
160
|
|
|
161
161
|
const aliases = (await this.#client.indices.getAlias({ index })).body;
|
|
162
|
-
const
|
|
162
|
+
const current = Object.keys(aliases)[0];
|
|
163
163
|
|
|
164
164
|
const allChange = removes.concat(fieldChanges);
|
|
165
165
|
|
|
166
166
|
const reindexBody: estypes.ReindexRequest = {
|
|
167
|
-
source: { index:
|
|
167
|
+
source: { index: current },
|
|
168
168
|
dest: { index: next },
|
|
169
169
|
script: {
|
|
170
170
|
lang: 'painless',
|
|
171
|
-
source: allChange.map(
|
|
171
|
+
source: allChange.map(part => `ctx._source.remove("${part}");`).join(' ') // Removing
|
|
172
172
|
},
|
|
173
173
|
wait_for_completion: true
|
|
174
174
|
};
|
|
@@ -177,7 +177,7 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
177
177
|
await this.#client.reindex(reindexBody);
|
|
178
178
|
|
|
179
179
|
await Promise.all(Object.keys(aliases)
|
|
180
|
-
.map(
|
|
180
|
+
.map(alias => this.#client.indices.delete({ index: alias })));
|
|
181
181
|
|
|
182
182
|
await this.#client.indices.putAlias({ index: next, name: index });
|
|
183
183
|
} else { // Only update the schema
|
package/src/internal/query.ts
CHANGED
|
@@ -15,15 +15,15 @@ export class ElasticsearchQueryUtil {
|
|
|
15
15
|
/**
|
|
16
16
|
* Convert `a.b.c` to `a : { b : { c : ... }}`
|
|
17
17
|
*/
|
|
18
|
-
static extractSimple<T>(
|
|
18
|
+
static extractSimple<T>(input: T, path: string = ''): Record<string, unknown> {
|
|
19
19
|
const out: Record<string, unknown> = {};
|
|
20
|
-
const keys = TypedObject.keys(
|
|
20
|
+
const keys = TypedObject.keys(input);
|
|
21
21
|
for (const key of keys) {
|
|
22
22
|
const subPath = `${path}${key}`;
|
|
23
|
-
if (DataUtil.isPlainObject(
|
|
24
|
-
Object.assign(out, this.extractSimple(
|
|
23
|
+
if (DataUtil.isPlainObject(input[key]) && !Object.keys(input[key])[0].startsWith('$')) {
|
|
24
|
+
Object.assign(out, this.extractSimple(input[key], `${subPath}.`));
|
|
25
25
|
} else {
|
|
26
|
-
out[subPath] =
|
|
26
|
+
out[subPath] = input[key];
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
return out;
|
|
@@ -36,13 +36,13 @@ export class ElasticsearchQueryUtil {
|
|
|
36
36
|
const simp = this.extractSimple(clause);
|
|
37
37
|
const include: string[] = [];
|
|
38
38
|
const exclude: string[] = [];
|
|
39
|
-
for (const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
exclude.push(
|
|
39
|
+
for (const key of Object.keys(simp)) {
|
|
40
|
+
const translatedKey = key === 'id' ? '_id' : key;
|
|
41
|
+
const value: 1 | 0 | boolean = castTo(simp[key]);
|
|
42
|
+
if (value === 0 || value === false) {
|
|
43
|
+
exclude.push(translatedKey);
|
|
44
44
|
} else {
|
|
45
|
-
include.push(
|
|
45
|
+
include.push(translatedKey);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
return [include, exclude];
|
|
@@ -52,73 +52,73 @@ export class ElasticsearchQueryUtil {
|
|
|
52
52
|
* Build sort mechanism
|
|
53
53
|
*/
|
|
54
54
|
static getSort<T extends ModelType>(sort: SortClause<T>[] | IndexConfig<T>['fields']): estypes.Sort {
|
|
55
|
-
return sort.map<estypes.SortOptions>(
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
return { [
|
|
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
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Extract specific term for a class, and a given field
|
|
65
65
|
*/
|
|
66
|
-
static extractWhereTermQuery<T>(cls: Class<T>,
|
|
66
|
+
static extractWhereTermQuery<T>(cls: Class<T>, item: Record<string, unknown>, config?: EsSchemaConfig, path: string = ''): Record<string, unknown> {
|
|
67
67
|
const items = [];
|
|
68
68
|
const fields = SchemaRegistryIndex.get(cls).getFields();
|
|
69
69
|
|
|
70
|
-
for (const
|
|
71
|
-
const top =
|
|
72
|
-
const declaredSchema = fields[
|
|
70
|
+
for (const property of TypedObject.keys(item)) {
|
|
71
|
+
const top = item[property];
|
|
72
|
+
const declaredSchema = fields[property];
|
|
73
73
|
const declaredType = declaredSchema.type;
|
|
74
|
-
const
|
|
75
|
-
((
|
|
76
|
-
`${path}${
|
|
74
|
+
const subPath = declaredType === String ?
|
|
75
|
+
((property === 'id' && !path) ? '_id' : `${path}${property}`) :
|
|
76
|
+
`${path}${property}`;
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
{ ids: { values: Array.isArray(
|
|
80
|
-
{ [Array.isArray(
|
|
78
|
+
const subPathQuery = (value: unknown): {} => (property === 'id' && !path) ?
|
|
79
|
+
{ ids: { values: Array.isArray(value) ? value : [value] } } :
|
|
80
|
+
{ [Array.isArray(value) ? 'terms' : 'term']: { [subPath]: value } };
|
|
81
81
|
|
|
82
82
|
if (DataUtil.isPlainObject(top)) {
|
|
83
83
|
const subKey = Object.keys(top)[0];
|
|
84
84
|
if (!subKey.startsWith('$')) {
|
|
85
|
-
const inner = this.extractWhereTermQuery(declaredType, top, config, `${
|
|
85
|
+
const inner = this.extractWhereTermQuery(declaredType, top, config, `${subPath}.`);
|
|
86
86
|
items.push(declaredSchema.array ?
|
|
87
|
-
{ nested: { path:
|
|
87
|
+
{ nested: { path: subPath, query: inner } } :
|
|
88
88
|
inner
|
|
89
89
|
);
|
|
90
90
|
} else {
|
|
91
|
-
const
|
|
91
|
+
const value = top[subKey];
|
|
92
92
|
|
|
93
93
|
switch (subKey) {
|
|
94
94
|
case '$all': {
|
|
95
|
-
const
|
|
95
|
+
const values = Array.isArray(value) ? value : [value];
|
|
96
96
|
items.push({
|
|
97
97
|
bool: {
|
|
98
|
-
must:
|
|
98
|
+
must: values.map(term => ({ term: { [subPath]: term } }))
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
break;
|
|
102
102
|
}
|
|
103
103
|
case '$in': {
|
|
104
|
-
items.push(
|
|
104
|
+
items.push(subPathQuery(Array.isArray(value) ? value : [value]));
|
|
105
105
|
break;
|
|
106
106
|
}
|
|
107
107
|
case '$nin': {
|
|
108
|
-
items.push({ bool: { ['must_not']: [
|
|
108
|
+
items.push({ bool: { ['must_not']: [subPathQuery(Array.isArray(value) ? value : [value])] } });
|
|
109
109
|
break;
|
|
110
110
|
}
|
|
111
111
|
case '$eq': {
|
|
112
|
-
items.push(
|
|
112
|
+
items.push(subPathQuery(value));
|
|
113
113
|
break;
|
|
114
114
|
}
|
|
115
115
|
case '$ne': {
|
|
116
|
-
items.push({ bool: { ['must_not']: [
|
|
116
|
+
items.push({ bool: { ['must_not']: [subPathQuery(value)] } });
|
|
117
117
|
break;
|
|
118
118
|
}
|
|
119
119
|
case '$exists': {
|
|
120
|
-
const
|
|
121
|
-
items.push(
|
|
120
|
+
const clause = { exists: { field: subPath } };
|
|
121
|
+
items.push(value ? clause : { bool: { ['must_not']: clause } });
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
124
|
case '$lt':
|
|
@@ -126,18 +126,18 @@ export class ElasticsearchQueryUtil {
|
|
|
126
126
|
case '$gte':
|
|
127
127
|
case '$lte': {
|
|
128
128
|
const out: Record<string, unknown> = {};
|
|
129
|
-
for (const
|
|
130
|
-
out[
|
|
129
|
+
for (const key of Object.keys(top)) {
|
|
130
|
+
out[key.replace(/^[$]/, '')] = ModelQueryUtil.resolveComparator(top[key]);
|
|
131
131
|
}
|
|
132
|
-
items.push({ range: { [
|
|
132
|
+
items.push({ range: { [subPath]: out } });
|
|
133
133
|
break;
|
|
134
134
|
}
|
|
135
135
|
case '$regex': {
|
|
136
|
-
const pattern = DataUtil.toRegex(castTo(
|
|
136
|
+
const pattern = DataUtil.toRegex(castTo(value));
|
|
137
137
|
if (pattern.source.startsWith('\\b') && pattern.source.endsWith('.*')) {
|
|
138
138
|
const textField = !pattern.flags.includes('i') && config && config.caseSensitive ?
|
|
139
|
-
`${
|
|
140
|
-
`${
|
|
139
|
+
`${subPath}.text_cs` :
|
|
140
|
+
`${subPath}.text`;
|
|
141
141
|
const query = pattern.source.substring(2, pattern.source.length - 2);
|
|
142
142
|
items.push({
|
|
143
143
|
['match_phrase_prefix']: {
|
|
@@ -145,12 +145,12 @@ export class ElasticsearchQueryUtil {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
} else {
|
|
148
|
-
items.push({ regexp: { [
|
|
148
|
+
items.push({ regexp: { [subPath]: pattern.source } });
|
|
149
149
|
}
|
|
150
150
|
break;
|
|
151
151
|
}
|
|
152
152
|
case '$geoWithin': {
|
|
153
|
-
items.push({ ['geo_polygon']: { [
|
|
153
|
+
items.push({ ['geo_polygon']: { [subPath]: { points: value } } });
|
|
154
154
|
break;
|
|
155
155
|
}
|
|
156
156
|
case '$unit':
|
|
@@ -165,7 +165,7 @@ export class ElasticsearchQueryUtil {
|
|
|
165
165
|
items.push({
|
|
166
166
|
['geo_distance']: {
|
|
167
167
|
distance: `${dist}${unit}`,
|
|
168
|
-
[
|
|
168
|
+
[subPath]: top.$near
|
|
169
169
|
}
|
|
170
170
|
});
|
|
171
171
|
break;
|
|
@@ -174,7 +174,7 @@ export class ElasticsearchQueryUtil {
|
|
|
174
174
|
}
|
|
175
175
|
// Handle operations
|
|
176
176
|
} else {
|
|
177
|
-
items.push(
|
|
177
|
+
items.push(subPathQuery(top));
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
if (items.length === 1) {
|
|
@@ -187,15 +187,15 @@ export class ElasticsearchQueryUtil {
|
|
|
187
187
|
/**
|
|
188
188
|
* Build query from the where clause
|
|
189
189
|
*/
|
|
190
|
-
static extractWhereQuery<T>(cls: Class<T>,
|
|
191
|
-
if (ModelQueryUtil.has$And(
|
|
192
|
-
return { bool: { must:
|
|
193
|
-
} else if (ModelQueryUtil.has$Or(
|
|
194
|
-
return { bool: { should:
|
|
195
|
-
} else if (ModelQueryUtil.has$Not(
|
|
196
|
-
return { bool: { ['must_not']: this.extractWhereQuery<T>(cls,
|
|
190
|
+
static extractWhereQuery<T>(cls: Class<T>, clause: WhereClause<T>, config?: EsSchemaConfig): Record<string, unknown> {
|
|
191
|
+
if (ModelQueryUtil.has$And(clause)) {
|
|
192
|
+
return { bool: { must: clause.$and.map(item => this.extractWhereQuery<T>(cls, item, config)) } };
|
|
193
|
+
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
194
|
+
return { bool: { should: clause.$or.map(item => this.extractWhereQuery<T>(cls, item, config)), ['minimum_should_match']: 1 } };
|
|
195
|
+
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
196
|
+
return { bool: { ['must_not']: this.extractWhereQuery<T>(cls, clause.$not, config) } };
|
|
197
197
|
} else {
|
|
198
|
-
return this.extractWhereTermQuery(cls,
|
|
198
|
+
return this.extractWhereTermQuery(cls, clause, config);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
package/src/internal/schema.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Point, DataUtil, SchemaRegistryIndex } from '@travetto/schema';
|
|
|
5
5
|
|
|
6
6
|
import { EsSchemaConfig } from './types.ts';
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const PointConcrete = toConcrete<Point>();
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Utils for ES Schema management
|
|
@@ -15,7 +15,7 @@ export class ElasticsearchSchemaUtil {
|
|
|
15
15
|
/**
|
|
16
16
|
* Build the update script for a given object
|
|
17
17
|
*/
|
|
18
|
-
static generateUpdateScript(
|
|
18
|
+
static generateUpdateScript(item: Record<string, unknown>): estypes.Script {
|
|
19
19
|
const out: estypes.Script = {
|
|
20
20
|
lang: 'painless',
|
|
21
21
|
source: `
|
|
@@ -32,21 +32,21 @@ export class ElasticsearchSchemaUtil {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
`,
|
|
35
|
-
params: { body:
|
|
35
|
+
params: { body: item },
|
|
36
36
|
};
|
|
37
37
|
return out;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Generate replace script
|
|
42
|
-
* @param
|
|
42
|
+
* @param item
|
|
43
43
|
* @returns
|
|
44
44
|
*/
|
|
45
|
-
static generateReplaceScript(
|
|
45
|
+
static generateReplaceScript(item: Record<string, unknown>): estypes.Script {
|
|
46
46
|
return {
|
|
47
47
|
lang: 'painless',
|
|
48
48
|
source: 'ctx._source.clear(); ctx._source.putAll(params.body)',
|
|
49
|
-
params: { body:
|
|
49
|
+
params: { body: item }
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -64,65 +64,65 @@ export class ElasticsearchSchemaUtil {
|
|
|
64
64
|
*/
|
|
65
65
|
static generateAllMapping(cls: Class, config?: EsSchemaConfig): estypes.MappingTypeMapping {
|
|
66
66
|
const allTypes = SchemaRegistryIndex.getDiscriminatedClasses(cls);
|
|
67
|
-
return allTypes.reduce<estypes.MappingTypeMapping>((
|
|
68
|
-
DataUtil.deepAssign(
|
|
69
|
-
return
|
|
67
|
+
return allTypes.reduce<estypes.MappingTypeMapping>((mapping, schemaCls) => {
|
|
68
|
+
DataUtil.deepAssign(mapping, this.generateSingleMapping(schemaCls, config));
|
|
69
|
+
return mapping;
|
|
70
70
|
}, { properties: {}, dynamic: false });
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Build a mapping for a given class
|
|
75
75
|
*/
|
|
76
|
-
static generateSingleMapping<T>(cls: Class<T>,
|
|
76
|
+
static generateSingleMapping<T>(cls: Class<T>, esSchema?: EsSchemaConfig): estypes.MappingTypeMapping {
|
|
77
77
|
const fields = SchemaRegistryIndex.get(cls).getFields();
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const properties: Record<string, estypes.MappingProperty> = {};
|
|
80
80
|
|
|
81
|
-
for (const [field,
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
} else if (
|
|
85
|
-
let
|
|
86
|
-
if (
|
|
87
|
-
const [digits, decimals] =
|
|
81
|
+
for (const [field, config] of Object.entries(fields)) {
|
|
82
|
+
if (config.type === PointConcrete) {
|
|
83
|
+
properties[field] = { type: 'geo_point' };
|
|
84
|
+
} else if (config.type === Number) {
|
|
85
|
+
let property: Record<string, unknown> = { type: 'integer' };
|
|
86
|
+
if (config.precision) {
|
|
87
|
+
const [digits, decimals] = config.precision;
|
|
88
88
|
if (decimals) {
|
|
89
89
|
if ((decimals + digits) < 16) {
|
|
90
|
-
|
|
90
|
+
property = { type: 'scaled_float', ['scaling_factor']: decimals };
|
|
91
91
|
} else {
|
|
92
92
|
if (digits < 6 && decimals < 9) {
|
|
93
|
-
|
|
93
|
+
property = { type: 'half_float' };
|
|
94
94
|
} else if (digits > 20) {
|
|
95
|
-
|
|
95
|
+
property = { type: 'double' };
|
|
96
96
|
} else {
|
|
97
|
-
|
|
97
|
+
property = { type: 'float' };
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
} else if (digits) {
|
|
101
101
|
if (digits <= 2) {
|
|
102
|
-
|
|
102
|
+
property = { type: 'byte' };
|
|
103
103
|
} else if (digits <= 4) {
|
|
104
|
-
|
|
104
|
+
property = { type: 'short' };
|
|
105
105
|
} else if (digits <= 9) {
|
|
106
|
-
|
|
106
|
+
property = { type: 'integer' };
|
|
107
107
|
} else {
|
|
108
|
-
|
|
108
|
+
property = { type: 'long' };
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
} else if (
|
|
114
|
-
|
|
115
|
-
} else if (
|
|
116
|
-
|
|
117
|
-
} else if (
|
|
112
|
+
properties[field] = property;
|
|
113
|
+
} else if (config.type === Date) {
|
|
114
|
+
properties[field] = { type: 'date', format: 'date_optional_time' };
|
|
115
|
+
} else if (config.type === Boolean) {
|
|
116
|
+
properties[field] = { type: 'boolean' };
|
|
117
|
+
} else if (config.type === String) {
|
|
118
118
|
let text = {};
|
|
119
|
-
if (
|
|
119
|
+
if (config.specifiers?.includes('text')) {
|
|
120
120
|
text = {
|
|
121
121
|
fields: {
|
|
122
122
|
text: { type: 'text' }
|
|
123
123
|
}
|
|
124
124
|
};
|
|
125
|
-
if (
|
|
125
|
+
if (esSchema && esSchema.caseSensitive) {
|
|
126
126
|
DataUtil.deepAssign(text, {
|
|
127
127
|
fields: {
|
|
128
128
|
['text_cs']: { type: 'text', analyzer: 'whitespace' }
|
|
@@ -130,17 +130,17 @@ export class ElasticsearchSchemaUtil {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
} else if (
|
|
135
|
-
|
|
136
|
-
} else if (SchemaRegistryIndex.has(
|
|
137
|
-
|
|
138
|
-
type:
|
|
139
|
-
...this.generateSingleMapping(
|
|
133
|
+
properties[field] = { type: 'keyword', ...text };
|
|
134
|
+
} else if (config.type === Object) {
|
|
135
|
+
properties[field] = { type: 'object', dynamic: true };
|
|
136
|
+
} else if (SchemaRegistryIndex.has(config.type)) {
|
|
137
|
+
properties[field] = {
|
|
138
|
+
type: config.array ? 'nested' : 'object',
|
|
139
|
+
...this.generateSingleMapping(config.type, esSchema)
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
return { properties
|
|
144
|
+
return { properties, dynamic: false };
|
|
145
145
|
}
|
|
146
146
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Client, errors, estypes } from '@elastic/elasticsearch';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
ModelCrudSupport,
|
|
4
|
+
ModelCrudSupport, BulkOperation, BulkResponse, ModelBulkSupport, ModelExpirySupport,
|
|
5
5
|
ModelIndexedSupport, ModelType, ModelStorageSupport, NotFoundError, ModelRegistryIndex, OptionalId,
|
|
6
6
|
ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
|
|
7
7
|
} from '@travetto/model';
|
|
@@ -55,41 +55,41 @@ export class ElasticsearchModelService implements
|
|
|
55
55
|
query
|
|
56
56
|
});
|
|
57
57
|
return result;
|
|
58
|
-
} catch (
|
|
59
|
-
if (
|
|
60
|
-
console.error(
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof errors.ResponseError && error.meta.body && typeof error.meta.body === 'object' && 'error' in error.meta.body) {
|
|
60
|
+
console.error(error.meta.body.error);
|
|
61
61
|
}
|
|
62
|
-
throw
|
|
62
|
+
throw error;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
preUpdate(
|
|
67
|
-
preUpdate(
|
|
68
|
-
preUpdate(
|
|
69
|
-
if ('id' in
|
|
70
|
-
const id =
|
|
66
|
+
preUpdate(item: { id: string }): string;
|
|
67
|
+
preUpdate(item: {}): undefined;
|
|
68
|
+
preUpdate(item: { id?: string }): string | undefined {
|
|
69
|
+
if ('id' in item && typeof item.id === 'string') {
|
|
70
|
+
const id = item.id;
|
|
71
71
|
if (!this.config.storeId) {
|
|
72
|
-
delete
|
|
72
|
+
delete item.id;
|
|
73
73
|
}
|
|
74
74
|
return id;
|
|
75
75
|
}
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
postUpdate<T extends ModelType>(
|
|
79
|
+
postUpdate<T extends ModelType>(item: T, id?: string): T {
|
|
80
80
|
if (!this.config.storeId) {
|
|
81
|
-
|
|
81
|
+
item.id = id!;
|
|
82
82
|
}
|
|
83
|
-
return
|
|
83
|
+
return item;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Convert _id to id
|
|
88
88
|
*/
|
|
89
|
-
async postLoad<T extends ModelType>(cls: Class<T>,
|
|
89
|
+
async postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
|
|
90
90
|
let item = {
|
|
91
|
-
...(
|
|
92
|
-
...
|
|
91
|
+
...(input._id ? { id: input._id } : {}),
|
|
92
|
+
...input._source!,
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
item = await ModelCrudUtil.load(cls, item);
|
|
@@ -149,17 +149,17 @@ export class ElasticsearchModelService implements
|
|
|
149
149
|
if (result.result === 'not_found') {
|
|
150
150
|
throw new NotFoundError(cls, id);
|
|
151
151
|
}
|
|
152
|
-
} catch (
|
|
153
|
-
if (
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error && error instanceof errors.ResponseError && error.body && error.body.result === 'not_found') {
|
|
154
154
|
throw new NotFoundError(cls, id);
|
|
155
155
|
}
|
|
156
|
-
throw
|
|
156
|
+
throw error;
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
async create<T extends ModelType>(cls: Class<T>,
|
|
160
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
161
161
|
try {
|
|
162
|
-
const clean = await ModelCrudUtil.preStore(cls,
|
|
162
|
+
const clean = await ModelCrudUtil.preStore(cls, item, this);
|
|
163
163
|
const id = this.preUpdate(clean);
|
|
164
164
|
|
|
165
165
|
await this.client.index({
|
|
@@ -170,18 +170,18 @@ export class ElasticsearchModelService implements
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
return this.postUpdate(clean, id);
|
|
173
|
-
} catch (
|
|
174
|
-
console.error(
|
|
175
|
-
throw
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(error);
|
|
175
|
+
throw error;
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
async update<T extends ModelType>(cls: Class<T>,
|
|
179
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
180
180
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
item = await ModelCrudUtil.preStore(cls, item, this);
|
|
183
183
|
|
|
184
|
-
const id = this.preUpdate(
|
|
184
|
+
const id = this.preUpdate(item);
|
|
185
185
|
|
|
186
186
|
if (ModelRegistryIndex.getConfig(cls).expiresAt) {
|
|
187
187
|
await this.get(cls, id);
|
|
@@ -192,16 +192,16 @@ export class ElasticsearchModelService implements
|
|
|
192
192
|
id,
|
|
193
193
|
op_type: 'index',
|
|
194
194
|
refresh: true,
|
|
195
|
-
body: castTo<T & { id: never }>(
|
|
195
|
+
body: castTo<T & { id: never }>(item)
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
return this.postUpdate(
|
|
198
|
+
return this.postUpdate(item, id);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
async upsert<T extends ModelType>(cls: Class<T>,
|
|
201
|
+
async upsert<T extends ModelType>(cls: Class<T>, input: OptionalId<T>): Promise<T> {
|
|
202
202
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
203
203
|
|
|
204
|
-
const item = await ModelCrudUtil.preStore(cls,
|
|
204
|
+
const item = await ModelCrudUtil.preStore(cls, input, this);
|
|
205
205
|
const id = this.preUpdate(item);
|
|
206
206
|
|
|
207
207
|
await this.client.update({
|
|
@@ -229,11 +229,11 @@ export class ElasticsearchModelService implements
|
|
|
229
229
|
refresh: true,
|
|
230
230
|
script,
|
|
231
231
|
});
|
|
232
|
-
} catch (
|
|
233
|
-
if (
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof Error && /document_missing_exception/.test(error.message)) {
|
|
234
234
|
throw new NotFoundError(cls, id);
|
|
235
235
|
}
|
|
236
|
-
throw
|
|
236
|
+
throw error;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
return this.get(cls, id);
|
|
@@ -247,12 +247,12 @@ export class ElasticsearchModelService implements
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
while (search.hits.hits.length > 0) {
|
|
250
|
-
for (const
|
|
250
|
+
for (const hit of search.hits.hits) {
|
|
251
251
|
try {
|
|
252
|
-
yield this.postLoad(cls,
|
|
253
|
-
} catch (
|
|
254
|
-
if (!(
|
|
255
|
-
throw
|
|
252
|
+
yield this.postLoad(cls, hit);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!(error instanceof NotFoundError)) {
|
|
255
|
+
throw error;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
search = await this.client.scroll({
|
|
@@ -263,28 +263,30 @@ export class ElasticsearchModelService implements
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
async processBulk<T extends ModelType>(cls: Class<T>, operations:
|
|
266
|
+
async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse<EsBulkError>> {
|
|
267
267
|
|
|
268
268
|
await ModelBulkUtil.preStore(cls, operations, this);
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
270
|
+
type BulkDoc = Partial<Record<'delete' | 'create' | 'index' | 'update', { _index: string, _id?: string }>>;
|
|
271
|
+
const body = operations.reduce<(T | BulkDoc | { doc: T })[]>((toRun, operation) => {
|
|
272
|
+
|
|
273
|
+
const core = (operation.upsert ?? operation.delete ?? operation.insert ?? operation.update ?? { constructor: cls });
|
|
274
|
+
const { index } = this.manager.getIdentity(asConstructable<T>(core).constructor);
|
|
275
|
+
const identity: { _index: string, _type?: unknown } = { _index: index };
|
|
276
|
+
|
|
277
|
+
if (operation.delete) {
|
|
278
|
+
toRun.push({ delete: { ...identity, _id: operation.delete.id } });
|
|
279
|
+
} else if (operation.insert) {
|
|
280
|
+
const id = this.preUpdate(operation.insert);
|
|
281
|
+
toRun.push({ create: { ...identity, _id: id } }, castTo(operation.insert));
|
|
282
|
+
} else if (operation.upsert) {
|
|
283
|
+
const id = this.preUpdate(operation.upsert);
|
|
284
|
+
toRun.push({ index: { ...identity, _id: id } }, castTo(operation.upsert));
|
|
285
|
+
} else if (operation.update) {
|
|
286
|
+
const id = this.preUpdate(operation.update);
|
|
287
|
+
toRun.push({ update: { ...identity, _id: id } }, { doc: operation.update });
|
|
286
288
|
}
|
|
287
|
-
return
|
|
289
|
+
return toRun;
|
|
288
290
|
}, []);
|
|
289
291
|
|
|
290
292
|
const result = await this.client.bulk({
|
|
@@ -304,35 +306,35 @@ export class ElasticsearchModelService implements
|
|
|
304
306
|
errors: []
|
|
305
307
|
};
|
|
306
308
|
|
|
307
|
-
type
|
|
309
|
+
type CountProperty = keyof typeof out['counts'];
|
|
308
310
|
|
|
309
311
|
for (let i = 0; i < result.items.length; i++) {
|
|
310
312
|
const item = result.items[i];
|
|
311
|
-
const [
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
313
|
+
const [key] = TypedObject.keys(item);
|
|
314
|
+
const responseItem = item[key]!;
|
|
315
|
+
if (responseItem.error) {
|
|
314
316
|
out.errors.push({
|
|
315
|
-
reason:
|
|
316
|
-
type:
|
|
317
|
+
reason: responseItem.error!.reason!,
|
|
318
|
+
type: responseItem.error!.type
|
|
317
319
|
});
|
|
318
320
|
out.counts.error += 1;
|
|
319
321
|
} else {
|
|
320
|
-
let
|
|
321
|
-
switch (
|
|
322
|
-
case 'create':
|
|
323
|
-
case 'index':
|
|
324
|
-
case 'delete': case 'update':
|
|
322
|
+
let property: CountProperty;
|
|
323
|
+
switch (key) {
|
|
324
|
+
case 'create': property = 'insert'; break;
|
|
325
|
+
case 'index': property = operations[i].insert ? 'insert' : 'upsert'; break;
|
|
326
|
+
case 'delete': case 'update': property = key; break;
|
|
325
327
|
default: {
|
|
326
|
-
throw new Error(`Unknown response key: ${
|
|
328
|
+
throw new Error(`Unknown response key: ${key}`);
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
if (
|
|
331
|
-
out.insertedIds.set(i,
|
|
332
|
-
(operations[i].insert ?? operations[i].upsert)!.id =
|
|
332
|
+
if (responseItem.result === 'created') {
|
|
333
|
+
out.insertedIds.set(i, responseItem._id!);
|
|
334
|
+
(operations[i].insert ?? operations[i].upsert)!.id = responseItem._id!;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
out.counts[
|
|
337
|
+
out.counts[property] += 1;
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
340
|
|
|
@@ -380,7 +382,7 @@ export class ElasticsearchModelService implements
|
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
383
|
-
const
|
|
385
|
+
const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
384
386
|
let search = await this.execSearch<T>(cls, {
|
|
385
387
|
scroll: '2m',
|
|
386
388
|
size: 100,
|
|
@@ -388,16 +390,16 @@ export class ElasticsearchModelService implements
|
|
|
388
390
|
ElasticsearchQueryUtil.extractWhereTermQuery(cls,
|
|
389
391
|
ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
390
392
|
),
|
|
391
|
-
sort: ElasticsearchQueryUtil.getSort(
|
|
393
|
+
sort: ElasticsearchQueryUtil.getSort(config.fields)
|
|
392
394
|
});
|
|
393
395
|
|
|
394
396
|
while (search.hits.hits.length > 0) {
|
|
395
|
-
for (const
|
|
397
|
+
for (const hit of search.hits.hits) {
|
|
396
398
|
try {
|
|
397
|
-
yield this.postLoad(cls,
|
|
398
|
-
} catch (
|
|
399
|
-
if (!(
|
|
400
|
-
throw
|
|
399
|
+
yield this.postLoad(cls, hit);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (!(error instanceof NotFoundError)) {
|
|
402
|
+
throw error;
|
|
401
403
|
}
|
|
402
404
|
}
|
|
403
405
|
search = await this.client.scroll({
|
|
@@ -412,14 +414,14 @@ export class ElasticsearchModelService implements
|
|
|
412
414
|
async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
413
415
|
await QueryVerifier.verify(cls, query);
|
|
414
416
|
|
|
415
|
-
const
|
|
416
|
-
const results = await this.execSearch(cls,
|
|
417
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
|
|
418
|
+
const results = await this.execSearch(cls, search);
|
|
417
419
|
const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
|
|
418
|
-
return Promise.all(results.hits.hits.map(
|
|
420
|
+
return Promise.all(results.hits.hits.map(hit => this.postLoad(cls, hit).then(item => {
|
|
419
421
|
if (shouldRemoveIds) {
|
|
420
|
-
delete castTo<OptionalId<T>>(
|
|
422
|
+
delete castTo<OptionalId<T>>(item).id;
|
|
421
423
|
}
|
|
422
|
-
return
|
|
424
|
+
return item;
|
|
423
425
|
})));
|
|
424
426
|
}
|
|
425
427
|
|
|
@@ -431,8 +433,8 @@ export class ElasticsearchModelService implements
|
|
|
431
433
|
async queryCount<T extends ModelType>(cls: Class<T>, query: Query<T>): Promise<number> {
|
|
432
434
|
await QueryVerifier.verify(cls, query);
|
|
433
435
|
|
|
434
|
-
const
|
|
435
|
-
const result: number | { value: number } = (await this.execSearch(cls,
|
|
436
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, { ...query, limit: 0 }, this.config.schemaConfig);
|
|
437
|
+
const result: number | { value: number } = (await this.execSearch(cls, search)).hits.total || { value: 0 };
|
|
436
438
|
return typeof result !== 'number' ? result.value : result;
|
|
437
439
|
}
|
|
438
440
|
|
|
@@ -470,11 +472,11 @@ export class ElasticsearchModelService implements
|
|
|
470
472
|
if (result.version_conflicts || result.updated === undefined || result.updated === 0) {
|
|
471
473
|
throw new NotFoundError(cls, id);
|
|
472
474
|
}
|
|
473
|
-
} catch (
|
|
474
|
-
if (
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof errors.ResponseError && 'version_conflicts' in error.body) {
|
|
475
477
|
throw new NotFoundError(cls, id);
|
|
476
478
|
} else {
|
|
477
|
-
throw
|
|
479
|
+
throw error;
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
@@ -484,10 +486,10 @@ export class ElasticsearchModelService implements
|
|
|
484
486
|
async deleteByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T> = {}): Promise<number> {
|
|
485
487
|
await QueryVerifier.verify(cls, query);
|
|
486
488
|
|
|
487
|
-
const { sort: _, ...
|
|
489
|
+
const { sort: _, ...rest } = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig, false);
|
|
488
490
|
const result = await this.client.deleteByQuery({
|
|
489
491
|
...this.manager.getIdentity(cls),
|
|
490
|
-
...
|
|
492
|
+
...rest,
|
|
491
493
|
refresh: true,
|
|
492
494
|
});
|
|
493
495
|
return result.deleted ?? 0;
|
|
@@ -514,34 +516,34 @@ export class ElasticsearchModelService implements
|
|
|
514
516
|
async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|
|
515
517
|
await QueryVerifier.verify(cls, query);
|
|
516
518
|
|
|
517
|
-
const
|
|
518
|
-
const search = ElasticsearchQueryUtil.getSearchObject(cls,
|
|
519
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
520
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
519
521
|
const result = await this.execSearch(cls, search);
|
|
520
|
-
const all = await Promise.all(result.hits.hits.map(
|
|
521
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (
|
|
522
|
+
const all = await Promise.all(result.hits.hits.map(hit => this.postLoad(cls, hit)));
|
|
523
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
|
|
522
524
|
}
|
|
523
525
|
|
|
524
526
|
async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
525
527
|
await QueryVerifier.verify(cls, query);
|
|
526
528
|
|
|
527
|
-
const
|
|
529
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, {
|
|
528
530
|
select: castTo({ [field]: 1 }),
|
|
529
531
|
...query
|
|
530
532
|
});
|
|
531
|
-
const search = ElasticsearchQueryUtil.getSearchObject(cls,
|
|
533
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
532
534
|
const result = await this.execSearch(cls, search);
|
|
533
|
-
const all = await Promise.all(result.hits.hits.map(
|
|
534
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all,
|
|
535
|
+
const all = await Promise.all(result.hits.hits.map(hit => castTo<T>(({ [field]: field === 'id' ? hit._id : hit._source![field] }))));
|
|
536
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, item => item, query && query.limit);
|
|
535
537
|
}
|
|
536
538
|
|
|
537
539
|
// Facet
|
|
538
540
|
async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
|
|
539
541
|
await QueryVerifier.verify(cls, query);
|
|
540
542
|
|
|
541
|
-
const
|
|
543
|
+
const resolvedSearch = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
|
|
542
544
|
|
|
543
545
|
const search: estypes.SearchRequest = {
|
|
544
|
-
query:
|
|
546
|
+
query: resolvedSearch.query ?? { ['match_all']: {} },
|
|
545
547
|
aggs: { [field]: { terms: { field, size: 100 } } },
|
|
546
548
|
size: 0
|
|
547
549
|
};
|