@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 CHANGED
@@ -65,9 +65,9 @@ export class ElasticsearchModelConfig {
65
65
  */
66
66
  namespace = 'app';
67
67
  /**
68
- * Auto-create, disabled in prod by default
68
+ * Allow storage modifification
69
69
  */
70
- autoCreate?: boolean;
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.2",
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.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"
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
- * Auto-create, disabled in prod by default
28
+ * Allow storage modifification
29
29
  */
30
- autoCreate?: boolean;
30
+ modifyStorage?: boolean;
31
31
  /**
32
32
  * Should we store the id as a string in the document
33
33
  */
@@ -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).toLowerCase().replace(/[^A-Za-z0-9_]+/g, '_');
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 alias = this.getNamespacedIndex(this.getStore(cls));
126
- if (this.#aliasToIndex.get(alias)) {
127
- await this.#client.indices.delete({
128
- index: this.#aliasToIndex.get(alias)!
129
- });
130
- await this.computeAliasMappings(true);
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
- * When the schema changes
99
+ * Create or update schema as necessary
136
100
  */
137
- async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
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
- // If removing fields or changing types, run as script to update data
158
- if (removes.length || fieldChanges.length) { // Removing and adding
159
- const next = await this.createIndex(cls, false);
160
-
161
- const aliases = (await this.#client.indices.getAlias({ index })).body;
162
- const current = Object.keys(aliases)[0];
163
-
164
- const allChange = removes.concat(fieldChanges);
165
-
166
- const reindexBody: estypes.ReindexRequest = {
167
- source: { index: current },
168
- dest: { index: next },
169
- script: {
170
- lang: 'painless',
171
- source: allChange.map(part => `ctx._source.remove("${part}");`).join(' ') // Removing
172
- },
173
- wait_for_completion: true
174
- };
175
-
176
- // Reindex
177
- await this.#client.reindex(reindexBody);
178
-
179
- await Promise.all(Object.keys(aliases)
180
- .map(alias => this.#client.indices.delete({ index: alias })));
181
-
182
- await this.#client.indices.putAlias({ index: next, name: index });
183
- } else { // Only update the schema
184
- const schema = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
185
-
186
- await this.#client.indices.putMapping({
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
  }
@@ -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 { SchemaChange, BindUtil } from '@travetto/schema';
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.registerModelChangeListener(this.manager);
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
- createModel(cls: Class): Promise<void> { return this.manager.createModel(cls); }
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> {