@travetto/model-elasticsearch 8.0.0-alpha.2 → 8.0.0-alpha.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,13 +13,13 @@ 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#L36) 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#L41) 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/types/crud.ts#L11)
20
20
  * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L64)
21
21
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
22
- * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11)
22
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
23
23
  * [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
24
24
  * [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
25
25
  * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-elasticsearch",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.20",
4
4
  "type": "module",
5
5
  "description": "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.",
6
6
  "keywords": [
@@ -28,13 +28,14 @@
28
28
  "directory": "module/model-elasticsearch"
29
29
  },
30
30
  "dependencies": {
31
- "@elastic/elasticsearch": "^9.3.4",
32
- "@travetto/config": "^8.0.0-alpha.2",
33
- "@travetto/model": "^8.0.0-alpha.2",
34
- "@travetto/model-query": "^8.0.0-alpha.2"
31
+ "@elastic/elasticsearch": "^9.4.2",
32
+ "@travetto/config": "^8.0.0-alpha.18",
33
+ "@travetto/model": "^8.0.0-alpha.18",
34
+ "@travetto/model-indexed": "^8.0.0-alpha.20",
35
+ "@travetto/model-query": "^8.0.0-alpha.19"
35
36
  },
36
37
  "peerDependencies": {
37
- "@travetto/cli": "^8.0.0-alpha.3"
38
+ "@travetto/cli": "^8.0.0-alpha.23"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "@travetto/cli": {
@@ -3,6 +3,7 @@ import type * as estypes from '@elastic/elasticsearch/api/types';
3
3
 
4
4
  import { JSONUtil, type Class } from '@travetto/runtime';
5
5
  import { ModelRegistryIndex, type ModelType, type ModelStorageSupport } from '@travetto/model';
6
+ import { warnIfIndexedUniqueIndex } from '@travetto/model-indexed';
6
7
 
7
8
  import type { ElasticsearchModelConfig } from './config.ts';
8
9
  import { ElasticsearchSchemaUtil } from './internal/schema.ts';
@@ -103,6 +104,8 @@ export class IndexManager implements ModelStorageSupport {
103
104
  const { index } = this.getIdentity(cls);
104
105
  const resolvedAlias = await this.#client.indices.getMapping({ index }).catch(() => undefined);
105
106
 
107
+ warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
108
+
106
109
  if (resolvedAlias) {
107
110
  const [currentIndex] = Object.keys(resolvedAlias ?? {});
108
111
  const pendingMapping = ElasticsearchSchemaUtil.generateSchemaMapping(cls, this.config.schemaConfig);
@@ -141,8 +144,8 @@ export class IndexManager implements ModelStorageSupport {
141
144
 
142
145
  async deleteStorage(): Promise<void> {
143
146
  console.debug('Deleting storage', { idx: this.getNamespacedIndex('*') });
144
- await this.#client.indices.delete({
145
- index: this.getNamespacedIndex('*')
146
- });
147
+ // await this.#client.indices.delete({
148
+ // index: this.getNamespacedIndex('*')
149
+ // });
147
150
  }
148
151
  }
@@ -1,9 +1,10 @@
1
1
  import type * as estypes from '@elastic/elasticsearch/api/types';
2
2
 
3
- import { castTo, type Class, TypedObject } from '@travetto/runtime';
3
+ import { type Any, castTo, type Class, TypedObject } from '@travetto/runtime';
4
4
  import { type WhereClause, type SelectClause, type SortClause, type Query, ModelQueryUtil } from '@travetto/model-query';
5
- import { type IndexConfig, type ModelType, ModelRegistryIndex } from '@travetto/model';
5
+ import { type ModelType, ModelRegistryIndex } from '@travetto/model';
6
6
  import { DataUtil, SchemaRegistryIndex } from '@travetto/schema';
7
+ import type { SortedIndex } from '@travetto/model-indexed';
7
8
 
8
9
  import { type EsSchemaConfig } from './types.ts';
9
10
 
@@ -13,7 +14,7 @@ import { type EsSchemaConfig } from './types.ts';
13
14
  export class ElasticsearchQueryUtil {
14
15
 
15
16
  /**
16
- * Convert `a.b.c` to `a : { b : { c : ... }}`
17
+ * Convert `a : { b : { c : ... }}` to `a.b.c`
17
18
  */
18
19
  static extractSimple<T>(input: T, path: string = ''): Record<string, unknown> {
19
20
  const out: Record<string, unknown> = {};
@@ -51,13 +52,19 @@ export class ElasticsearchQueryUtil {
51
52
  /**
52
53
  * Build sort mechanism
53
54
  */
54
- static getSort<T extends ModelType>(sort: SortClause<T>[] | IndexConfig<T>['fields']): estypes.Sort {
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
- });
55
+ static getSort<T extends ModelType>(sort: SortClause<T>[] | SortedIndex<T, Any, Any>): estypes.Sort {
56
+ if (Array.isArray(sort)) {
57
+ return sort.map<estypes.SortOptions>(option => {
58
+ const item = this.extractSimple(option);
59
+ const key = Object.keys(item)[0];
60
+ const value: boolean | -1 | 1 = castTo(item[key]);
61
+ return { [key]: { order: value === 1 || value === true ? 'asc' : 'desc' } };
62
+ });
63
+ } else {
64
+ return sort.sortTemplate.map<estypes.SortOptions>(field =>
65
+ ({ [field.path.join('.')]: { order: field.value === 1 ? 'asc' : 'desc' } })
66
+ );
67
+ }
61
68
  }
62
69
 
63
70
  /**
@@ -134,7 +141,13 @@ export class ElasticsearchQueryUtil {
134
141
  }
135
142
  case '$regex': {
136
143
  const pattern = DataUtil.toRegex(castTo(value));
137
- if (pattern.source.startsWith('\\b') && pattern.source.endsWith('.*')) {
144
+ if (pattern.source.startsWith('^')) { // We have a prefix query
145
+ if (/^\^[A-Za-z0-9_\-]+/.test(pattern.source)) {
146
+ items.push({ prefix: { [subPath]: pattern.source.substring(1) } });
147
+ } else {
148
+ items.push({ regexp: { [subPath]: pattern.source.substring(1) } });
149
+ }
150
+ } else if (pattern.source.startsWith('\\b') && pattern.source.endsWith('.*') && declaredSchema.specifiers?.includes('text')) {
138
151
  const textField = !pattern.flags.includes('i') && config && config.caseSensitive ?
139
152
  `${subPath}.text_cs` :
140
153
  `${subPath}.text`;
@@ -191,7 +204,7 @@ export class ElasticsearchQueryUtil {
191
204
  if (ModelQueryUtil.has$And(clause)) {
192
205
  return { bool: { must: clause.$and.map(item => this.extractWhereQuery<T>(cls, item, config)) } };
193
206
  } else if (ModelQueryUtil.has$Or(clause)) {
194
- return { bool: { should: clause.$or.map(item => this.extractWhereQuery<T>(cls, item, config)), ['minimum_should_match']: 1 } };
207
+ return { bool: { should: clause.$or.map(item => this.extractWhereQuery<T>(cls, item, config)), minimum_should_match: 1 } };
195
208
  } else if (ModelQueryUtil.has$Not(clause)) {
196
209
  return { bool: { ['must_not']: this.extractWhereQuery<T>(cls, clause.$not, config) } };
197
210
  } else {
@@ -7,8 +7,8 @@ import type { 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;
10
+ const isMappingType = (input: estypes.MappingProperty): input is estypes.MappingTypeMapping & estypes.MappingProperty =>
11
+ !!input && !!input.type && (input.type === 'object' || input.type === 'nested');
12
12
 
13
13
  /**
14
14
  * Utils for ES Schema management
@@ -163,14 +163,20 @@ export class ElasticsearchSchemaUtil {
163
163
  const currentProperty = currentProperties[key];
164
164
  const neededProperty = neededProperties[key];
165
165
 
166
- if (!currentProperty || !neededProperty || currentProperty.type !== neededProperty.type) {
167
- changed.push(path);
166
+ if (isMappingType(currentProperty) && isMappingType(neededProperty)) {
167
+ if (currentProperty.type !== neededProperty.type) {
168
+ changed.push(path);
169
+ } else {
170
+ changed.push(...this.getChangedFields(
171
+ 'properties' in currentProperty ? currentProperty : { properties: {} },
172
+ 'properties' in neededProperty ? neededProperty : { properties: {} },
173
+ path
174
+ ));
175
+ }
168
176
  } else if (isMappingType(currentProperty) || isMappingType(neededProperty)) {
169
- changed.push(...this.getChangedFields(
170
- 'properties' in currentProperty ? currentProperty : { properties: {} },
171
- 'properties' in neededProperty ? neededProperty : { properties: {} },
172
- path
173
- ));
177
+ changed.push(path);
178
+ } else if (!currentProperty || !neededProperty || currentProperty.type !== neededProperty.type) {
179
+ changed.push(path);
174
180
  }
175
181
  }
176
182
  return changed;
package/src/service.ts CHANGED
@@ -3,19 +3,24 @@ import type * as estypes from '@elastic/elasticsearch/api/types';
3
3
 
4
4
  import {
5
5
  type ModelCrudSupport, type BulkOperation, type BulkResponse, type ModelBulkSupport, type ModelExpirySupport,
6
- type ModelIndexedSupport, type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
7
- ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
6
+ type ModelType, type ModelStorageSupport, NotFoundError, ModelRegistryIndex, type OptionalId,
7
+ ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
8
+ type ModelListOptions
8
9
  } from '@travetto/model';
9
- import { ShutdownManager, type DeepPartial, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
10
+ import { ShutdownManager, type Class, castTo, asFull, TypedObject, asConstructable, JSONUtil } from '@travetto/runtime';
10
11
  import { BindUtil } from '@travetto/schema';
11
12
  import { Injectable, PostConstruct } from '@travetto/di';
12
13
  import {
13
- type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport,
14
- type ModelQuerySupport, type PageableModelQuery, type Query, type ValidStringFields,
15
- QueryVerifier, type ModelQuerySuggestSupport,
16
- ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil,
17
- type ModelQueryFacet,
14
+ type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport, type PageableModelQuery,
15
+ type Query, type ValidStringFields, QueryVerifier, type ModelQuerySuggestSupport, ModelQueryUtil, ModelQuerySuggestUtil,
16
+ ModelQueryCrudUtil, type ModelQueryFacet,
17
+ type WhereClause,
18
18
  } from '@travetto/model-query';
19
+ import {
20
+ type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
21
+ type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, type FullKeyedIndexBody,
22
+ type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType
23
+ } from '@travetto/model-indexed';
19
24
 
20
25
  import type { ElasticsearchModelConfig } from './config.ts';
21
26
  import type { EsBulkError } from './internal/types.ts';
@@ -48,6 +53,101 @@ export class ElasticsearchModelService implements
48
53
 
49
54
  constructor(config: ElasticsearchModelConfig) { this.config = config; }
50
55
 
56
+ /**
57
+ * Convert _id to id
58
+ */
59
+ async #postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
60
+ let item = {
61
+ ...(input._id ? { id: input._id } : {}),
62
+ ...input._source!,
63
+ };
64
+
65
+ item = await ModelCrudUtil.load(cls, item);
66
+
67
+ const { expiresAt } = ModelRegistryIndex.getConfig(cls);
68
+
69
+ if (expiresAt) {
70
+ const expiry = ModelExpiryUtil.getExpiryState(cls, item);
71
+ if (!expiry.expired) {
72
+ return item;
73
+ }
74
+ throw new NotFoundError(cls, item.id);
75
+ } else {
76
+ return item;
77
+ }
78
+ }
79
+
80
+ async * #scrollCollection<T extends ModelType>(
81
+ cls: Class<T>,
82
+ buildSearch: (offset?: estypes.SortResults) => estypes.SearchRequest,
83
+ options?: ModelPageOptions<estypes.SortResults> & ModelListOptions
84
+ ): AsyncIterable<{ items: T[], nextOffset?: estypes.SortResults | undefined }> {
85
+ const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
86
+ const batchSize = Math.min(limit, options?.batchSizeHint ?? 100);
87
+
88
+ let search: estypes.SearchResponse<T> = await this.execSearch<T>(cls, {
89
+ size: batchSize,
90
+ ...buildSearch(options?.offset),
91
+ });
92
+
93
+ let hits = search.hits.hits;
94
+ let produced = 0;
95
+
96
+ while (produced < limit && hits.length && !(options?.abort?.aborted)) {
97
+ hits = search.hits.hits;
98
+ produced += hits.length;
99
+ if (produced > limit) {
100
+ hits = hits.slice(0, limit - (produced - hits.length));
101
+ }
102
+ const items = await ModelCrudUtil.filterOutNotFound(
103
+ hits.map(hit => this.#postLoad(cls, hit))
104
+ );
105
+ const nextOffset = hits.at(-1)?.sort;
106
+ yield { items, nextOffset };
107
+ if (search._scroll_id) {
108
+ search = await this.client.scroll({ scroll_id: search._scroll_id });
109
+ } else if (nextOffset) {
110
+ search = await this.execSearch<T>(cls, {
111
+ size: batchSize,
112
+ ...buildSearch(nextOffset),
113
+ });
114
+ } else {
115
+ hits = [];
116
+ }
117
+ }
118
+ if (search._scroll_id) {
119
+ await this.client.clearScroll({ scroll_id: search._scroll_id }).catch(() => { });
120
+ }
121
+ }
122
+
123
+ async * #scrollIndex<T extends ModelType>(
124
+ cls: Class<T>,
125
+ idx: SortedIndex<T>,
126
+ body: KeyedIndexBody<T>,
127
+ options?: ModelPageOptions<estypes.SortResults> & ModelListOptions,
128
+ transformWhere?: (where: WhereClause<T>) => WhereClause<T>
129
+ ): AsyncIterable<{
130
+ items: T[];
131
+ nextOffset?: estypes.SortResults | undefined;
132
+ }> {
133
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
134
+ let whereClause: WhereClause<T> = castTo(computed.project());
135
+ if (transformWhere) {
136
+ whereClause = transformWhere(whereClause);
137
+ }
138
+
139
+ yield* this.#scrollCollection(cls, (offset) => {
140
+ const result = {
141
+ query: ElasticsearchQueryUtil.getSearchQuery(cls,
142
+ ElasticsearchQueryUtil.extractWhereQuery(cls, whereClause)
143
+ ),
144
+ ...(offset ? { search_after: offset } : {}),
145
+ sort: ElasticsearchQueryUtil.getSort(idx)
146
+ };
147
+ return result;
148
+ }, options);
149
+ }
150
+
51
151
  @PostConstruct()
52
152
  async initializeClient(this: ElasticsearchModelService): Promise<void> {
53
153
  this.client = new Client({
@@ -109,30 +209,6 @@ export class ElasticsearchModelService implements
109
209
  return item;
110
210
  }
111
211
 
112
- /**
113
- * Convert _id to id
114
- */
115
- async postLoad<T extends ModelType>(cls: Class<T>, input: estypes.SearchHit<T> | estypes.GetGetResult<T>): Promise<T> {
116
- let item = {
117
- ...(input._id ? { id: input._id } : {}),
118
- ...input._source!,
119
- };
120
-
121
- item = await ModelCrudUtil.load(cls, item);
122
-
123
- const { expiresAt } = ModelRegistryIndex.getConfig(cls);
124
-
125
- if (expiresAt) {
126
- const expiry = ModelExpiryUtil.getExpiryState(cls, item);
127
- if (!expiry.expired) {
128
- return item;
129
- }
130
- throw new NotFoundError(cls, item.id);
131
- } else {
132
- return item;
133
- }
134
- }
135
-
136
212
  createStorage(): Promise<void> { return this.manager.createStorage(); }
137
213
  deleteStorage(): Promise<void> { return this.manager.deleteStorage(); }
138
214
  upsertModel(cls: Class): Promise<void> { return this.manager.upsertModel(cls); }
@@ -143,7 +219,7 @@ export class ElasticsearchModelService implements
143
219
  async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
144
220
  try {
145
221
  const result = await this.client.get<T>({ ...this.manager.getIdentity(cls), id });
146
- return this.postLoad(cls, result);
222
+ return this.#postLoad(cls, result);
147
223
  } catch {
148
224
  throw new NotFoundError(cls, id);
149
225
  }
@@ -246,27 +322,12 @@ export class ElasticsearchModelService implements
246
322
  return this.get(cls, id);
247
323
  }
248
324
 
249
- async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
250
- let search: estypes.SearchResponse<T> = await this.execSearch<T>(cls, {
251
- scroll: '2m',
252
- size: 100,
253
- query: ElasticsearchQueryUtil.getSearchQuery(cls, {})
254
- });
255
-
256
- while (search.hits.hits.length > 0) {
257
- for (const hit of search.hits.hits) {
258
- try {
259
- yield this.postLoad(cls, hit);
260
- } catch (error) {
261
- if (!(error instanceof NotFoundError)) {
262
- throw error;
263
- }
264
- }
265
- search = await this.client.scroll({
266
- scroll_id: search._scroll_id,
267
- scroll: '2m'
268
- });
269
- }
325
+ async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
326
+ for await (const batch of this.#scrollCollection(cls, () => ({
327
+ query: ElasticsearchQueryUtil.getSearchQuery(cls, {}),
328
+ scroll: '2m'
329
+ }), options)) {
330
+ yield batch.items;
270
331
  }
271
332
  }
272
333
 
@@ -354,67 +415,129 @@ export class ElasticsearchModelService implements
354
415
  }
355
416
 
356
417
  // Indexed
357
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
358
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
418
+ async getByIndex<
419
+ T extends ModelType,
420
+ K extends KeyedIndexSelection<T>,
421
+ S extends SortedIndexSelection<T>
422
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
423
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
424
+ const projected = computed.project({
425
+ sort: true,
426
+ includeId: true,
427
+ emptyValue: { $exists: true }
428
+ });
429
+
359
430
  const result = await this.execSearch<T>(cls, {
360
431
  query: ElasticsearchQueryUtil.getSearchQuery(cls,
361
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
362
- ModelIndexedUtil.projectIndex(cls, idx, body))
432
+ ElasticsearchQueryUtil.extractWhereTermQuery(cls, projected)
363
433
  )
364
434
  });
365
435
  if (!result.hits.hits.length) {
366
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
436
+ throw new NotFoundError(`${cls.name}: ${idx.name}`, computed.getKey({ sort: true }));
367
437
  }
368
- return this.postLoad(cls, result.hits.hits[0]);
438
+ return this.#postLoad(cls, result.hits.hits[0]);
439
+
369
440
  }
370
441
 
371
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
372
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
442
+ async deleteByIndex<
443
+ T extends ModelType,
444
+ K extends KeyedIndexSelection<T>,
445
+ S extends SortedIndexSelection<T>
446
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
447
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
373
448
  const result = await this.client.deleteByQuery({
374
449
  index: this.manager.getIdentity(cls).index,
375
450
  query: ElasticsearchQueryUtil.getSearchQuery(cls,
376
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
377
- ModelIndexedUtil.projectIndex(cls, idx, body))
451
+ ElasticsearchQueryUtil.extractWhereTermQuery(cls, computed.project({
452
+ sort: true,
453
+ includeId: true,
454
+ emptyValue: { $exists: true }
455
+ }))
378
456
  ),
379
457
  refresh: true
380
458
  });
381
- if (result.deleted) {
382
- return;
459
+ if (!result.deleted) {
460
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
383
461
  }
384
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
385
462
  }
386
463
 
387
- async upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
464
+ upsertByIndex<
465
+ T extends ModelType,
466
+ K extends KeyedIndexSelection<T>,
467
+ S extends SortedIndexSelection<T>
468
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
388
469
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
389
470
  }
390
471
 
391
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
392
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
393
- let search = await this.execSearch<T>(cls, {
394
- scroll: '2m',
395
- size: 100,
396
- query: ElasticsearchQueryUtil.getSearchQuery(cls,
397
- ElasticsearchQueryUtil.extractWhereTermQuery(cls,
398
- ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
399
- ),
400
- sort: ElasticsearchQueryUtil.getSort(config.fields)
401
- });
472
+ updateByIndex<
473
+ T extends ModelType,
474
+ K extends KeyedIndexSelection<T>,
475
+ S extends SortedIndexSelection<T>
476
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
477
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
478
+ }
402
479
 
403
- while (search.hits.hits.length > 0) {
404
- for (const hit of search.hits.hits) {
405
- try {
406
- yield this.postLoad(cls, hit);
407
- } catch (error) {
408
- if (!(error instanceof NotFoundError)) {
409
- throw error;
410
- }
411
- }
412
- search = await this.client.scroll({
413
- scroll_id: search._scroll_id,
414
- scroll: '2m'
415
- });
416
- }
480
+ async updatePartialByIndex<
481
+ T extends ModelType,
482
+ K extends KeyedIndexSelection<T>,
483
+ S extends SortedIndexSelection<T>
484
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
485
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
486
+ return this.update(cls, item);
487
+ }
488
+
489
+ async pageByIndex<
490
+ T extends ModelType,
491
+ K extends KeyedIndexSelection<T>,
492
+ S extends SortedIndexSelection<T>
493
+ >(
494
+ cls: Class<T>,
495
+ idx: SortedIndex<T, K, S>,
496
+ body: KeyedIndexBody<T, K>,
497
+ options?: ModelPageOptions,
498
+ ): Promise<ModelPageResult<T>> {
499
+ const offset = options?.offset ? JSONUtil.fromBase64<estypes.SortResults>(options.offset) : undefined;
500
+ const items: T[] = [];
501
+ let lastNextOffset: estypes.SortResults | undefined;
502
+ for await (const { items: fetched, nextOffset } of this.#scrollIndex(cls, idx, body, { limit: 100, ...options, offset })) {
503
+ lastNextOffset = nextOffset;
504
+ items.push(...fetched);
505
+ }
506
+ return { items, nextOffset: lastNextOffset ? JSONUtil.toBase64(lastNextOffset) : undefined };
507
+ }
508
+
509
+ async * listByIndex<
510
+ T extends ModelType,
511
+ K extends KeyedIndexSelection<T>,
512
+ S extends SortedIndexSelection<T>
513
+ >(
514
+ cls: Class<T>,
515
+ idx: SortedIndex<T, K, S>,
516
+ body: KeyedIndexBody<T, K>,
517
+ options?: ModelListOptions
518
+ ): AsyncIterable<T[]> {
519
+ for await (const { items } of this.#scrollIndex(cls, idx, body, options)) {
520
+ yield items;
521
+ }
522
+ }
523
+
524
+ async suggestByIndex<
525
+ T extends ModelType,
526
+ S extends SortedIndexSelection<T>,
527
+ K extends KeyedIndexSelection<T>,
528
+ B extends SortedIndexSelectionType<T, S> & string
529
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
530
+ const items: T[] = [];
531
+ for await (const { items: fetched } of this.#scrollIndex(cls, idx, body, { limit: 10, ...options },
532
+ where => castTo({
533
+ $and: [where, {
534
+ [idx.sortTemplate[0].path.join('.')]: { $regex: ModelIndexedUtil.getSuggestRegex(prefix) }
535
+ }]
536
+ })
537
+ )) {
538
+ items.push(...fetched);
417
539
  }
540
+ return items;
418
541
  }
419
542
 
420
543
  // Query
@@ -424,7 +547,7 @@ export class ElasticsearchModelService implements
424
547
  const search = ElasticsearchQueryUtil.getSearchObject(cls, query, this.config.schemaConfig);
425
548
  const results = await this.execSearch(cls, search);
426
549
  const shouldRemoveIds = query.select && 'id' in query.select && !query.select.id;
427
- return Promise.all(results.hits.hits.map(hit => this.postLoad(cls, hit).then(item => {
550
+ return Promise.all(results.hits.hits.map(hit => this.#postLoad(cls, hit).then(item => {
428
551
  if (shouldRemoveIds) {
429
552
  delete castTo<OptionalId<T>>(item).id;
430
553
  }
@@ -520,17 +643,17 @@ export class ElasticsearchModelService implements
520
643
  }
521
644
 
522
645
  // Query Facet
523
- async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
646
+ async suggestByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
524
647
  await QueryVerifier.verify(cls, query);
525
648
 
526
649
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
527
650
  const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
528
651
  const result = await this.execSearch(cls, search);
529
- const all = await Promise.all(result.hits.hits.map(hit => this.postLoad(cls, hit)));
652
+ const all = await Promise.all(result.hits.hits.map(hit => this.#postLoad(cls, hit)));
530
653
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, (_, value) => value, query && query.limit);
531
654
  }
532
655
 
533
- async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
656
+ async suggestValuesByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
534
657
  await QueryVerifier.verify(cls, query);
535
658
 
536
659
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, {
@@ -539,12 +662,12 @@ export class ElasticsearchModelService implements
539
662
  });
540
663
  const search = ElasticsearchQueryUtil.getSearchObject(cls, resolvedQuery);
541
664
  const result = await this.execSearch(cls, search);
542
- const all = result.hits.hits.map(hit => castTo<T>(({ [field]: field === 'id' ? hit._id : hit._source![field] })));
665
+ const all = result.hits.hits.map(hit => castTo<T>((field === 'id' ? { id: hit._id } : hit._source)));
543
666
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, all, item => item, query && query.limit);
544
667
  }
545
668
 
546
669
  // Facet
547
- async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
670
+ async facetByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
548
671
  await QueryVerifier.verify(cls, query);
549
672
 
550
673
  const resolvedSearch = ElasticsearchQueryUtil.getSearchObject(cls, query ?? {}, this.config.schemaConfig);
@@ -1,6 +1,6 @@
1
1
  import type { ServiceDescriptor } from '@travetto/cli';
2
2
 
3
- const version = '9.2.4';
3
+ const version = '9.2.8';
4
4
 
5
5
  const port = 9200;
6
6