@travetto/model-mongo 5.0.0 → 5.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
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",
29
- "@travetto/model": "^5.0.0",
30
- "@travetto/model-query": "^5.0.0",
28
+ "@travetto/config": "^5.0.2",
29
+ "@travetto/model": "^5.0.2",
30
+ "@travetto/model-query": "^5.0.2",
31
31
  "mongodb": "^6.8.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0"
34
+ "@travetto/command": "^5.0.2"
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,15 +1,11 @@
1
- import {
2
- type Db, GridFSBucket, MongoClient, type Sort, type CreateIndexesOptions,
3
- type GridFSFile, type IndexSpecification, type Collection, ObjectId,
4
- Binary
5
- } from 'mongodb';
6
1
  import { pipeline } from 'node:stream/promises';
7
- import { Readable } from 'node:stream';
2
+
3
+ import { type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection, type ObjectId, type Binary, type RootFilterOperators } from 'mongodb';
8
4
 
9
5
  import {
10
6
  ModelRegistry, ModelType, OptionalId, ModelCrudSupport, ModelStorageSupport,
11
7
  ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport, BulkOp, BulkResponse,
12
- NotFoundError, ExistsError, IndexConfig, ModelBlobSupport
8
+ NotFoundError, ExistsError, ModelBlobSupport
13
9
  } from '@travetto/model';
14
10
  import {
15
11
  ModelQuery, ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySupport,
@@ -18,35 +14,30 @@ import {
18
14
  } from '@travetto/model-query';
19
15
 
20
16
  import {
21
- ShutdownManager, type Class, type DeepPartial, AppError, TypedObject,
17
+ ShutdownManager, type Class, type DeepPartial, TypedObject,
22
18
  castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil
23
19
  } from '@travetto/runtime';
24
20
  import { Injectable } from '@travetto/di';
25
- import { FieldConfig, SchemaRegistry, SchemaValidator } from '@travetto/schema';
26
21
 
27
22
  import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
28
23
  import { ModelIndexedUtil } from '@travetto/model/src/internal/service/indexed';
29
24
  import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
30
25
  import { ModelQueryUtil } from '@travetto/model-query/src/internal/service/query';
31
26
  import { ModelQuerySuggestUtil } from '@travetto/model-query/src/internal/service/suggest';
32
- import { PointImpl } from '@travetto/model-query/src/internal/model/point';
33
27
  import { ModelQueryExpiryUtil } from '@travetto/model-query/src/internal/service/expiry';
34
28
  import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
35
- import { AllViewⲐ } from '@travetto/schema/src/internal/types';
36
29
  import { ModelBulkUtil } from '@travetto/model/src/internal/service/bulk';
37
30
  import { MODEL_BLOB, ModelBlobNamespace, ModelBlobUtil } from '@travetto/model/src/internal/service/blob';
38
31
 
39
- import { MongoUtil, WithId } from './internal/util';
32
+ import { MongoUtil, PlainIdx, WithId } from './internal/util';
40
33
  import { MongoModelConfig } from './config';
41
34
 
42
- const IdxFieldsⲐ = Symbol.for('@travetto/model-mongo:idx');
43
-
44
- const asFielded = (cfg: IndexConfig<ModelType>): { [IdxFieldsⲐ]: Sort } => castTo(cfg);
45
-
46
- type IdxCfg = CreateIndexesOptions;
35
+ const ListIndexⲐ = Symbol.for('@travetto/mongo-model:list-index');
47
36
 
48
37
  type BlobRaw = GridFSFile & { metadata?: BlobMeta };
49
38
 
39
+ type MongoTextSearch = RootFilterOperators<unknown>['$text'];
40
+
50
41
  /**
51
42
  * Mongo-based model source
52
43
  */
@@ -98,45 +89,13 @@ export class MongoModelService implements
98
89
  await this.#db.dropDatabase();
99
90
  }
100
91
 
101
- getGeoIndices<T extends ModelType>(cls: Class<T>, path: FieldConfig[] = [], root = cls): IndexSpecification[] {
102
- const fields = SchemaRegistry.has(cls) ?
103
- Object.values(SchemaRegistry.get(cls).views[AllViewⲐ].schema) :
104
- [];
105
- const out: IndexSpecification[] = [];
106
- for (const field of fields) {
107
- if (SchemaRegistry.has(field.type)) {
108
- // Recurse
109
- out.push(...this.getGeoIndices(field.type, [...path, field], root));
110
- } else if (field.type === PointImpl) {
111
- const name = [...path, field].map(x => x.name).join('.');
112
- console.debug('Preparing geo-index', { cls: root.Ⲑid, name });
113
- out.push({ [name]: '2d' });
114
- }
115
- }
116
- return out;
117
- }
118
-
119
- getIndices<T extends ModelType>(cls: Class<T>): ([IndexSpecification] | [IndexSpecification, IdxCfg])[] {
120
- const indices = ModelRegistry.get(cls).indices ?? [];
121
- return [
122
- ...indices.map((idx): [IndexSpecification, IdxCfg] => {
123
- const combined = asFielded(idx)[IdxFieldsⲐ] ??= Object.assign({}, ...idx.fields.map(x => MongoUtil.toIndex(x)));
124
- return [
125
- castTo(combined),
126
- (idx.type === 'unique' ? { unique: true } : {})
127
- ];
128
- }),
129
- ...this.getGeoIndices(cls).map((x): [IndexSpecification] => [x])
130
- ];
131
- }
132
-
133
92
  async establishIndices<T extends ModelType>(cls: Class<T>): Promise<void> {
134
93
  const col = await this.getStore(cls);
135
- const creating = this.getIndices(cls);
94
+ const creating = MongoUtil.getIndices(cls, ModelRegistry.get(cls).indices);
136
95
  if (creating.length) {
137
96
  console.debug('Creating indexes', { indices: creating });
138
97
  for (const el of creating) {
139
- await col.createIndex(el[0], el[1] ?? {});
98
+ await col.createIndex(...el);
140
99
  }
141
100
  }
142
101
  }
@@ -223,13 +182,7 @@ export class MongoModelService implements
223
182
  async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
224
183
  const store = await this.getStore(cls);
225
184
 
226
- if (view) {
227
- await SchemaValidator.validate(cls, item, view);
228
- }
229
-
230
- item = await ModelCrudUtil.prePersist(cls, item, 'partial');
231
-
232
- let final: Record<string, unknown> = item;
185
+ let final: Record<string, unknown> = await ModelCrudUtil.prePartialUpdate(cls, item, view);
233
186
 
234
187
  const items = MongoUtil.extractSimple(cls, final, undefined, false);
235
188
  final = Object
@@ -303,8 +256,7 @@ export class MongoModelService implements
303
256
  const meta = await this.describeBlob(location);
304
257
  const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
305
258
  const mongoRange = final ? { start: final.start, end: final.end + 1 } : undefined;
306
- const res = (): Readable => this.#bucket.openDownloadStreamByName(location, mongoRange);
307
- return BinaryUtil.readableBlob(res, { ...meta, range: final });
259
+ return BinaryUtil.readableBlob(() => this.#bucket.openDownloadStreamByName(location, mongoRange), { ...meta, range: final });
308
260
  }
309
261
 
310
262
  async describeBlob(location: string): Promise<BlobMeta> {
@@ -425,18 +377,15 @@ export class MongoModelService implements
425
377
 
426
378
  async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
427
379
  const store = await this.getStore(cls);
428
- const idxCfg = ModelRegistry.getIndex(cls, idx);
429
-
430
- if (idxCfg.type === 'unique') {
431
- throw new AppError('Cannot list on unique indices', 'data');
432
- }
380
+ const idxCfg = ModelRegistry.getIndex(cls, idx, ['sorted', 'unsorted']);
433
381
 
434
382
  const where = this.getWhereFilter(
435
383
  cls,
436
384
  castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
437
385
  );
438
386
 
439
- 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));
440
389
 
441
390
  for await (const el of cursor) {
442
391
  yield (await MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el)));
@@ -449,31 +398,8 @@ export class MongoModelService implements
449
398
 
450
399
  const col = await this.getStore(cls);
451
400
  const filter = MongoUtil.extractWhereFilter(cls, query.where);
452
- let cursor = col.find<T>(filter, {});
453
- if (query.select) {
454
- const selectKey = Object.keys(query.select)[0];
455
- const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : MongoUtil.extractSimple(cls, query.select);
456
- // Remove id if not explicitly defined, and selecting fields directly
457
- if (!select['_id']) {
458
- const values = new Set([...Object.values(select)]);
459
- if (values.has(1) || values.has(true)) {
460
- select['_id'] = false;
461
- }
462
- }
463
- cursor.project(select);
464
- }
465
-
466
- if (query.sort) {
467
- cursor = cursor.sort(Object.assign({}, ...query.sort.map(x => MongoUtil.extractSimple(cls, x))));
468
- }
469
-
470
- cursor = cursor.limit(Math.trunc(query.limit ?? 200));
471
-
472
- if (query.offset && typeof query.offset === 'number') {
473
- cursor = cursor.skip(Math.trunc(query.offset ?? 0));
474
- }
475
-
476
- const items = await cursor.toArray();
401
+ const cursor = col.find<T>(filter, {});
402
+ const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
477
403
  return await Promise.all(items.map(r => ModelCrudUtil.load(cls, r).then(MongoUtil.postLoadId)));
478
404
  }
479
405
 
@@ -491,7 +417,7 @@ export class MongoModelService implements
491
417
  }
492
418
 
493
419
  // Query Crud
494
- 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> {
495
421
  await QueryVerifier.verify(cls, query);
496
422
 
497
423
  const col = await this.getStore(cls);
@@ -516,11 +442,13 @@ export class MongoModelService implements
516
442
  return res.deletedCount ?? 0;
517
443
  }
518
444
 
519
- 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> {
520
446
  await QueryVerifier.verify(cls, query);
521
447
 
448
+ const item = await ModelCrudUtil.prePartialUpdate(cls, data);
449
+
522
450
  const col = await this.getStore(cls);
523
- const items = MongoUtil.extractSimple(cls, data);
451
+ const items = MongoUtil.extractSimple(cls, item);
524
452
  const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
525
453
  (acc, [k, v]) => {
526
454
  if (v === null || v === undefined) {
@@ -585,4 +513,24 @@ export class MongoModelService implements
585
513
  const results = await this.query<T>(cls, q);
586
514
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
587
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
+ }
588
536
  }