@travetto/model-elasticsearch 5.0.18 → 5.0.20

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
@@ -13,7 +13,7 @@ npm install @travetto/model-elasticsearch
13
13
  yarn add @travetto/model-elasticsearch
14
14
  ```
15
15
 
16
- This module provides an [elasticsearch](https://elastic.co)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [elasticsearch](https://elastic.co). In development mode, [ElasticsearchModelService](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch/src/service.ts#L42) will also modify the [elasticsearch](https://elastic.co) schema in real time to minimize impact to development.
16
+ This module provides an [elasticsearch](https://elastic.co)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [elasticsearch](https://elastic.co). In development mode, [ElasticsearchModelService](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch/src/service.ts#L37) will also modify the [elasticsearch](https://elastic.co) schema in real time to minimize impact to development.
17
17
 
18
18
  Supported features:
19
19
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
@@ -76,6 +76,11 @@ export class ElasticsearchModelConfig {
76
76
  * Auto-create, disabled in prod by default
77
77
  */
78
78
  autoCreate?: boolean;
79
+ /**
80
+ * Should we store the id as a string in the document
81
+ */
82
+ storeId?: boolean;
83
+
79
84
  /**
80
85
  * Base schema config for elasticsearch
81
86
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-elasticsearch",
3
- "version": "5.0.18",
3
+ "version": "5.0.20",
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": "^8.17.0",
31
- "@travetto/cli": "^5.0.17",
32
- "@travetto/config": "^5.0.14",
33
- "@travetto/model": "^5.0.15",
34
- "@travetto/model-query": "^5.0.15"
31
+ "@travetto/cli": "^5.0.19",
32
+ "@travetto/config": "^5.0.16",
33
+ "@travetto/model": "^5.0.17",
34
+ "@travetto/model-query": "^5.0.17"
35
35
  },
36
36
  "travetto": {
37
37
  "displayName": "Elasticsearch Model Source"
package/src/config.ts CHANGED
@@ -29,6 +29,11 @@ export class ElasticsearchModelConfig {
29
29
  * Auto-create, disabled in prod by default
30
30
  */
31
31
  autoCreate?: boolean;
32
+ /**
33
+ * Should we store the id as a string in the document
34
+ */
35
+ storeId?: boolean;
36
+
32
37
  /**
33
38
  * Base schema config for elasticsearch
34
39
  */
@@ -1,5 +1,4 @@
1
- import { Client } from '@elastic/elasticsearch';
2
- import { ReindexRequest } from '@elastic/elasticsearch/lib/api/types';
1
+ import { Client, estypes } from '@elastic/elasticsearch';
3
2
 
4
3
  import { Class } from '@travetto/runtime';
5
4
  import { ModelRegistry, ModelType } from '@travetto/model';
@@ -164,7 +163,7 @@ export class IndexManager implements ModelStorageSupport {
164
163
 
165
164
  const allChange = removes.concat(fieldChanges);
166
165
 
167
- const reindexBody: ReindexRequest = {
166
+ const reindexBody: estypes.ReindexRequest = {
168
167
  source: { index: curr },
169
168
  dest: { index: next },
170
169
  script: {
@@ -78,6 +78,10 @@ export class ElasticsearchQueryUtil {
78
78
  ((key === 'id' && !path) ? '_id' : `${path}${key}`) :
79
79
  `${path}${key}`;
80
80
 
81
+ const sPathQuery = (val: unknown): {} => (key === 'id' && !path) ?
82
+ { ids: { values: Array.isArray(val) ? val : [val] } } :
83
+ { [Array.isArray(val) ? 'terms' : 'term']: { [sPath]: val } };
84
+
81
85
  if (DataUtil.isPlainObject(top)) {
82
86
  const subKey = Object.keys(top)[0];
83
87
  if (!subKey.startsWith('$')) {
@@ -100,29 +104,19 @@ export class ElasticsearchQueryUtil {
100
104
  break;
101
105
  }
102
106
  case '$in': {
103
- items.push({ terms: { [sPath]: Array.isArray(v) ? v : [v] } });
107
+ items.push(sPathQuery(Array.isArray(v) ? v : [v]));
104
108
  break;
105
109
  }
106
110
  case '$nin': {
107
- items.push({
108
- bool: {
109
- ['must_not']: [{
110
- terms: {
111
- [sPath]: Array.isArray(v) ? v : [v]
112
- }
113
- }]
114
- }
115
- });
111
+ items.push({ bool: { ['must_not']: [sPathQuery(Array.isArray(v) ? v : [v])] } });
116
112
  break;
117
113
  }
118
114
  case '$eq': {
119
- items.push({ term: { [sPath]: v } });
115
+ items.push(sPathQuery(v));
120
116
  break;
121
117
  }
122
118
  case '$ne': {
123
- items.push({
124
- bool: { ['must_not']: [{ term: { [sPath]: v } }] }
125
- });
119
+ items.push({ bool: { ['must_not']: [sPathQuery(v)] } });
126
120
  break;
127
121
  }
128
122
  case '$exists': {
@@ -183,11 +177,7 @@ export class ElasticsearchQueryUtil {
183
177
  }
184
178
  // Handle operations
185
179
  } else {
186
- items.push({
187
- [Array.isArray(top) ? 'terms' : 'term']: {
188
- [(key === 'id' && !path) ? '_id' : `${path}${key}`]: top
189
- }
190
- });
180
+ items.push(sPathQuery(top));
191
181
  }
192
182
  }
193
183
  if (items.length === 1) {
@@ -281,31 +271,4 @@ export class ElasticsearchQueryUtil {
281
271
 
282
272
  return search;
283
273
  }
284
-
285
-
286
- /**
287
- * Safely load the data, excluding ids if needed
288
- */
289
- static cleanIdRemoval<T>(req: estypes.SearchRequest, results: estypes.SearchResponse<T>): T[] {
290
- const out: T[] = [];
291
-
292
- const toArr = <V>(x: V | V[] | undefined): V[] => (x ? (Array.isArray(x) ? x : [x]) : []);
293
-
294
- // determine if id
295
- const select = [
296
- toArr(req._source_includes),
297
- toArr(req._source_excludes)
298
- ];
299
- const includeId = select[0].includes('_id') || (select[0].length === 0 && !select[1].includes('_id'));
300
-
301
- for (const r of results.hits.hits) {
302
- const obj = r._source!;
303
- if (includeId) {
304
- castTo<{ _id: string }>(obj)._id = r._id!;
305
- }
306
- out.push(obj);
307
- }
308
-
309
- return out;
310
- }
311
274
  }
@@ -1,4 +1,4 @@
1
- import { InlineScript, MappingProperty, MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
1
+ import { estypes } from '@elastic/elasticsearch';
2
2
 
3
3
  import { Class } from '@travetto/runtime';
4
4
  import { ModelRegistry } from '@travetto/model';
@@ -15,8 +15,8 @@ export class ElasticsearchSchemaUtil {
15
15
  /**
16
16
  * Build the update script for a given object
17
17
  */
18
- static generateUpdateScript(o: Record<string, unknown>): InlineScript {
19
- const out: InlineScript = {
18
+ static generateUpdateScript(o: Record<string, unknown>): estypes.Script {
19
+ const out: estypes.Script = {
20
20
  lang: 'painless',
21
21
  source: `
22
22
  for (entry in params.body.entrySet()) {
@@ -42,7 +42,7 @@ export class ElasticsearchSchemaUtil {
42
42
  * @param o
43
43
  * @returns
44
44
  */
45
- static generateReplaceScript(o: Record<string, unknown>): InlineScript {
45
+ static generateReplaceScript(o: Record<string, unknown>): estypes.Script {
46
46
  return {
47
47
  lang: 'painless',
48
48
  source: 'ctx._source.clear(); ctx._source.putAll(params.body)',
@@ -53,7 +53,7 @@ export class ElasticsearchSchemaUtil {
53
53
  /**
54
54
  * Build one or more mappings depending on the polymorphic state
55
55
  */
56
- static generateSchemaMapping(cls: Class, config?: EsSchemaConfig): MappingTypeMapping {
56
+ static generateSchemaMapping(cls: Class, config?: EsSchemaConfig): estypes.MappingTypeMapping {
57
57
  return ModelRegistry.get(cls).baseType ?
58
58
  this.generateAllMapping(cls, config) :
59
59
  this.generateSingleMapping(cls, config);
@@ -62,9 +62,9 @@ export class ElasticsearchSchemaUtil {
62
62
  /**
63
63
  * Generate all mappings
64
64
  */
65
- static generateAllMapping(cls: Class, config?: EsSchemaConfig): MappingTypeMapping {
65
+ static generateAllMapping(cls: Class, config?: EsSchemaConfig): estypes.MappingTypeMapping {
66
66
  const allTypes = ModelRegistry.getClassesByBaseType(cls);
67
- return allTypes.reduce<MappingTypeMapping>((acc, schemaCls) => {
67
+ return allTypes.reduce<estypes.MappingTypeMapping>((acc, schemaCls) => {
68
68
  DataUtil.deepAssign(acc, this.generateSingleMapping(schemaCls, config));
69
69
  return acc;
70
70
  }, { properties: {}, dynamic: false });
@@ -73,10 +73,10 @@ export class ElasticsearchSchemaUtil {
73
73
  /**
74
74
  * Build a mapping for a given class
75
75
  */
76
- static generateSingleMapping<T>(cls: Class<T>, config?: EsSchemaConfig): MappingTypeMapping {
76
+ static generateSingleMapping<T>(cls: Class<T>, config?: EsSchemaConfig): estypes.MappingTypeMapping {
77
77
  const schema = SchemaRegistry.getViewSchema(cls);
78
78
 
79
- const props: Record<string, MappingProperty> = {};
79
+ const props: Record<string, estypes.MappingProperty> = {};
80
80
 
81
81
  for (const field of schema.fields) {
82
82
  const conf = schema.schema[field];
package/src/service.ts CHANGED
@@ -30,10 +30,6 @@ import { ElasticsearchQueryUtil } from './internal/query';
30
30
  import { ElasticsearchSchemaUtil } from './internal/schema';
31
31
  import { IndexManager } from './index-manager';
32
32
 
33
- type WithId<T> = T & { _id?: string };
34
-
35
- const isWithId = <T extends ModelType>(o: T): o is WithId<T> => !o && '_id' in o;
36
-
37
33
  /**
38
34
  * Elasticsearch model source.
39
35
  */
@@ -74,13 +70,34 @@ export class ElasticsearchModelService implements
74
70
  }
75
71
  }
76
72
 
73
+ preUpdate(o: { id: string }): string;
74
+ preUpdate(o: {}): undefined;
75
+ preUpdate(o: { id?: string }): string | undefined {
76
+ if ('id' in o && typeof o.id === 'string') {
77
+ const id = o.id;
78
+ if (!this.config.storeId) {
79
+ delete o.id;
80
+ }
81
+ return id;
82
+ }
83
+ return;
84
+ }
85
+
86
+ postUpdate<T extends ModelType>(o: T, id?: string): T {
87
+ if (!this.config.storeId) {
88
+ o.id = id!;
89
+ }
90
+ return o;
91
+ }
92
+
77
93
  /**
78
94
  * Convert _id to id
79
95
  */
80
- async postLoad<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
81
- if (isWithId(item)) {
82
- delete item._id;
83
- }
96
+ async postLoad<T extends ModelType>(cls: Class<T>, inp: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
97
+ let item = {
98
+ ...(inp._id ? { id: inp._id } : {}),
99
+ ...inp._source!,
100
+ };
84
101
 
85
102
  item = await ModelCrudUtil.load(cls, item);
86
103
 
@@ -120,8 +137,8 @@ export class ElasticsearchModelService implements
120
137
 
121
138
  async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
122
139
  try {
123
- const res = await this.client.get({ ...this.manager.getIdentity(cls), id });
124
- return this.postLoad(cls, castTo(res._source));
140
+ const res = await this.client.get<T>({ ...this.manager.getIdentity(cls), id });
141
+ return this.postLoad(cls, res);
125
142
  } catch {
126
143
  throw new NotFoundError(cls, id);
127
144
  }
@@ -150,7 +167,7 @@ export class ElasticsearchModelService implements
150
167
  async create<T extends ModelType>(cls: Class<T>, o: OptionalId<T>): Promise<T> {
151
168
  try {
152
169
  const clean = await ModelCrudUtil.preStore(cls, o, this);
153
- const id = clean.id;
170
+ const id = this.preUpdate(clean);
154
171
 
155
172
  await this.client.index({
156
173
  ...this.manager.getIdentity(cls),
@@ -159,7 +176,7 @@ export class ElasticsearchModelService implements
159
176
  body: clean
160
177
  });
161
178
 
162
- return clean;
179
+ return this.postUpdate(clean, id);
163
180
  } catch (err) {
164
181
  console.error(err);
165
182
  throw err;
@@ -171,7 +188,7 @@ export class ElasticsearchModelService implements
171
188
 
172
189
  o = await ModelCrudUtil.preStore(cls, o, this);
173
190
 
174
- const id = o.id;
191
+ const id = this.preUpdate(o);
175
192
 
176
193
  if (ModelRegistry.get(cls).expiresAt) {
177
194
  await this.get(cls, id);
@@ -185,18 +202,18 @@ export class ElasticsearchModelService implements
185
202
  body: o
186
203
  });
187
204
 
188
- o.id = id;
189
- return o;
205
+ return this.postUpdate(o, id);
190
206
  }
191
207
 
192
208
  async upsert<T extends ModelType>(cls: Class<T>, o: OptionalId<T>): Promise<T> {
193
209
  ModelCrudUtil.ensureNotSubType(cls);
194
210
 
195
211
  const item = await ModelCrudUtil.preStore(cls, o, this);
212
+ const id = this.preUpdate(item);
196
213
 
197
214
  await this.client.update({
198
215
  ...this.manager.getIdentity(cls),
199
- id: item.id,
216
+ id,
200
217
  refresh: true,
201
218
  body: {
202
219
  doc: item,
@@ -204,18 +221,15 @@ export class ElasticsearchModelService implements
204
221
  }
205
222
  });
206
223
 
207
- return item;
224
+ return this.postUpdate(item, id);
208
225
  }
209
226
 
210
227
  async updatePartial<T extends ModelType>(cls: Class<T>, data: Partial<T> & { id: string }, view?: string): Promise<T> {
211
228
  ModelCrudUtil.ensureNotSubType(cls);
212
229
 
213
- const item = await ModelCrudUtil.prePartialUpdate(cls, data, view);
214
-
230
+ const id = data.id;
231
+ const item = castTo<typeof data>(await ModelCrudUtil.prePartialUpdate(cls, data, view));
215
232
  const script = ElasticsearchSchemaUtil.generateUpdateScript(item);
216
- const id = item.id!;
217
-
218
- console.debug('Partial Script', { script });
219
233
 
220
234
  try {
221
235
  await this.client.update({
@@ -246,7 +260,7 @@ export class ElasticsearchModelService implements
246
260
  while (search.hits.hits.length > 0) {
247
261
  for (const el of search.hits.hits) {
248
262
  try {
249
- yield this.postLoad(cls, el._source!);
263
+ yield this.postLoad(cls, el);
250
264
  } catch (err) {
251
265
  if (!(err instanceof NotFoundError)) {
252
266
  throw err;
@@ -272,11 +286,14 @@ export class ElasticsearchModelService implements
272
286
  if (op.delete) {
273
287
  acc.push({ delete: { ...ident, _id: op.delete.id } });
274
288
  } else if (op.insert) {
275
- acc.push({ create: { ...ident, _id: op.insert.id } }, castTo(op.insert));
289
+ const id = this.preUpdate(op.insert);
290
+ acc.push({ create: { ...ident, _id: id } }, castTo(op.insert));
276
291
  } else if (op.upsert) {
277
- acc.push({ index: { ...ident, _id: op.upsert.id } }, castTo(op.upsert));
292
+ const id = this.preUpdate(op.upsert);
293
+ acc.push({ index: { ...ident, _id: id } }, castTo(op.upsert));
278
294
  } else if (op.update) {
279
- acc.push({ update: { ...ident, _id: op.update.id } }, { doc: op.update });
295
+ const id = this.preUpdate(op.update);
296
+ acc.push({ update: { ...ident, _id: id } }, { doc: op.update });
280
297
  }
281
298
  return acc;
282
299
  }, []);
@@ -350,7 +367,7 @@ export class ElasticsearchModelService implements
350
367
  if (!res.hits.hits.length) {
351
368
  throw new NotFoundError(`${cls.name}: ${idx}`, key);
352
369
  }
353
- return this.postLoad(cls, res.hits.hits[0]._source!);
370
+ return this.postLoad(cls, res.hits.hits[0]);
354
371
  }
355
372
 
356
373
  async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
@@ -388,7 +405,7 @@ export class ElasticsearchModelService implements
388
405
  while (search.hits.hits.length > 0) {
389
406
  for (const el of search.hits.hits) {
390
407
  try {
391
- yield this.postLoad(cls, el._source!);
408
+ yield this.postLoad(cls, el);
392
409
  } catch (err) {
393
410
  if (!(err instanceof NotFoundError)) {
394
411
  throw err;
@@ -408,8 +425,13 @@ export class ElasticsearchModelService implements
408
425
 
409
426
  const req = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
410
427
  const results = await this.execSearch(cls, req);
411
- const items = ElasticsearchQueryUtil.cleanIdRemoval(req, results);
412
- return Promise.all(items.map(m => this.postLoad(cls, m)));
428
+ const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
429
+ return Promise.all(results.hits.hits.map(m => this.postLoad(cls, m).then(v => {
430
+ if (shouldRemoveIds) {
431
+ delete castTo<OptionalId<T>>(v).id;
432
+ }
433
+ return v;
434
+ })));
413
435
  }
414
436
 
415
437
  async queryOne<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, failOnMany: boolean = true): Promise<T> {
@@ -431,10 +453,12 @@ export class ElasticsearchModelService implements
431
453
  await QueryVerifier.verify(cls, query);
432
454
 
433
455
  const item = await ModelCrudUtil.preStore(cls, data, this);
434
- const id = item.id;
456
+ const id = this.preUpdate(item);
435
457
 
436
458
  const where = ModelQueryUtil.getWhereClause(cls, query.where);
437
- where.id = item.id;
459
+ if (id) {
460
+ where.id = id;
461
+ }
438
462
  query.where = where;
439
463
 
440
464
  if (ModelRegistry.get(cls).expiresAt) {
@@ -465,7 +489,7 @@ export class ElasticsearchModelService implements
465
489
  }
466
490
  }
467
491
 
468
- return item;
492
+ return this.postUpdate(item, id);
469
493
  }
470
494
 
471
495
  async deleteByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T> = {}): Promise<number> {
@@ -484,7 +508,6 @@ export class ElasticsearchModelService implements
484
508
  await QueryVerifier.verify(cls, query);
485
509
 
486
510
  const item = await ModelCrudUtil.prePartialUpdate(cls, data);
487
-
488
511
  const script = ElasticsearchSchemaUtil.generateUpdateScript(item);
489
512
 
490
513
  const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
@@ -505,9 +528,8 @@ export class ElasticsearchModelService implements
505
528
  const q = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
506
529
  const search = ElasticsearchQueryUtil.getSearchObject(cls, q);
507
530
  const res = await this.execSearch(cls, search);
508
- const safe = ElasticsearchQueryUtil.cleanIdRemoval<T>(search, res);
509
- const combined = ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, safe, (x, v) => v, query && query.limit);
510
- return Promise.all(combined.map(m => this.postLoad(cls, m)));
531
+ const all = await Promise.all(res.hits.hits.map(x => this.postLoad(cls, x)));
532
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (x, v) => v, query && query.limit);
511
533
  }
512
534
 
513
535
  async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
@@ -519,8 +541,8 @@ export class ElasticsearchModelService implements
519
541
  });
520
542
  const search = ElasticsearchQueryUtil.getSearchObject(cls, q);
521
543
  const res = await this.execSearch(cls, search);
522
- const safe = ElasticsearchQueryUtil.cleanIdRemoval(search, res);
523
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, safe, x => x, query && query.limit);
544
+ const all = await Promise.all(res.hits.hits.map(x => castTo<T>(({ [field]: field === 'id' ? x._id : x._source![field] }))));
545
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, x => x, query && query.limit);
524
546
  }
525
547
 
526
548
  // Facet