@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 CHANGED
@@ -36,8 +36,8 @@ export class Init {
36
36
  @InjectableFactory({
37
37
  primary: true
38
38
  })
39
- static getModelSource(conf: ElasticsearchModelConfig) {
40
- return new ElasticsearchModelService(conf);
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
- * 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
  */
@@ -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(x => x.includes(':') ? x : `${x}:${this.port}`)
103
- .map(x => x.startsWith('http') ? x : `http://${x}`);
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.1",
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.1",
32
- "@travetto/config": "^7.0.0-rc.1",
33
- "@travetto/model": "^7.0.0-rc.1",
34
- "@travetto/model-query": "^7.0.0-rc.1"
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
- * 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
  */
@@ -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(x => x.includes(':') ? x : `${x}:${this.port}`)
63
- .map(x => x.startsWith('http') ? x : `http://${x}`);
62
+ .map(host => host.includes(':') ? host : `${host}:${this.port}`)
63
+ .map(host => host.startsWith('http') ? host : `http://${host}`);
64
64
  }
65
65
  }
@@ -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
@@ -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 ident = this.getIdentity(cls); // Already namespaced
78
- const concreteIndex = `${ident.index}_${Date.now()}`;
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: { [ident.index]: {} } } : {})
65
+ ...(alias ? { aliases: { [index]: {} } } : {})
85
66
  });
86
- console.debug('Index created', { index: ident.index, concrete: concreteIndex });
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 (err) {
92
- console.warn('Index already created', { index: ident.index, error: err });
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 ident = this.getIdentity(cls); // Already namespaced
118
- return `curl -XPOST $ES_HOST/${ident.index} -d '${JSON.stringify({
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 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[]>((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
- // 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 curr = Object.keys(aliases)[0];
163
-
164
- const allChange = removes.concat(fieldChanges);
165
-
166
- const reindexBody: estypes.ReindexRequest = {
167
- source: { index: curr },
168
- dest: { index: next },
169
- script: {
170
- lang: 'painless',
171
- source: allChange.map(x => `ctx._source.remove("${x}");`).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(x => this.#client.indices.delete({ index: x })));
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
  }
@@ -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>(o: T, path: string = ''): Record<string, unknown> {
18
+ static extractSimple<T>(input: T, path: string = ''): Record<string, unknown> {
19
19
  const out: Record<string, unknown> = {};
20
- const keys = TypedObject.keys(o);
20
+ const keys = TypedObject.keys(input);
21
21
  for (const key of keys) {
22
22
  const subPath = `${path}${key}`;
23
- if (DataUtil.isPlainObject(o[key]) && !Object.keys(o[key])[0].startsWith('$')) {
24
- Object.assign(out, this.extractSimple(o[key], `${subPath}.`));
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] = o[key];
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 k of Object.keys(simp)) {
40
- const nk = k === 'id' ? '_id' : k;
41
- const v: 1 | 0 | boolean = castTo(simp[k]);
42
- if (v === 0 || v === false) {
43
- exclude.push(nk);
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(nk);
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>(x => {
56
- const o = this.extractSimple(x);
57
- const k = Object.keys(o)[0];
58
- const v: boolean | -1 | 1 = castTo(o[k]);
59
- return { [k]: { order: v === 1 || v === true ? 'asc' : 'desc' } };
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>, o: Record<string, unknown>, config?: EsSchemaConfig, path: string = ''): Record<string, unknown> {
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 key of TypedObject.keys(o)) {
71
- const top = o[key];
72
- const declaredSchema = fields[key];
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 sPath = declaredType === String ?
75
- ((key === 'id' && !path) ? '_id' : `${path}${key}`) :
76
- `${path}${key}`;
74
+ const subPath = declaredType === String ?
75
+ ((property === 'id' && !path) ? '_id' : `${path}${property}`) :
76
+ `${path}${property}`;
77
77
 
78
- const sPathQuery = (val: unknown): {} => (key === 'id' && !path) ?
79
- { ids: { values: Array.isArray(val) ? val : [val] } } :
80
- { [Array.isArray(val) ? 'terms' : 'term']: { [sPath]: val } };
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, `${sPath}.`);
85
+ const inner = this.extractWhereTermQuery(declaredType, top, config, `${subPath}.`);
86
86
  items.push(declaredSchema.array ?
87
- { nested: { path: sPath, query: inner } } :
87
+ { nested: { path: subPath, query: inner } } :
88
88
  inner
89
89
  );
90
90
  } else {
91
- const v = top[subKey];
91
+ const value = top[subKey];
92
92
 
93
93
  switch (subKey) {
94
94
  case '$all': {
95
- const arr = Array.isArray(v) ? v : [v];
95
+ const values = Array.isArray(value) ? value : [value];
96
96
  items.push({
97
97
  bool: {
98
- must: arr.map(x => ({ term: { [sPath]: x } }))
98
+ must: values.map(term => ({ term: { [subPath]: term } }))
99
99
  }
100
100
  });
101
101
  break;
102
102
  }
103
103
  case '$in': {
104
- items.push(sPathQuery(Array.isArray(v) ? v : [v]));
104
+ items.push(subPathQuery(Array.isArray(value) ? value : [value]));
105
105
  break;
106
106
  }
107
107
  case '$nin': {
108
- items.push({ bool: { ['must_not']: [sPathQuery(Array.isArray(v) ? v : [v])] } });
108
+ items.push({ bool: { ['must_not']: [subPathQuery(Array.isArray(value) ? value : [value])] } });
109
109
  break;
110
110
  }
111
111
  case '$eq': {
112
- items.push(sPathQuery(v));
112
+ items.push(subPathQuery(value));
113
113
  break;
114
114
  }
115
115
  case '$ne': {
116
- items.push({ bool: { ['must_not']: [sPathQuery(v)] } });
116
+ items.push({ bool: { ['must_not']: [subPathQuery(value)] } });
117
117
  break;
118
118
  }
119
119
  case '$exists': {
120
- const q = { exists: { field: sPath } };
121
- items.push(v ? q : { bool: { ['must_not']: q } });
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 k of Object.keys(top)) {
130
- out[k.replace(/^[$]/, '')] = ModelQueryUtil.resolveComparator(top[k]);
129
+ for (const key of Object.keys(top)) {
130
+ out[key.replace(/^[$]/, '')] = ModelQueryUtil.resolveComparator(top[key]);
131
131
  }
132
- items.push({ range: { [sPath]: out } });
132
+ items.push({ range: { [subPath]: out } });
133
133
  break;
134
134
  }
135
135
  case '$regex': {
136
- const pattern = DataUtil.toRegex(castTo(v));
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
- `${sPath}.text_cs` :
140
- `${sPath}.text`;
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: { [sPath]: pattern.source } });
148
+ items.push({ regexp: { [subPath]: pattern.source } });
149
149
  }
150
150
  break;
151
151
  }
152
152
  case '$geoWithin': {
153
- items.push({ ['geo_polygon']: { [sPath]: { points: v } } });
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
- [sPath]: top.$near
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(sPathQuery(top));
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>, o: WhereClause<T>, config?: EsSchemaConfig): Record<string, unknown> {
191
- if (ModelQueryUtil.has$And(o)) {
192
- return { bool: { must: o.$and.map(x => this.extractWhereQuery<T>(cls, x, config)) } };
193
- } else if (ModelQueryUtil.has$Or(o)) {
194
- return { bool: { should: o.$or.map(x => this.extractWhereQuery<T>(cls, x, config)), ['minimum_should_match']: 1 } };
195
- } else if (ModelQueryUtil.has$Not(o)) {
196
- return { bool: { ['must_not']: this.extractWhereQuery<T>(cls, o.$not, config) } };
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, o, config);
198
+ return this.extractWhereTermQuery(cls, clause, config);
199
199
  }
200
200
  }
201
201
 
@@ -5,7 +5,10 @@ import { Point, DataUtil, SchemaRegistryIndex } from '@travetto/schema';
5
5
 
6
6
  import { EsSchemaConfig } from './types.ts';
7
7
 
8
- const PointImpl = toConcrete<Point>();
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(o: Record<string, unknown>): estypes.Script {
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: o },
38
+ params: { body: item },
36
39
  };
37
40
  return out;
38
41
  }
39
42
 
40
43
  /**
41
44
  * Generate replace script
42
- * @param o
45
+ * @param item
43
46
  * @returns
44
47
  */
45
- static generateReplaceScript(o: Record<string, unknown>): estypes.Script {
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: o }
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>((acc, schemaCls) => {
68
- DataUtil.deepAssign(acc, this.generateSingleMapping(schemaCls, config));
69
- return acc;
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>, config?: EsSchemaConfig): estypes.MappingTypeMapping {
79
+ static generateSingleMapping<T>(cls: Class<T>, esSchema?: EsSchemaConfig): estypes.MappingTypeMapping {
77
80
  const fields = SchemaRegistryIndex.get(cls).getFields();
78
81
 
79
- const props: Record<string, estypes.MappingProperty> = {};
82
+ const properties: Record<string, estypes.MappingProperty> = {};
80
83
 
81
- for (const [field, conf] of Object.entries(fields)) {
82
- if (conf.type === PointImpl) {
83
- props[field] = { type: 'geo_point' };
84
- } else if (conf.type === Number) {
85
- let prop: Record<string, unknown> = { type: 'integer' };
86
- if (conf.precision) {
87
- const [digits, decimals] = conf.precision;
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
- prop = { type: 'scaled_float', ['scaling_factor']: decimals };
93
+ property = { type: 'scaled_float', ['scaling_factor']: decimals };
91
94
  } else {
92
95
  if (digits < 6 && decimals < 9) {
93
- prop = { type: 'half_float' };
96
+ property = { type: 'half_float' };
94
97
  } else if (digits > 20) {
95
- prop = { type: 'double' };
98
+ property = { type: 'double' };
96
99
  } else {
97
- prop = { type: 'float' };
100
+ property = { type: 'float' };
98
101
  }
99
102
  }
100
103
  } else if (digits) {
101
104
  if (digits <= 2) {
102
- prop = { type: 'byte' };
105
+ property = { type: 'byte' };
103
106
  } else if (digits <= 4) {
104
- prop = { type: 'short' };
107
+ property = { type: 'short' };
105
108
  } else if (digits <= 9) {
106
- prop = { type: 'integer' };
109
+ property = { type: 'integer' };
107
110
  } else {
108
- prop = { type: 'long' };
111
+ property = { type: 'long' };
109
112
  }
110
113
  }
111
114
  }
112
- props[field] = prop;
113
- } else if (conf.type === Date) {
114
- props[field] = { type: 'date', format: 'date_optional_time' };
115
- } else if (conf.type === Boolean) {
116
- props[field] = { type: 'boolean' };
117
- } else if (conf.type === String) {
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 (conf.specifiers?.includes('text')) {
122
+ if (config.specifiers?.includes('text')) {
120
123
  text = {
121
124
  fields: {
122
125
  text: { type: 'text' }
123
126
  }
124
127
  };
125
- if (config && config.caseSensitive) {
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
- props[field] = { type: 'keyword', ...text };
134
- } else if (conf.type === Object) {
135
- props[field] = { type: 'object', dynamic: true };
136
- } else if (SchemaRegistryIndex.has(conf.type)) {
137
- props[field] = {
138
- type: conf.array ? 'nested' : 'object',
139
- ...this.generateSingleMapping(conf.type, config)
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: props, dynamic: false };
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, BulkOp, BulkResponse, ModelBulkSupport, ModelExpirySupport,
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 { 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,
@@ -55,41 +55,41 @@ export class ElasticsearchModelService implements
55
55
  query
56
56
  });
57
57
  return result;
58
- } catch (err) {
59
- if (err instanceof errors.ResponseError && err.meta.body && typeof err.meta.body === 'object' && 'error' in err.meta.body) {
60
- console.error(err.meta.body.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 err;
62
+ throw error;
63
63
  }
64
64
  }
65
65
 
66
- preUpdate(o: { id: string }): string;
67
- preUpdate(o: {}): undefined;
68
- preUpdate(o: { id?: string }): string | undefined {
69
- if ('id' in o && typeof o.id === 'string') {
70
- const id = o.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 o.id;
72
+ delete item.id;
73
73
  }
74
74
  return id;
75
75
  }
76
76
  return;
77
77
  }
78
78
 
79
- postUpdate<T extends ModelType>(o: T, id?: string): T {
79
+ postUpdate<T extends ModelType>(item: T, id?: string): T {
80
80
  if (!this.config.storeId) {
81
- o.id = id!;
81
+ item.id = id!;
82
82
  }
83
- return o;
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>, inp: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
89
+ async postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
90
90
  let item = {
91
- ...(inp._id ? { id: inp._id } : {}),
92
- ...inp._source!,
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.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> {
@@ -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 (err) {
153
- if (err && err instanceof errors.ResponseError && err.body && err.body.result === 'not_found') {
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 err;
155
+ throw error;
157
156
  }
158
157
  }
159
158
 
160
- async create<T extends ModelType>(cls: Class<T>, o: OptionalId<T>): Promise<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, o, this);
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 (err) {
174
- console.error(err);
175
- throw err;
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>, o: T): Promise<T> {
178
+ async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
180
179
  ModelCrudUtil.ensureNotSubType(cls);
181
180
 
182
- o = await ModelCrudUtil.preStore(cls, o, this);
181
+ item = await ModelCrudUtil.preStore(cls, item, this);
183
182
 
184
- const id = this.preUpdate(o);
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 }>(o)
194
+ body: castTo<T & { id: never }>(item)
196
195
  });
197
196
 
198
- return this.postUpdate(o, id);
197
+ return this.postUpdate(item, id);
199
198
  }
200
199
 
201
- async upsert<T extends ModelType>(cls: Class<T>, o: OptionalId<T>): Promise<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, o, this);
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 (err) {
233
- if (err instanceof Error && /document_missing_exception/.test(err.message)) {
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 err;
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 el of search.hits.hits) {
249
+ for (const hit of search.hits.hits) {
251
250
  try {
252
- yield this.postLoad(cls, el);
253
- } catch (err) {
254
- if (!(err instanceof NotFoundError)) {
255
- throw err;
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: BulkOp<T>[]): Promise<BulkResponse<EsBulkError>> {
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
- const body = operations.reduce<(T | Partial<Record<'delete' | 'create' | 'index' | 'update', { _index: string, _id?: string }>> | { doc: T })[]>((acc, op) => {
271
-
272
- const esIdent = this.manager.getIdentity(asConstructable<T>((op.upsert ?? op.delete ?? op.insert ?? op.update ?? { constructor: cls })).constructor);
273
- const ident: { _index: string, _type?: unknown } = { _index: esIdent.index };
274
-
275
- if (op.delete) {
276
- acc.push({ delete: { ...ident, _id: op.delete.id } });
277
- } else if (op.insert) {
278
- const id = this.preUpdate(op.insert);
279
- acc.push({ create: { ...ident, _id: id } }, castTo(op.insert));
280
- } else if (op.upsert) {
281
- const id = this.preUpdate(op.upsert);
282
- acc.push({ index: { ...ident, _id: id } }, castTo(op.upsert));
283
- } else if (op.update) {
284
- const id = this.preUpdate(op.update);
285
- acc.push({ update: { ...ident, _id: id } }, { doc: op.update });
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 acc;
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 Count = keyof typeof out['counts'];
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 [k] = TypedObject.keys(item);
312
- const v = item[k]!;
313
- if (v.error) {
312
+ const [key] = TypedObject.keys(item);
313
+ const responseItem = item[key]!;
314
+ if (responseItem.error) {
314
315
  out.errors.push({
315
- reason: v.error!.reason!,
316
- type: v.error!.type
316
+ reason: responseItem.error!.reason!,
317
+ type: responseItem.error!.type
317
318
  });
318
319
  out.counts.error += 1;
319
320
  } else {
320
- let sk: Count;
321
- switch (k) {
322
- case 'create': sk = 'insert'; break;
323
- case 'index': sk = operations[i].insert ? 'insert' : 'upsert'; break;
324
- case 'delete': case 'update': sk = k; break;
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: ${k}`);
327
+ throw new Error(`Unknown response key: ${key}`);
327
328
  }
328
329
  }
329
330
 
330
- if (v.result === 'created') {
331
- out.insertedIds.set(i, v._id!);
332
- (operations[i].insert ?? operations[i].upsert)!.id = v._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[sk] += 1;
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 cfg = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
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(cfg.fields)
392
+ sort: ElasticsearchQueryUtil.getSort(config.fields)
392
393
  });
393
394
 
394
395
  while (search.hits.hits.length > 0) {
395
- for (const el of search.hits.hits) {
396
+ for (const hit of search.hits.hits) {
396
397
  try {
397
- yield this.postLoad(cls, el);
398
- } catch (err) {
399
- if (!(err instanceof NotFoundError)) {
400
- throw err;
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 req = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
416
- const results = await this.execSearch(cls, req);
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(m => this.postLoad(cls, m).then(v => {
419
+ return Promise.all(results.hits.hits.map(hit => this.postLoad(cls, hit).then(item => {
419
420
  if (shouldRemoveIds) {
420
- delete castTo<OptionalId<T>>(v).id;
421
+ delete castTo<OptionalId<T>>(item).id;
421
422
  }
422
- return v;
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 req = ElasticsearchQueryUtil.getSearchObject(cls, { ...query, limit: 0 }, this.config.schemaConfig);
435
- const result: number | { value: number } = (await this.execSearch(cls, req)).hits.total || { value: 0 };
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 (err) {
474
- if (err instanceof errors.ResponseError && 'version_conflicts' in err.body) {
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 err;
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: _, ...q } = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig, false);
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
- ...q,
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 q = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
518
- const search = ElasticsearchQueryUtil.getSearchObject(cls, q);
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(x => this.postLoad(cls, x)));
521
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (x, v) => v, query && query.limit);
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 q = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, {
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, q);
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(x => castTo<T>(({ [field]: field === 'id' ? x._id : x._source![field] }))));
534
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, x => x, query && query.limit);
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 q = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
542
+ const resolvedSearch = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
542
543
 
543
544
  const search: estypes.SearchRequest = {
544
- query: q.query ?? { ['match_all']: {} },
545
+ query: resolvedSearch.query ?? { ['match_all']: {} },
545
546
  aggs: { [field]: { terms: { field, size: 100 } } },
546
547
  size: 0
547
548
  };