@travetto/model-mongo 5.0.0-rc.9 → 5.0.1

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
@@ -17,10 +17,10 @@ This module provides an [mongodb](https://mongodb.com)-based implementation for
17
17
 
18
18
  Supported features:
19
19
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
20
- * [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3)
21
20
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11)
22
21
  * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L19)
23
22
  * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/service/indexed.ts#L12)
23
+ * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8)
24
24
  * [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11)
25
25
  * [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/facet.ts#L12)
26
26
  * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/query.ts#L10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "5.0.0-rc.9",
3
+ "version": "5.0.1",
4
4
  "description": "Mongo backing for the travetto model module.",
5
5
  "keywords": [
6
6
  "mongo",
@@ -25,13 +25,13 @@
25
25
  "directory": "module/model-mongo"
26
26
  },
27
27
  "dependencies": {
28
- "@travetto/config": "^5.0.0-rc.9",
29
- "@travetto/model": "^5.0.0-rc.9",
30
- "@travetto/model-query": "^5.0.0-rc.9",
28
+ "@travetto/config": "^5.0.1",
29
+ "@travetto/model": "^5.0.1",
30
+ "@travetto/model-query": "^5.0.1",
31
31
  "mongodb": "^6.8.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0-rc.9"
34
+ "@travetto/command": "^5.0.1"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/command": {
@@ -1,11 +1,14 @@
1
- import { Binary, ObjectId } from 'mongodb';
1
+ import { Binary, CreateIndexesOptions, FindCursor, IndexDirection, ObjectId } from 'mongodb';
2
2
 
3
3
  import { castTo, Class, TypedObject } from '@travetto/runtime';
4
- import { DistanceUnit, WhereClause } from '@travetto/model-query';
5
- import type { ModelType, IndexField } from '@travetto/model';
4
+ import { DistanceUnit, PageableModelQuery, WhereClause } from '@travetto/model-query';
5
+ import type { ModelType, IndexField, IndexConfig } from '@travetto/model';
6
6
  import { DataUtil, SchemaRegistry } from '@travetto/schema';
7
7
  import { ModelQueryUtil } from '@travetto/model-query/src/internal/service/query';
8
8
  import { AllViewⲐ } from '@travetto/schema/src/internal/types';
9
+ import { PointImpl } from '@travetto/model-query/src/internal/model/point';
10
+
11
+ type IdxCfg = CreateIndexesOptions;
9
12
 
10
13
  /**
11
14
  * Converting units to various radians
@@ -21,12 +24,15 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
21
24
  export type WithId<T, I = unknown> = T & { _id?: I };
22
25
  const isWithId = <T extends ModelType, I = unknown>(o: T): o is WithId<T, I> => o && '_id' in o;
23
26
 
27
+ export type BasicIdx = Record<string, IndexDirection>;
28
+ export type PlainIdx = Record<string, -1 | 0 | 1>;
29
+
24
30
  /**
25
31
  * Basic mongo utils for conforming to the model module
26
32
  */
27
33
  export class MongoUtil {
28
34
 
29
- static toIndex<T extends ModelType>(f: IndexField<T>): Record<string, number> {
35
+ static toIndex<T extends ModelType>(f: IndexField<T>): PlainIdx {
30
36
  const keys = [];
31
37
  while (typeof f !== 'number' && typeof f !== 'boolean' && Object.keys(f)) {
32
38
  const key = TypedObject.keys(f)[0];
@@ -35,7 +41,7 @@ export class MongoUtil {
35
41
  }
36
42
  const rf: number | boolean = castTo(f);
37
43
  return {
38
- [keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : rf
44
+ [keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : castTo<-1 | 1 | 0>(rf)
39
45
  };
40
46
  }
41
47
 
@@ -153,4 +159,68 @@ export class MongoUtil {
153
159
  }
154
160
  return out;
155
161
  }
162
+
163
+ static getExtraIndices<T extends ModelType>(cls: Class<T>): BasicIdx[] {
164
+ const out: BasicIdx[] = [];
165
+ const textFields: string[] = [];
166
+ SchemaRegistry.visitFields(cls, (field, path) => {
167
+ if (field.type === PointImpl) {
168
+ const name = [...path, field].map(x => x.name).join('.');
169
+ out.push({ [name]: '2d' });
170
+ } else if (field.specifiers?.includes('text') && (field.specifiers?.includes('long') || field.specifiers.includes('search'))) {
171
+ const name = [...path, field].map(x => x.name).join('.');
172
+ textFields.push(name);
173
+ }
174
+ });
175
+ if (textFields.length) {
176
+ const text: BasicIdx = {};
177
+ for (const field of textFields) {
178
+ text[field] = 'text';
179
+ }
180
+ out.push(text);
181
+ }
182
+ return out;
183
+ }
184
+
185
+ static getPlainIndex(idx: IndexConfig<ModelType>): PlainIdx {
186
+ let out: PlainIdx = {};
187
+ for (const cfg of idx.fields.map(x => this.toIndex(x))) {
188
+ out = Object.assign(out, cfg);
189
+ }
190
+ return out;
191
+ }
192
+
193
+ static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx, IdxCfg][] {
194
+ return [
195
+ ...indices.map(idx => [this.getPlainIndex(idx), (idx.type === 'unique' ? { unique: true } : {})] as const),
196
+ ...this.getExtraIndices(cls).map((x) => [x, {}] as const)
197
+ ].map(x => [...x]);
198
+ }
199
+
200
+ static prepareCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor<T>, query: PageableModelQuery<T>): FindCursor<T> {
201
+ if (query.select) {
202
+ const selectKey = Object.keys(query.select)[0];
203
+ const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
204
+ // Remove id if not explicitly defined, and selecting fields directly
205
+ if (!select['_id']) {
206
+ const values = new Set([...Object.values(select)]);
207
+ if (values.has(1) || values.has(true)) {
208
+ select['_id'] = false;
209
+ }
210
+ }
211
+ cursor.project(select);
212
+ }
213
+
214
+ if (query.sort) {
215
+ cursor = cursor.sort(Object.assign({}, ...query.sort.map(x => this.extractSimple(cls, x))));
216
+ }
217
+
218
+ cursor = cursor.limit(Math.trunc(query.limit ?? 200));
219
+
220
+ if (query.offset && typeof query.offset === 'number') {
221
+ cursor = cursor.skip(Math.trunc(query.offset ?? 0));
222
+ }
223
+
224
+ return cursor;
225
+ }
156
226
  }
package/src/service.ts CHANGED
@@ -1,19 +1,11 @@
1
- // Wildcard import needed here due to packaging issues
2
- import {
3
- type Db, GridFSBucket, MongoClient, type Sort, type CreateIndexesOptions,
4
- type GridFSFile, type IndexSpecification, type Collection, ObjectId,
5
- Binary
6
- } from 'mongodb';
7
- import { Readable } from 'node:stream';
8
1
  import { pipeline } from 'node:stream/promises';
9
2
 
3
+ import { type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection, type ObjectId, type Binary, type RootFilterOperators } from 'mongodb';
4
+
10
5
  import {
11
- ModelRegistry, ModelType, OptionalId,
12
- ModelCrudSupport, ModelStorageSupport, ModelStreamSupport,
13
- ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport,
14
- StreamMeta, BulkOp, BulkResponse,
15
- NotFoundError, ExistsError, IndexConfig,
16
- StreamRange
6
+ ModelRegistry, ModelType, OptionalId, ModelCrudSupport, ModelStorageSupport,
7
+ ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport, BulkOp, BulkResponse,
8
+ NotFoundError, ExistsError, ModelBlobSupport
17
9
  } from '@travetto/model';
18
10
  import {
19
11
  ModelQuery, ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySupport,
@@ -21,32 +13,30 @@ import {
21
13
  QueryVerifier
22
14
  } from '@travetto/model-query';
23
15
 
24
- import { ShutdownManager, type Class, type DeepPartial, AppError, TypedObject, castTo, asFull } from '@travetto/runtime';
16
+ import {
17
+ ShutdownManager, type Class, type DeepPartial, TypedObject,
18
+ castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil
19
+ } from '@travetto/runtime';
25
20
  import { Injectable } from '@travetto/di';
26
- import { FieldConfig, SchemaRegistry, SchemaValidator } from '@travetto/schema';
27
21
 
28
22
  import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
29
23
  import { ModelIndexedUtil } from '@travetto/model/src/internal/service/indexed';
30
24
  import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
31
25
  import { ModelQueryUtil } from '@travetto/model-query/src/internal/service/query';
32
26
  import { ModelQuerySuggestUtil } from '@travetto/model-query/src/internal/service/suggest';
33
- import { PointImpl } from '@travetto/model-query/src/internal/model/point';
34
27
  import { ModelQueryExpiryUtil } from '@travetto/model-query/src/internal/service/expiry';
35
28
  import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
36
- import { enforceRange, StreamModel, STREAMS } from '@travetto/model/src/internal/service/stream';
37
- import { AllViewⲐ } from '@travetto/schema/src/internal/types';
38
29
  import { ModelBulkUtil } from '@travetto/model/src/internal/service/bulk';
30
+ import { MODEL_BLOB, ModelBlobNamespace, ModelBlobUtil } from '@travetto/model/src/internal/service/blob';
39
31
 
40
- import { MongoUtil, WithId } from './internal/util';
32
+ import { MongoUtil, PlainIdx, WithId } from './internal/util';
41
33
  import { MongoModelConfig } from './config';
42
34
 
43
- const IdxFieldsⲐ = Symbol.for('@travetto/model-mongo:idx');
35
+ const ListIndexⲐ = Symbol.for('@travetto/mongo-model:list-index');
44
36
 
45
- const asFielded = (cfg: IndexConfig<ModelType>): { [IdxFieldsⲐ]: Sort } => castTo(cfg);
37
+ type BlobRaw = GridFSFile & { metadata?: BlobMeta };
46
38
 
47
- type IdxCfg = CreateIndexesOptions;
48
-
49
- type StreamRaw = GridFSFile & { metadata: StreamMeta };
39
+ type MongoTextSearch = RootFilterOperators<unknown>['$text'];
50
40
 
51
41
  /**
52
42
  * Mongo-based model source
@@ -54,7 +44,7 @@ type StreamRaw = GridFSFile & { metadata: StreamMeta };
54
44
  @Injectable()
55
45
  export class MongoModelService implements
56
46
  ModelCrudSupport, ModelStorageSupport,
57
- ModelBulkSupport, ModelStreamSupport,
47
+ ModelBulkSupport, ModelBlobSupport,
58
48
  ModelIndexedSupport, ModelQuerySupport,
59
49
  ModelQueryCrudSupport, ModelQueryFacetSupport,
60
50
  ModelQuerySuggestSupport, ModelExpirySupport {
@@ -66,11 +56,11 @@ export class MongoModelService implements
66
56
 
67
57
  constructor(public readonly config: MongoModelConfig) { }
68
58
 
69
- async #describeStreamRaw(location: string): Promise<StreamRaw> {
70
- const files: StreamRaw[] = castTo(await this.#bucket.find({ filename: location }, { limit: 1 }).toArray());
59
+ async #describeBlobRaw(location: string): Promise<BlobRaw> {
60
+ const files: BlobRaw[] = await this.#bucket.find({ filename: location }, { limit: 1 }).toArray();
71
61
 
72
62
  if (!files?.length) {
73
- throw new NotFoundError(STREAMS, location);
63
+ throw new NotFoundError(ModelBlobNamespace, location);
74
64
  }
75
65
 
76
66
  return files[0];
@@ -80,7 +70,7 @@ export class MongoModelService implements
80
70
  this.client = await MongoClient.connect(this.config.url, this.config.options);
81
71
  this.#db = this.client.db(this.config.namespace);
82
72
  this.#bucket = new GridFSBucket(this.#db, {
83
- bucketName: STREAMS,
73
+ bucketName: ModelBlobNamespace,
84
74
  writeConcern: { w: 1 }
85
75
  });
86
76
  await ModelStorageUtil.registerModelChangeListener(this);
@@ -99,45 +89,13 @@ export class MongoModelService implements
99
89
  await this.#db.dropDatabase();
100
90
  }
101
91
 
102
- getGeoIndices<T extends ModelType>(cls: Class<T>, path: FieldConfig[] = [], root = cls): IndexSpecification[] {
103
- const fields = SchemaRegistry.has(cls) ?
104
- Object.values(SchemaRegistry.get(cls).views[AllViewⲐ].schema) :
105
- [];
106
- const out: IndexSpecification[] = [];
107
- for (const field of fields) {
108
- if (SchemaRegistry.has(field.type)) {
109
- // Recurse
110
- out.push(...this.getGeoIndices(field.type, [...path, field], root));
111
- } else if (field.type === PointImpl) {
112
- const name = [...path, field].map(x => x.name).join('.');
113
- console.debug('Preparing geo-index', { cls: root.Ⲑid, name });
114
- out.push({ [name]: '2d' });
115
- }
116
- }
117
- return out;
118
- }
119
-
120
- getIndices<T extends ModelType>(cls: Class<T>): ([IndexSpecification] | [IndexSpecification, IdxCfg])[] {
121
- const indices = ModelRegistry.get(cls).indices ?? [];
122
- return [
123
- ...indices.map((idx): [IndexSpecification, IdxCfg] => {
124
- const combined = asFielded(idx)[IdxFieldsⲐ] ??= Object.assign({}, ...idx.fields.map(x => MongoUtil.toIndex(x)));
125
- return [
126
- castTo(combined),
127
- (idx.type === 'unique' ? { unique: true } : {})
128
- ];
129
- }),
130
- ...this.getGeoIndices(cls).map((x): [IndexSpecification] => [x])
131
- ];
132
- }
133
-
134
92
  async establishIndices<T extends ModelType>(cls: Class<T>): Promise<void> {
135
93
  const col = await this.getStore(cls);
136
- const creating = this.getIndices(cls);
94
+ const creating = MongoUtil.getIndices(cls, ModelRegistry.get(cls).indices);
137
95
  if (creating.length) {
138
96
  console.debug('Creating indexes', { indices: creating });
139
97
  for (const el of creating) {
140
- await col.createIndex(el[0], el[1] ?? {});
98
+ await col.createIndex(...el);
141
99
  }
142
100
  }
143
101
  }
@@ -151,10 +109,8 @@ export class MongoModelService implements
151
109
  }
152
110
 
153
111
  async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
154
- if (cls === StreamModel) {
155
- try {
156
- await this.#bucket.drop();
157
- } catch { }
112
+ if (cls === MODEL_BLOB) {
113
+ await this.#bucket.drop().catch(() => { });
158
114
  } else {
159
115
  const col = await this.getStore(cls);
160
116
  await col.deleteMany({});
@@ -226,13 +182,7 @@ export class MongoModelService implements
226
182
  async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
227
183
  const store = await this.getStore(cls);
228
184
 
229
- if (view) {
230
- await SchemaValidator.validate(cls, item, view);
231
- }
232
-
233
- item = await ModelCrudUtil.prePersist(cls, item, 'partial');
234
-
235
- let final: Record<string, unknown> = item;
185
+ let final: Record<string, unknown> = await ModelCrudUtil.prePartialUpdate(cls, item, view);
236
186
 
237
187
  const items = MongoUtil.extractSimple(cls, final, undefined, false);
238
188
  final = Object
@@ -283,37 +233,38 @@ export class MongoModelService implements
283
233
  }
284
234
  }
285
235
 
286
- // Stream
287
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
236
+ // Blob
237
+ async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
238
+ const existing = await this.describeBlob(location).then(() => true, () => false);
239
+ if (!overwrite && existing) {
240
+ return;
241
+ }
242
+ const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
288
243
  const writeStream = this.#bucket.openUploadStream(location, {
289
- contentType: meta.contentType,
290
- metadata: meta
244
+ contentType: blobMeta.contentType,
245
+ metadata: blobMeta,
291
246
  });
247
+ await pipeline(stream, writeStream);
292
248
 
293
- await pipeline(input, writeStream);
294
- }
295
-
296
- async getStream(location: string, range?: StreamRange): Promise<Readable> {
297
- const meta = await this.describeStream(location);
298
-
299
- if (range) {
300
- range = enforceRange(range, meta.size);
301
- range.end! += 1; // range is exclusive
249
+ if (existing) {
250
+ const [read] = await this.#bucket.find({ filename: location, _id: { $ne: writeStream.id } }).toArray();
251
+ await this.#bucket.delete(read._id);
302
252
  }
253
+ }
303
254
 
304
- const res = await this.#bucket.openDownloadStreamByName(location, range);
305
- if (!res) {
306
- throw new NotFoundError(STREAMS, location);
307
- }
308
- return res;
255
+ async getBlob(location: string, range?: ByteRange): Promise<Blob> {
256
+ const meta = await this.describeBlob(location);
257
+ const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
258
+ const mongoRange = final ? { start: final.start, end: final.end + 1 } : undefined;
259
+ return BinaryUtil.readableBlob(() => this.#bucket.openDownloadStreamByName(location, mongoRange), { ...meta, range: final });
309
260
  }
310
261
 
311
- async describeStream(location: string): Promise<StreamMeta> {
312
- return (await this.#describeStreamRaw(location)).metadata;
262
+ async describeBlob(location: string): Promise<BlobMeta> {
263
+ return (await this.#describeBlobRaw(location)).metadata ?? {};
313
264
  }
314
265
 
315
- async deleteStream(location: string): Promise<void> {
316
- const fileId = (await this.#describeStreamRaw(location))._id;
266
+ async deleteBlob(location: string): Promise<void> {
267
+ const fileId = (await this.#describeBlobRaw(location))._id;
317
268
  await this.#bucket.delete(fileId);
318
269
  }
319
270
 
@@ -426,18 +377,15 @@ export class MongoModelService implements
426
377
 
427
378
  async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
428
379
  const store = await this.getStore(cls);
429
- const idxCfg = ModelRegistry.getIndex(cls, idx);
430
-
431
- if (idxCfg.type === 'unique') {
432
- throw new AppError('Cannot list on unique indices', 'data');
433
- }
380
+ const idxCfg = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
434
381
 
435
382
  const where = this.getWhereFilter(
436
383
  cls,
437
384
  castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
438
385
  );
439
386
 
440
- const cursor = store.find(where, { timeout: true }).batchSize(100).sort(asFielded(idxCfg)[IdxFieldsⲐ]);
387
+ const sort = castTo<{ [ListIndexⲐ]: PlainIdx }>(idxCfg)[ListIndexⲐ] ??= MongoUtil.getPlainIndex(idxCfg);
388
+ const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
441
389
 
442
390
  for await (const el of cursor) {
443
391
  yield (await MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el)));
@@ -450,31 +398,8 @@ export class MongoModelService implements
450
398
 
451
399
  const col = await this.getStore(cls);
452
400
  const filter = MongoUtil.extractWhereFilter(cls, query.where);
453
- let cursor = col.find<T>(filter, {});
454
- if (query.select) {
455
- const selectKey = Object.keys(query.select)[0];
456
- const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : MongoUtil.extractSimple(cls, query.select);
457
- // Remove id if not explicitly defined, and selecting fields directly
458
- if (!select['_id']) {
459
- const values = new Set([...Object.values(select)]);
460
- if (values.has(1) || values.has(true)) {
461
- select['_id'] = false;
462
- }
463
- }
464
- cursor.project(select);
465
- }
466
-
467
- if (query.sort) {
468
- cursor = cursor.sort(Object.assign({}, ...query.sort.map(x => MongoUtil.extractSimple(cls, x))));
469
- }
470
-
471
- cursor = cursor.limit(Math.trunc(query.limit ?? 200));
472
-
473
- if (query.offset && typeof query.offset === 'number') {
474
- cursor = cursor.skip(Math.trunc(query.offset ?? 0));
475
- }
476
-
477
- const items = await cursor.toArray();
401
+ const cursor = col.find<T>(filter, {});
402
+ const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
478
403
  return await Promise.all(items.map(r => ModelCrudUtil.load(cls, r).then(MongoUtil.postLoadId)));
479
404
  }
480
405
 
@@ -492,7 +417,7 @@ export class MongoModelService implements
492
417
  }
493
418
 
494
419
  // Query Crud
495
- async updateOneWithQuery<T extends ModelType>(cls: Class<T>, data: T, query: ModelQuery<T>): Promise<T> {
420
+ async updateByQuery<T extends ModelType>(cls: Class<T>, data: T, query: ModelQuery<T>): Promise<T> {
496
421
  await QueryVerifier.verify(cls, query);
497
422
 
498
423
  const col = await this.getStore(cls);
@@ -517,11 +442,13 @@ export class MongoModelService implements
517
442
  return res.deletedCount ?? 0;
518
443
  }
519
444
 
520
- async updateByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
445
+ async updatePartialByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
521
446
  await QueryVerifier.verify(cls, query);
522
447
 
448
+ const item = await ModelCrudUtil.prePartialUpdate(cls, data);
449
+
523
450
  const col = await this.getStore(cls);
524
- const items = MongoUtil.extractSimple(cls, data);
451
+ const items = MongoUtil.extractSimple(cls, item);
525
452
  const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
526
453
  (acc, [k, v]) => {
527
454
  if (v === null || v === undefined) {
@@ -586,4 +513,24 @@ export class MongoModelService implements
586
513
  const results = await this.query<T>(cls, q);
587
514
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
588
515
  }
516
+
517
+ // Other
518
+ async queryText<T extends ModelType>(cls: Class<T>, search: string | MongoTextSearch, query: PageableModelQuery<T> = {}): Promise<T[]> {
519
+ await QueryVerifier.verify(cls, query);
520
+
521
+ const col = await this.getStore(cls);
522
+ const filter = MongoUtil.extractWhereFilter(cls, query.where);
523
+ if (typeof search === 'string') {
524
+ search = { $search: search, $language: 'en' };
525
+ }
526
+
527
+ (query.sort ??= []).unshift({
528
+ // @ts-expect-error
529
+ score: { $meta: 'textScore' }
530
+ });
531
+
532
+ const cursor = col.find<T>({ $and: [{ $text: search }, filter] }, {});
533
+ const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
534
+ return await Promise.all(items.map(r => ModelCrudUtil.load(cls, r).then(MongoUtil.postLoadId)));
535
+ }
589
536
  }