@travetto/model-elasticsearch 7.0.0-rc.1 → 7.0.0-rc.3
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 +6 -6
- package/package.json +5 -5
- package/src/config.ts +4 -4
- package/src/index-manager.ts +50 -109
- package/src/internal/query.ts +55 -55
- package/src/internal/schema.ts +72 -42
- package/src/service.ts +108 -107
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
|
```
|
|
@@ -65,9 +65,9 @@ export class ElasticsearchModelConfig {
|
|
|
65
65
|
*/
|
|
66
66
|
namespace = 'app';
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Allow storage modifification
|
|
69
69
|
*/
|
|
70
|
-
|
|
70
|
+
modifyStorage?: boolean;
|
|
71
71
|
/**
|
|
72
72
|
* Should we store the id as a string in the document
|
|
73
73
|
*/
|
|
@@ -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.3",
|
|
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.3",
|
|
32
|
+
"@travetto/config": "^7.0.0-rc.3",
|
|
33
|
+
"@travetto/model": "^7.0.0-rc.3",
|
|
34
|
+
"@travetto/model-query": "^7.0.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"travetto": {
|
|
37
37
|
"displayName": "Elasticsearch Model Source"
|
package/src/config.ts
CHANGED
|
@@ -25,9 +25,9 @@ export class ElasticsearchModelConfig {
|
|
|
25
25
|
*/
|
|
26
26
|
namespace = 'app';
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Allow storage modifification
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
modifyStorage?: boolean;
|
|
31
31
|
/**
|
|
32
32
|
* Should we store the id as a string in the document
|
|
33
33
|
*/
|
|
@@ -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
|
@@ -2,7 +2,6 @@ import { Client, estypes } from '@elastic/elasticsearch';
|
|
|
2
2
|
|
|
3
3
|
import { Class } from '@travetto/runtime';
|
|
4
4
|
import { ModelRegistryIndex, ModelType, ModelStorageSupport } from '@travetto/model';
|
|
5
|
-
import { SchemaChange, SchemaRegistryIndex } from '@travetto/schema';
|
|
6
5
|
|
|
7
6
|
import { ElasticsearchModelConfig } from './config.ts';
|
|
8
7
|
import { ElasticsearchSchemaUtil } from './internal/schema.ts';
|
|
@@ -12,8 +11,6 @@ import { ElasticsearchSchemaUtil } from './internal/schema.ts';
|
|
|
12
11
|
*/
|
|
13
12
|
export class IndexManager implements ModelStorageSupport {
|
|
14
13
|
|
|
15
|
-
#indexToAlias = new Map<string, string>();
|
|
16
|
-
#aliasToIndex = new Map<string, string>();
|
|
17
14
|
#identities = new Map<Class, { index: string }>();
|
|
18
15
|
#client: Client;
|
|
19
16
|
config: ElasticsearchModelConfig;
|
|
@@ -24,7 +21,7 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
getStore(cls: Class): string {
|
|
27
|
-
return ModelRegistryIndex.getStoreName(cls)
|
|
24
|
+
return ModelRegistryIndex.getStoreName(cls);
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
/**
|
|
@@ -51,22 +48,6 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
51
48
|
return { ...this.#identities.get(cls)! };
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
/**
|
|
55
|
-
* Build alias mappings from the current state in the database
|
|
56
|
-
*/
|
|
57
|
-
async computeAliasMappings(force = false): Promise<void> {
|
|
58
|
-
if (force || !this.#indexToAlias.size) {
|
|
59
|
-
const aliases = await this.#client.cat.aliases({ format: 'json' });
|
|
60
|
-
|
|
61
|
-
this.#indexToAlias = new Map();
|
|
62
|
-
this.#aliasToIndex = new Map();
|
|
63
|
-
for (const al of aliases) {
|
|
64
|
-
this.#indexToAlias.set(al.index!, al.alias!);
|
|
65
|
-
this.#aliasToIndex.set(al.alias!, al.index!);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
51
|
/**
|
|
71
52
|
* Create index for type
|
|
72
53
|
* @param cls
|
|
@@ -74,126 +55,87 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
74
55
|
*/
|
|
75
56
|
async createIndex(cls: Class, alias = true): Promise<string> {
|
|
76
57
|
const mapping = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
77
|
-
const
|
|
78
|
-
const concreteIndex = `${
|
|
58
|
+
const { index } = this.getIdentity(cls); // Already namespaced
|
|
59
|
+
const concreteIndex = `${index}_${Date.now()}`;
|
|
79
60
|
try {
|
|
80
61
|
await this.#client.indices.create({
|
|
81
62
|
index: concreteIndex,
|
|
82
63
|
mappings: mapping,
|
|
83
64
|
settings: this.config.indexCreate,
|
|
84
|
-
...(alias ? { aliases: { [
|
|
65
|
+
...(alias ? { aliases: { [index]: {} } } : {})
|
|
85
66
|
});
|
|
86
|
-
console.debug('Index created', { index
|
|
67
|
+
console.debug('Index created', { index, concrete: concreteIndex });
|
|
87
68
|
console.debug('Index Config', {
|
|
88
69
|
mappings: mapping,
|
|
89
70
|
settings: this.config.indexCreate
|
|
90
71
|
});
|
|
91
|
-
} catch (
|
|
92
|
-
console.warn('Index already created', { index
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn('Index already created', { index, error });
|
|
93
74
|
}
|
|
94
75
|
return concreteIndex;
|
|
95
76
|
}
|
|
96
77
|
|
|
97
|
-
/**
|
|
98
|
-
* Build an index if missing
|
|
99
|
-
*/
|
|
100
|
-
async createIndexIfMissing(cls: Class): Promise<void> {
|
|
101
|
-
const baseCls = SchemaRegistryIndex.getBaseClass(cls);
|
|
102
|
-
const ident = this.getIdentity(baseCls);
|
|
103
|
-
try {
|
|
104
|
-
await this.#client.search(ident);
|
|
105
|
-
} catch {
|
|
106
|
-
await this.createIndex(baseCls);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async createModel(cls: Class<ModelType>): Promise<void> {
|
|
111
|
-
await this.createIndexIfMissing(cls);
|
|
112
|
-
await this.computeAliasMappings(true);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
78
|
async exportModel(cls: Class<ModelType>): Promise<string> {
|
|
116
79
|
const schema = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
117
|
-
const
|
|
118
|
-
return `curl -XPOST $ES_HOST/${
|
|
80
|
+
const { index } = this.getIdentity(cls); // Already namespaced
|
|
81
|
+
return `curl -XPOST $ES_HOST/${index} -d '${JSON.stringify({
|
|
119
82
|
mappings: schema,
|
|
120
83
|
settings: this.config.indexCreate
|
|
121
84
|
})}'`;
|
|
122
85
|
}
|
|
123
86
|
|
|
124
87
|
async deleteModel(cls: Class<ModelType>): Promise<void> {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
}
|
|
88
|
+
const { index } = this.getIdentity(cls);
|
|
89
|
+
const aliasedIndices = await this.#client.indices.getAlias();
|
|
90
|
+
|
|
91
|
+
const toDelete = Object.keys(aliasedIndices[index]?.aliases ?? {})
|
|
92
|
+
.filter(item => index in (aliasedIndices[item]?.aliases ?? {}));
|
|
93
|
+
|
|
94
|
+
console.debug('Deleting Model', { index, toDelete });
|
|
95
|
+
await Promise.all(toDelete.map(target => this.#client.indices.delete({ index: target })));
|
|
132
96
|
}
|
|
133
97
|
|
|
134
98
|
/**
|
|
135
|
-
*
|
|
99
|
+
* Create or update schema as necessary
|
|
136
100
|
*/
|
|
137
|
-
async
|
|
138
|
-
// Find which fields are gone
|
|
139
|
-
const removes = change.subs.reduce<string[]>((acc, v) => {
|
|
140
|
-
acc.push(...v.fields
|
|
141
|
-
.filter(ev => ev.type === 'removing')
|
|
142
|
-
.map(ev => [...v.path.map(f => f.name), ev.prev!.name].join('.')));
|
|
143
|
-
return acc;
|
|
144
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
// Find which types have changed
|
|
147
|
-
const fieldChanges = change.subs.reduce<string[]>((acc, v) => {
|
|
148
|
-
acc.push(...v.fields
|
|
149
|
-
.filter(ev => ev.type === 'changed')
|
|
150
|
-
.filter(ev => ev.prev?.type !== ev.curr?.type)
|
|
151
|
-
.map(ev => [...v.path.map(f => f.name), ev.prev!.name].join('.')));
|
|
152
|
-
return acc;
|
|
153
|
-
}, []);
|
|
154
|
-
|
|
101
|
+
async upsertModel(cls: Class<ModelType>): Promise<void> {
|
|
155
102
|
const { index } = this.getIdentity(cls);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await this
|
|
187
|
-
index,
|
|
188
|
-
...schema,
|
|
189
|
-
});
|
|
103
|
+
const resolvedAlias = await this.#client.indices.getMapping({ index }).catch(() => undefined);
|
|
104
|
+
|
|
105
|
+
if (resolvedAlias) {
|
|
106
|
+
const [currentIndex] = Object.keys(resolvedAlias ?? {});
|
|
107
|
+
const pendingMapping = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
|
|
108
|
+
const changedFields = ElasticsearchSchemaUtil.getChangedFields(resolvedAlias[currentIndex].mappings, pendingMapping);
|
|
109
|
+
|
|
110
|
+
if (changedFields.length) { // If any fields changed, reindex
|
|
111
|
+
console.debug('Updated Model', { index, currentIndex, changedFields });
|
|
112
|
+
const pendingIndex = await this.createIndex(cls, false);
|
|
113
|
+
|
|
114
|
+
const reindexBody: estypes.ReindexRequest = {
|
|
115
|
+
source: { index: currentIndex },
|
|
116
|
+
dest: { index: pendingIndex },
|
|
117
|
+
script: {
|
|
118
|
+
lang: 'painless',
|
|
119
|
+
source: changedFields.map(change => `ctx._source.remove("${change}");`).join(' ') // Removing
|
|
120
|
+
},
|
|
121
|
+
wait_for_completion: true
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await this.#client.reindex(reindexBody);
|
|
125
|
+
|
|
126
|
+
// Update aliases
|
|
127
|
+
await this.#client.indices.putAlias({ index: pendingIndex, name: index });
|
|
128
|
+
const toDelete = Object.keys(resolvedAlias).filter(item => item !== pendingIndex);
|
|
129
|
+
await Promise.all(toDelete.map(alias => this.#client.indices.delete({ index: alias })));
|
|
130
|
+
}
|
|
131
|
+
} else { // Create if non-existent
|
|
132
|
+
console.debug('Creating Model', { index });
|
|
133
|
+
await this.createIndex(cls);
|
|
190
134
|
}
|
|
191
135
|
}
|
|
192
136
|
|
|
193
137
|
async createStorage(): Promise<void> {
|
|
194
|
-
// Pre-create indexes if missing
|
|
195
138
|
console.debug('Create Storage', { idx: this.getNamespacedIndex('*') });
|
|
196
|
-
await this.computeAliasMappings(true);
|
|
197
139
|
}
|
|
198
140
|
|
|
199
141
|
async deleteStorage(): Promise<void> {
|
|
@@ -201,6 +143,5 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
201
143
|
await this.#client.indices.delete({
|
|
202
144
|
index: this.getNamespacedIndex('*')
|
|
203
145
|
});
|
|
204
|
-
await this.computeAliasMappings(true);
|
|
205
146
|
}
|
|
206
147
|
}
|
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,10 @@ 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
|
+
|
|
10
|
+
const isMappingType = (input: estypes.MappingProperty): input is estypes.MappingTypeMapping =>
|
|
11
|
+
(input.type === 'object' || input.type === 'nested') && 'properties' in input && !!input.properties;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Utils for ES Schema management
|
|
@@ -15,7 +18,7 @@ export class ElasticsearchSchemaUtil {
|
|
|
15
18
|
/**
|
|
16
19
|
* Build the update script for a given object
|
|
17
20
|
*/
|
|
18
|
-
static generateUpdateScript(
|
|
21
|
+
static generateUpdateScript(item: Record<string, unknown>): estypes.Script {
|
|
19
22
|
const out: estypes.Script = {
|
|
20
23
|
lang: 'painless',
|
|
21
24
|
source: `
|
|
@@ -32,21 +35,21 @@ export class ElasticsearchSchemaUtil {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
`,
|
|
35
|
-
params: { body:
|
|
38
|
+
params: { body: item },
|
|
36
39
|
};
|
|
37
40
|
return out;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* Generate replace script
|
|
42
|
-
* @param
|
|
45
|
+
* @param item
|
|
43
46
|
* @returns
|
|
44
47
|
*/
|
|
45
|
-
static generateReplaceScript(
|
|
48
|
+
static generateReplaceScript(item: Record<string, unknown>): estypes.Script {
|
|
46
49
|
return {
|
|
47
50
|
lang: 'painless',
|
|
48
51
|
source: 'ctx._source.clear(); ctx._source.putAll(params.body)',
|
|
49
|
-
params: { body:
|
|
52
|
+
params: { body: item }
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -64,65 +67,65 @@ export class ElasticsearchSchemaUtil {
|
|
|
64
67
|
*/
|
|
65
68
|
static generateAllMapping(cls: Class, config?: EsSchemaConfig): estypes.MappingTypeMapping {
|
|
66
69
|
const allTypes = SchemaRegistryIndex.getDiscriminatedClasses(cls);
|
|
67
|
-
return allTypes.reduce<estypes.MappingTypeMapping>((
|
|
68
|
-
DataUtil.deepAssign(
|
|
69
|
-
return
|
|
70
|
+
return allTypes.reduce<estypes.MappingTypeMapping>((mapping, schemaCls) => {
|
|
71
|
+
DataUtil.deepAssign(mapping, this.generateSingleMapping(schemaCls, config));
|
|
72
|
+
return mapping;
|
|
70
73
|
}, { properties: {}, dynamic: false });
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/**
|
|
74
77
|
* Build a mapping for a given class
|
|
75
78
|
*/
|
|
76
|
-
static generateSingleMapping<T>(cls: Class<T>,
|
|
79
|
+
static generateSingleMapping<T>(cls: Class<T>, esSchema?: EsSchemaConfig): estypes.MappingTypeMapping {
|
|
77
80
|
const fields = SchemaRegistryIndex.get(cls).getFields();
|
|
78
81
|
|
|
79
|
-
const
|
|
82
|
+
const properties: Record<string, estypes.MappingProperty> = {};
|
|
80
83
|
|
|
81
|
-
for (const [field,
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
} else if (
|
|
85
|
-
let
|
|
86
|
-
if (
|
|
87
|
-
const [digits, decimals] =
|
|
84
|
+
for (const [field, config] of Object.entries(fields)) {
|
|
85
|
+
if (config.type === PointConcrete) {
|
|
86
|
+
properties[field] = { type: 'geo_point' };
|
|
87
|
+
} else if (config.type === Number) {
|
|
88
|
+
let property: Record<string, unknown> = { type: 'integer' };
|
|
89
|
+
if (config.precision) {
|
|
90
|
+
const [digits, decimals] = config.precision;
|
|
88
91
|
if (decimals) {
|
|
89
92
|
if ((decimals + digits) < 16) {
|
|
90
|
-
|
|
93
|
+
property = { type: 'scaled_float', ['scaling_factor']: decimals };
|
|
91
94
|
} else {
|
|
92
95
|
if (digits < 6 && decimals < 9) {
|
|
93
|
-
|
|
96
|
+
property = { type: 'half_float' };
|
|
94
97
|
} else if (digits > 20) {
|
|
95
|
-
|
|
98
|
+
property = { type: 'double' };
|
|
96
99
|
} else {
|
|
97
|
-
|
|
100
|
+
property = { type: 'float' };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
} else if (digits) {
|
|
101
104
|
if (digits <= 2) {
|
|
102
|
-
|
|
105
|
+
property = { type: 'byte' };
|
|
103
106
|
} else if (digits <= 4) {
|
|
104
|
-
|
|
107
|
+
property = { type: 'short' };
|
|
105
108
|
} else if (digits <= 9) {
|
|
106
|
-
|
|
109
|
+
property = { type: 'integer' };
|
|
107
110
|
} else {
|
|
108
|
-
|
|
111
|
+
property = { type: 'long' };
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
} else if (
|
|
114
|
-
|
|
115
|
-
} else if (
|
|
116
|
-
|
|
117
|
-
} else if (
|
|
115
|
+
properties[field] = property;
|
|
116
|
+
} else if (config.type === Date) {
|
|
117
|
+
properties[field] = { type: 'date', format: 'date_optional_time' };
|
|
118
|
+
} else if (config.type === Boolean) {
|
|
119
|
+
properties[field] = { type: 'boolean' };
|
|
120
|
+
} else if (config.type === String) {
|
|
118
121
|
let text = {};
|
|
119
|
-
if (
|
|
122
|
+
if (config.specifiers?.includes('text')) {
|
|
120
123
|
text = {
|
|
121
124
|
fields: {
|
|
122
125
|
text: { type: 'text' }
|
|
123
126
|
}
|
|
124
127
|
};
|
|
125
|
-
if (
|
|
128
|
+
if (esSchema && esSchema.caseSensitive) {
|
|
126
129
|
DataUtil.deepAssign(text, {
|
|
127
130
|
fields: {
|
|
128
131
|
['text_cs']: { type: 'text', analyzer: 'whitespace' }
|
|
@@ -130,17 +133,44 @@ export class ElasticsearchSchemaUtil {
|
|
|
130
133
|
});
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
|
-
|
|
134
|
-
} else if (
|
|
135
|
-
|
|
136
|
-
} else if (SchemaRegistryIndex.has(
|
|
137
|
-
|
|
138
|
-
type:
|
|
139
|
-
...this.generateSingleMapping(
|
|
136
|
+
properties[field] = { type: 'keyword', ...text };
|
|
137
|
+
} else if (config.type === Object) {
|
|
138
|
+
properties[field] = { type: 'object', dynamic: true };
|
|
139
|
+
} else if (SchemaRegistryIndex.has(config.type)) {
|
|
140
|
+
properties[field] = {
|
|
141
|
+
type: config.array ? 'nested' : 'object',
|
|
142
|
+
...this.generateSingleMapping(config.type, esSchema)
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
return { properties
|
|
147
|
+
return { properties, dynamic: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gets list of all changed fields between two mappings
|
|
152
|
+
*/
|
|
153
|
+
static getChangedFields(current: estypes.MappingTypeMapping, needed: estypes.MappingTypeMapping, prefix = ''): string[] {
|
|
154
|
+
const currentProperties = (current.properties ?? {});
|
|
155
|
+
const neededProperties = (needed.properties ?? {});
|
|
156
|
+
const allKeys = new Set([...Object.keys(currentProperties), ...Object.keys(neededProperties)]);
|
|
157
|
+
const changed: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const key of allKeys) {
|
|
160
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
161
|
+
const currentProperty = currentProperties[key];
|
|
162
|
+
const neededProperty = neededProperties[key];
|
|
163
|
+
|
|
164
|
+
if (!currentProperty || !neededProperty || currentProperty.type !== neededProperty.type) {
|
|
165
|
+
changed.push(path);
|
|
166
|
+
} else if (isMappingType(currentProperty) || isMappingType(neededProperty)) {
|
|
167
|
+
changed.push(...this.getChangedFields(
|
|
168
|
+
'properties' in currentProperty ? currentProperty : { properties: {} },
|
|
169
|
+
'properties' in neededProperty ? neededProperty : { properties: {} },
|
|
170
|
+
path
|
|
171
|
+
));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return changed;
|
|
145
175
|
}
|
|
146
176
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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';
|
|
8
8
|
import { ShutdownManager, type DeepPartial, type Class, castTo, asFull, TypedObject, asConstructable } from '@travetto/runtime';
|
|
9
|
-
import {
|
|
9
|
+
import { BindUtil } from '@travetto/schema';
|
|
10
10
|
import { Injectable } from '@travetto/di';
|
|
11
11
|
import {
|
|
12
12
|
ModelQuery, ModelQueryCrudSupport, ModelQueryFacetSupport,
|
|
@@ -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);
|
|
@@ -115,17 +115,16 @@ export class ElasticsearchModelService implements
|
|
|
115
115
|
await this.client.cluster.health({});
|
|
116
116
|
this.manager = new IndexManager(this.config, this.client);
|
|
117
117
|
|
|
118
|
-
await ModelStorageUtil.
|
|
118
|
+
await ModelStorageUtil.storageInitialization(this.manager);
|
|
119
119
|
ShutdownManager.onGracefulShutdown(() => this.client.close());
|
|
120
120
|
ModelExpiryUtil.registerCull(this);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
createStorage(): Promise<void> { return this.manager.createStorage(); }
|
|
124
124
|
deleteStorage(): Promise<void> { return this.manager.deleteStorage(); }
|
|
125
|
-
|
|
125
|
+
upsertModel(cls: Class): Promise<void> { return this.manager.upsertModel(cls); }
|
|
126
126
|
exportModel(cls: Class): Promise<string> { return this.manager.exportModel(cls); }
|
|
127
127
|
deleteModel(cls: Class): Promise<void> { return this.manager.deleteModel(cls); }
|
|
128
|
-
changeSchema(cls: Class, change: SchemaChange): Promise<void> { return this.manager.changeSchema(cls, change); }
|
|
129
128
|
truncateModel(cls: Class): Promise<void> { return this.deleteByQuery(cls, {}).then(() => { }); }
|
|
130
129
|
|
|
131
130
|
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
@@ -149,17 +148,17 @@ export class ElasticsearchModelService implements
|
|
|
149
148
|
if (result.result === 'not_found') {
|
|
150
149
|
throw new NotFoundError(cls, id);
|
|
151
150
|
}
|
|
152
|
-
} catch (
|
|
153
|
-
if (
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error && error instanceof errors.ResponseError && error.body && error.body.result === 'not_found') {
|
|
154
153
|
throw new NotFoundError(cls, id);
|
|
155
154
|
}
|
|
156
|
-
throw
|
|
155
|
+
throw error;
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
async create<T extends ModelType>(cls: Class<T>,
|
|
159
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
161
160
|
try {
|
|
162
|
-
const clean = await ModelCrudUtil.preStore(cls,
|
|
161
|
+
const clean = await ModelCrudUtil.preStore(cls, item, this);
|
|
163
162
|
const id = this.preUpdate(clean);
|
|
164
163
|
|
|
165
164
|
await this.client.index({
|
|
@@ -170,18 +169,18 @@ export class ElasticsearchModelService implements
|
|
|
170
169
|
});
|
|
171
170
|
|
|
172
171
|
return this.postUpdate(clean, id);
|
|
173
|
-
} catch (
|
|
174
|
-
console.error(
|
|
175
|
-
throw
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(error);
|
|
174
|
+
throw error;
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
async update<T extends ModelType>(cls: Class<T>,
|
|
178
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
180
179
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
181
180
|
|
|
182
|
-
|
|
181
|
+
item = await ModelCrudUtil.preStore(cls, item, this);
|
|
183
182
|
|
|
184
|
-
const id = this.preUpdate(
|
|
183
|
+
const id = this.preUpdate(item);
|
|
185
184
|
|
|
186
185
|
if (ModelRegistryIndex.getConfig(cls).expiresAt) {
|
|
187
186
|
await this.get(cls, id);
|
|
@@ -192,16 +191,16 @@ export class ElasticsearchModelService implements
|
|
|
192
191
|
id,
|
|
193
192
|
op_type: 'index',
|
|
194
193
|
refresh: true,
|
|
195
|
-
body: castTo<T & { id: never }>(
|
|
194
|
+
body: castTo<T & { id: never }>(item)
|
|
196
195
|
});
|
|
197
196
|
|
|
198
|
-
return this.postUpdate(
|
|
197
|
+
return this.postUpdate(item, id);
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
async upsert<T extends ModelType>(cls: Class<T>,
|
|
200
|
+
async upsert<T extends ModelType>(cls: Class<T>, input: OptionalId<T>): Promise<T> {
|
|
202
201
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
203
202
|
|
|
204
|
-
const item = await ModelCrudUtil.preStore(cls,
|
|
203
|
+
const item = await ModelCrudUtil.preStore(cls, input, this);
|
|
205
204
|
const id = this.preUpdate(item);
|
|
206
205
|
|
|
207
206
|
await this.client.update({
|
|
@@ -229,11 +228,11 @@ export class ElasticsearchModelService implements
|
|
|
229
228
|
refresh: true,
|
|
230
229
|
script,
|
|
231
230
|
});
|
|
232
|
-
} catch (
|
|
233
|
-
if (
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof Error && /document_missing_exception/.test(error.message)) {
|
|
234
233
|
throw new NotFoundError(cls, id);
|
|
235
234
|
}
|
|
236
|
-
throw
|
|
235
|
+
throw error;
|
|
237
236
|
}
|
|
238
237
|
|
|
239
238
|
return this.get(cls, id);
|
|
@@ -247,12 +246,12 @@ export class ElasticsearchModelService implements
|
|
|
247
246
|
});
|
|
248
247
|
|
|
249
248
|
while (search.hits.hits.length > 0) {
|
|
250
|
-
for (const
|
|
249
|
+
for (const hit of search.hits.hits) {
|
|
251
250
|
try {
|
|
252
|
-
yield this.postLoad(cls,
|
|
253
|
-
} catch (
|
|
254
|
-
if (!(
|
|
255
|
-
throw
|
|
251
|
+
yield this.postLoad(cls, hit);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (!(error instanceof NotFoundError)) {
|
|
254
|
+
throw error;
|
|
256
255
|
}
|
|
257
256
|
}
|
|
258
257
|
search = await this.client.scroll({
|
|
@@ -263,28 +262,30 @@ export class ElasticsearchModelService implements
|
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
264
|
|
|
266
|
-
async processBulk<T extends ModelType>(cls: Class<T>, operations:
|
|
265
|
+
async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse<EsBulkError>> {
|
|
267
266
|
|
|
268
267
|
await ModelBulkUtil.preStore(cls, operations, this);
|
|
269
268
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
269
|
+
type BulkDoc = Partial<Record<'delete' | 'create' | 'index' | 'update', { _index: string, _id?: string }>>;
|
|
270
|
+
const body = operations.reduce<(T | BulkDoc | { doc: T })[]>((toRun, operation) => {
|
|
271
|
+
|
|
272
|
+
const core = (operation.upsert ?? operation.delete ?? operation.insert ?? operation.update ?? { constructor: cls });
|
|
273
|
+
const { index } = this.manager.getIdentity(asConstructable<T>(core).constructor);
|
|
274
|
+
const identity: { _index: string, _type?: unknown } = { _index: index };
|
|
275
|
+
|
|
276
|
+
if (operation.delete) {
|
|
277
|
+
toRun.push({ delete: { ...identity, _id: operation.delete.id } });
|
|
278
|
+
} else if (operation.insert) {
|
|
279
|
+
const id = this.preUpdate(operation.insert);
|
|
280
|
+
toRun.push({ create: { ...identity, _id: id } }, castTo(operation.insert));
|
|
281
|
+
} else if (operation.upsert) {
|
|
282
|
+
const id = this.preUpdate(operation.upsert);
|
|
283
|
+
toRun.push({ index: { ...identity, _id: id } }, castTo(operation.upsert));
|
|
284
|
+
} else if (operation.update) {
|
|
285
|
+
const id = this.preUpdate(operation.update);
|
|
286
|
+
toRun.push({ update: { ...identity, _id: id } }, { doc: operation.update });
|
|
286
287
|
}
|
|
287
|
-
return
|
|
288
|
+
return toRun;
|
|
288
289
|
}, []);
|
|
289
290
|
|
|
290
291
|
const result = await this.client.bulk({
|
|
@@ -304,35 +305,35 @@ export class ElasticsearchModelService implements
|
|
|
304
305
|
errors: []
|
|
305
306
|
};
|
|
306
307
|
|
|
307
|
-
type
|
|
308
|
+
type CountProperty = keyof typeof out['counts'];
|
|
308
309
|
|
|
309
310
|
for (let i = 0; i < result.items.length; i++) {
|
|
310
311
|
const item = result.items[i];
|
|
311
|
-
const [
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
312
|
+
const [key] = TypedObject.keys(item);
|
|
313
|
+
const responseItem = item[key]!;
|
|
314
|
+
if (responseItem.error) {
|
|
314
315
|
out.errors.push({
|
|
315
|
-
reason:
|
|
316
|
-
type:
|
|
316
|
+
reason: responseItem.error!.reason!,
|
|
317
|
+
type: responseItem.error!.type
|
|
317
318
|
});
|
|
318
319
|
out.counts.error += 1;
|
|
319
320
|
} else {
|
|
320
|
-
let
|
|
321
|
-
switch (
|
|
322
|
-
case 'create':
|
|
323
|
-
case 'index':
|
|
324
|
-
case 'delete': case 'update':
|
|
321
|
+
let property: CountProperty;
|
|
322
|
+
switch (key) {
|
|
323
|
+
case 'create': property = 'insert'; break;
|
|
324
|
+
case 'index': property = operations[i].insert ? 'insert' : 'upsert'; break;
|
|
325
|
+
case 'delete': case 'update': property = key; break;
|
|
325
326
|
default: {
|
|
326
|
-
throw new Error(`Unknown response key: ${
|
|
327
|
+
throw new Error(`Unknown response key: ${key}`);
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
if (
|
|
331
|
-
out.insertedIds.set(i,
|
|
332
|
-
(operations[i].insert ?? operations[i].upsert)!.id =
|
|
331
|
+
if (responseItem.result === 'created') {
|
|
332
|
+
out.insertedIds.set(i, responseItem._id!);
|
|
333
|
+
(operations[i].insert ?? operations[i].upsert)!.id = responseItem._id!;
|
|
333
334
|
}
|
|
334
335
|
|
|
335
|
-
out.counts[
|
|
336
|
+
out.counts[property] += 1;
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
339
|
|
|
@@ -380,7 +381,7 @@ export class ElasticsearchModelService implements
|
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
383
|
-
const
|
|
384
|
+
const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
384
385
|
let search = await this.execSearch<T>(cls, {
|
|
385
386
|
scroll: '2m',
|
|
386
387
|
size: 100,
|
|
@@ -388,16 +389,16 @@ export class ElasticsearchModelService implements
|
|
|
388
389
|
ElasticsearchQueryUtil.extractWhereTermQuery(cls,
|
|
389
390
|
ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
390
391
|
),
|
|
391
|
-
sort: ElasticsearchQueryUtil.getSort(
|
|
392
|
+
sort: ElasticsearchQueryUtil.getSort(config.fields)
|
|
392
393
|
});
|
|
393
394
|
|
|
394
395
|
while (search.hits.hits.length > 0) {
|
|
395
|
-
for (const
|
|
396
|
+
for (const hit of search.hits.hits) {
|
|
396
397
|
try {
|
|
397
|
-
yield this.postLoad(cls,
|
|
398
|
-
} catch (
|
|
399
|
-
if (!(
|
|
400
|
-
throw
|
|
398
|
+
yield this.postLoad(cls, hit);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (!(error instanceof NotFoundError)) {
|
|
401
|
+
throw error;
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
search = await this.client.scroll({
|
|
@@ -412,14 +413,14 @@ export class ElasticsearchModelService implements
|
|
|
412
413
|
async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
|
|
413
414
|
await QueryVerifier.verify(cls, query);
|
|
414
415
|
|
|
415
|
-
const
|
|
416
|
-
const results = await this.execSearch(cls,
|
|
416
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
|
|
417
|
+
const results = await this.execSearch(cls, search);
|
|
417
418
|
const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
|
|
418
|
-
return Promise.all(results.hits.hits.map(
|
|
419
|
+
return Promise.all(results.hits.hits.map(hit => this.postLoad(cls, hit).then(item => {
|
|
419
420
|
if (shouldRemoveIds) {
|
|
420
|
-
delete castTo<OptionalId<T>>(
|
|
421
|
+
delete castTo<OptionalId<T>>(item).id;
|
|
421
422
|
}
|
|
422
|
-
return
|
|
423
|
+
return item;
|
|
423
424
|
})));
|
|
424
425
|
}
|
|
425
426
|
|
|
@@ -431,8 +432,8 @@ export class ElasticsearchModelService implements
|
|
|
431
432
|
async queryCount<T extends ModelType>(cls: Class<T>, query: Query<T>): Promise<number> {
|
|
432
433
|
await QueryVerifier.verify(cls, query);
|
|
433
434
|
|
|
434
|
-
const
|
|
435
|
-
const result: number | { value: number } = (await this.execSearch(cls,
|
|
435
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, { ...query, limit: 0 }, this.config.schemaConfig);
|
|
436
|
+
const result: number | { value: number } = (await this.execSearch(cls, search)).hits.total || { value: 0 };
|
|
436
437
|
return typeof result !== 'number' ? result.value : result;
|
|
437
438
|
}
|
|
438
439
|
|
|
@@ -470,11 +471,11 @@ export class ElasticsearchModelService implements
|
|
|
470
471
|
if (result.version_conflicts || result.updated === undefined || result.updated === 0) {
|
|
471
472
|
throw new NotFoundError(cls, id);
|
|
472
473
|
}
|
|
473
|
-
} catch (
|
|
474
|
-
if (
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error instanceof errors.ResponseError && 'version_conflicts' in error.body) {
|
|
475
476
|
throw new NotFoundError(cls, id);
|
|
476
477
|
} else {
|
|
477
|
-
throw
|
|
478
|
+
throw error;
|
|
478
479
|
}
|
|
479
480
|
}
|
|
480
481
|
|
|
@@ -484,10 +485,10 @@ export class ElasticsearchModelService implements
|
|
|
484
485
|
async deleteByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T> = {}): Promise<number> {
|
|
485
486
|
await QueryVerifier.verify(cls, query);
|
|
486
487
|
|
|
487
|
-
const { sort: _, ...
|
|
488
|
+
const { sort: _, ...rest } = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig, false);
|
|
488
489
|
const result = await this.client.deleteByQuery({
|
|
489
490
|
...this.manager.getIdentity(cls),
|
|
490
|
-
...
|
|
491
|
+
...rest,
|
|
491
492
|
refresh: true,
|
|
492
493
|
});
|
|
493
494
|
return result.deleted ?? 0;
|
|
@@ -514,34 +515,34 @@ export class ElasticsearchModelService implements
|
|
|
514
515
|
async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|
|
515
516
|
await QueryVerifier.verify(cls, query);
|
|
516
517
|
|
|
517
|
-
const
|
|
518
|
-
const search = ElasticsearchQueryUtil.getSearchObject(cls,
|
|
518
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
519
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
519
520
|
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, (
|
|
521
|
+
const all = await Promise.all(result.hits.hits.map(hit => this.postLoad(cls, hit)));
|
|
522
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
525
526
|
await QueryVerifier.verify(cls, query);
|
|
526
527
|
|
|
527
|
-
const
|
|
528
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, {
|
|
528
529
|
select: castTo({ [field]: 1 }),
|
|
529
530
|
...query
|
|
530
531
|
});
|
|
531
|
-
const search = ElasticsearchQueryUtil.getSearchObject(cls,
|
|
532
|
+
const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
|
|
532
533
|
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,
|
|
534
|
+
const all = await Promise.all(result.hits.hits.map(hit => castTo<T>(({ [field]: field === 'id' ? hit._id : hit._source![field] }))));
|
|
535
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, item => item, query && query.limit);
|
|
535
536
|
}
|
|
536
537
|
|
|
537
538
|
// Facet
|
|
538
539
|
async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
|
|
539
540
|
await QueryVerifier.verify(cls, query);
|
|
540
541
|
|
|
541
|
-
const
|
|
542
|
+
const resolvedSearch = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
|
|
542
543
|
|
|
543
544
|
const search: estypes.SearchRequest = {
|
|
544
|
-
query:
|
|
545
|
+
query: resolvedSearch.query ?? { ['match_all']: {} },
|
|
545
546
|
aggs: { [field]: { terms: { field, size: 100 } } },
|
|
546
547
|
size: 0
|
|
547
548
|
};
|