@travetto/model-mongo 5.0.0 → 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/package.json +5 -5
- package/src/internal/util.ts +75 -5
- package/src/service.ts +42 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "5.0.
|
|
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.
|
|
29
|
-
"@travetto/model": "^5.0.
|
|
30
|
-
"@travetto/model-query": "^5.0.
|
|
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.
|
|
34
|
+
"@travetto/command": "^5.0.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/command": {
|
package/src/internal/util.ts
CHANGED
|
@@ -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>):
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
453
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
}
|