@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 +1 -1
- package/package.json +5 -5
- package/src/internal/util.ts +75 -5
- package/src/service.ts +77 -130
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.
|
|
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,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
|
-
|
|
13
|
-
|
|
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 {
|
|
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
|
|
35
|
+
const ListIndexⲐ = Symbol.for('@travetto/mongo-model:list-index');
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
type BlobRaw = GridFSFile & { metadata?: BlobMeta };
|
|
46
38
|
|
|
47
|
-
type
|
|
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,
|
|
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 #
|
|
70
|
-
const files:
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
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 ===
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
287
|
-
async
|
|
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:
|
|
290
|
-
metadata:
|
|
244
|
+
contentType: blobMeta.contentType,
|
|
245
|
+
metadata: blobMeta,
|
|
291
246
|
});
|
|
247
|
+
await pipeline(stream, writeStream);
|
|
292
248
|
|
|
293
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
return
|
|
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
|
|
312
|
-
return (await this.#
|
|
262
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
263
|
+
return (await this.#describeBlobRaw(location)).metadata ?? {};
|
|
313
264
|
}
|
|
314
265
|
|
|
315
|
-
async
|
|
316
|
-
const fileId = (await this.#
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
}
|