@travetto/model-elasticsearch 7.0.0-rc.2 → 7.0.0-rc.4
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 +5 -5
- package/src/config.ts +2 -2
- package/src/index-manager.ts +42 -101
- package/src/internal/schema.ts +30 -0
- package/src/service.ts +3 -4
package/README.md
CHANGED
|
@@ -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
|
*/
|
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.4",
|
|
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.4",
|
|
32
|
+
"@travetto/config": "^7.0.0-rc.4",
|
|
33
|
+
"@travetto/model": "^7.0.0-rc.4",
|
|
34
|
+
"@travetto/model-query": "^7.0.0-rc.4"
|
|
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
|
*/
|
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
|
|
@@ -94,24 +75,6 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
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 identity = this.getIdentity(baseCls);
|
|
103
|
-
try {
|
|
104
|
-
await this.#client.search(identity);
|
|
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
80
|
const { index } = this.getIdentity(cls); // Already namespaced
|
|
@@ -122,78 +85,57 @@ export class IndexManager implements ModelStorageSupport {
|
|
|
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[]>((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
|
-
}, []);
|
|
145
|
-
|
|
146
|
-
// Find which types have changed
|
|
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
|
-
}, []);
|
|
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/schema.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { 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') && 'properties' in input && !!input.properties;
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Utils for ES Schema management
|
|
12
15
|
*/
|
|
@@ -143,4 +146,31 @@ export class ElasticsearchSchemaUtil {
|
|
|
143
146
|
|
|
144
147
|
return { properties, dynamic: false };
|
|
145
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;
|
|
175
|
+
}
|
|
146
176
|
}
|
package/src/service.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
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,
|
|
@@ -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> {
|