@travetto/model-mongo 8.0.0-alpha.24 → 8.0.0-alpha.25
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 +13 -17
- package/__index__.ts +1 -1
- package/package.json +6 -6
- package/src/config.ts +8 -12
- package/src/internal/util.ts +38 -26
- package/src/service.ts +195 -139
- package/support/service.mongo.ts +1 -1
package/README.md
CHANGED
|
@@ -13,25 +13,25 @@ npm install @travetto/model-mongo
|
|
|
13
13
|
yarn add @travetto/model-mongo
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides an [mongodb](https://mongodb.com)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.").
|
|
16
|
+
This module provides an [mongodb](https://mongodb.com)-based implementation for 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 [mongodb](https://mongodb.com).. Given the dynamic nature of [mongodb](https://mongodb.com), during development when models are modified, nothing needs to be done to adapt to the latest schema.
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
|
-
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
21
|
-
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L64)
|
|
22
19
|
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
|
|
23
|
-
* [
|
|
20
|
+
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L60)
|
|
21
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
|
|
22
|
+
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
23
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
|
|
24
24
|
* [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
|
|
25
25
|
* [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
|
|
26
|
-
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
|
|
27
26
|
* [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/suggest.ts#L12)
|
|
27
|
+
* [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
|
|
28
28
|
|
|
29
29
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
30
30
|
|
|
31
31
|
**Code: Wiring up a custom Model Source**
|
|
32
32
|
```typescript
|
|
33
33
|
import { InjectableFactory } from '@travetto/di';
|
|
34
|
-
import {
|
|
34
|
+
import { type MongoModelConfig, MongoModelService } from '@travetto/model-mongo';
|
|
35
35
|
|
|
36
36
|
export class Init {
|
|
37
37
|
@InjectableFactory({
|
|
@@ -82,7 +82,7 @@ export class MongoModelConfig {
|
|
|
82
82
|
*/
|
|
83
83
|
@Field({ type: Object })
|
|
84
84
|
options: Omit<mongo.MongoClientOptions, 'cert'> & {
|
|
85
|
-
cert?:
|
|
85
|
+
cert?: Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
|
|
86
86
|
} = {};
|
|
87
87
|
/**
|
|
88
88
|
* Allow storage modification at runtime
|
|
@@ -124,15 +124,14 @@ export class MongoModelConfig {
|
|
|
124
124
|
if (!this.port || Number.isNaN(this.port)) {
|
|
125
125
|
this.port = 27017;
|
|
126
126
|
}
|
|
127
|
-
if (!this.hosts
|
|
127
|
+
if (!this.hosts?.length) {
|
|
128
128
|
this.hosts = ['localhost'];
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
const options = this.options;
|
|
132
132
|
if (options.ssl) {
|
|
133
133
|
if (options.cert) {
|
|
134
|
-
options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
|
|
135
|
-
.map(BinaryUtil.binaryArrayToUint8Array);
|
|
134
|
+
options.cert = (await Promise.all([options.cert].flat(2).map(readCert))).map(BinaryUtil.binaryArrayToUint8Array);
|
|
136
135
|
}
|
|
137
136
|
if (options.tlsCertificateKeyFile) {
|
|
138
137
|
options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
|
|
@@ -155,15 +154,12 @@ export class MongoModelConfig {
|
|
|
155
154
|
* Build connection URLs
|
|
156
155
|
*/
|
|
157
156
|
get url(): string {
|
|
158
|
-
const hosts = this.hosts
|
|
159
|
-
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
160
|
-
.join(',');
|
|
157
|
+
const hosts = this.hosts!.map(host => (this.srvRecord || host.includes(':') ? host : `${host}:${this.port ?? 27017}`)).join(',');
|
|
161
158
|
const optionString = new URLSearchParams(
|
|
162
159
|
Object.entries(this.options)
|
|
163
160
|
.filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
|
|
164
161
|
.map(([k, v]) => [k, `${v}`])
|
|
165
|
-
)
|
|
166
|
-
.toString();
|
|
162
|
+
).toString();
|
|
167
163
|
let creds = '';
|
|
168
164
|
if (this.username) {
|
|
169
165
|
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
@@ -174,4 +170,4 @@ export class MongoModelConfig {
|
|
|
174
170
|
}
|
|
175
171
|
```
|
|
176
172
|
|
|
177
|
-
Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) annotation, and so these values can be overridden using the standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") resolution paths.The SSL file options in `clientOptions` will automatically be resolved to files when given a path.
|
|
173
|
+
Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) annotation, and so these values can be overridden using the standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") resolution paths.The SSL file options in `clientOptions` will automatically be resolved to files when given a path. This path can be a resource path (will attempt to lookup using [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8)) or just a standard file path.
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mongo backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/model-mongo"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
30
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/model-indexed": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/model-query": "^8.0.0-alpha.
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.22",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.23",
|
|
31
|
+
"@travetto/model-indexed": "^8.0.0-alpha.25",
|
|
32
|
+
"@travetto/model-query": "^8.0.0-alpha.24",
|
|
33
33
|
"mongodb": "^7.5.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.28"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type mongo from 'mongodb';
|
|
2
2
|
|
|
3
|
-
import { type TimeSpan, Runtime, RuntimeResources, BinaryUtil, CodecUtil, type BinaryType, type BinaryArray } from '@travetto/runtime';
|
|
4
3
|
import { Config } from '@travetto/config';
|
|
5
|
-
import { Field } from '@travetto/schema';
|
|
6
4
|
import { PostConstruct } from '@travetto/di';
|
|
5
|
+
import { type BinaryArray, type BinaryType, BinaryUtil, CodecUtil, Runtime, RuntimeResources, type TimeSpan } from '@travetto/runtime';
|
|
6
|
+
import { Field } from '@travetto/schema';
|
|
7
7
|
|
|
8
8
|
const readCert = async (input: BinaryType | string): Promise<BinaryArray> => {
|
|
9
9
|
if (BinaryUtil.isBinaryType(input)) {
|
|
@@ -55,7 +55,7 @@ export class MongoModelConfig {
|
|
|
55
55
|
*/
|
|
56
56
|
@Field({ type: Object })
|
|
57
57
|
options: Omit<mongo.MongoClientOptions, 'cert'> & {
|
|
58
|
-
cert?:
|
|
58
|
+
cert?: Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
|
|
59
59
|
} = {};
|
|
60
60
|
/**
|
|
61
61
|
* Allow storage modification at runtime
|
|
@@ -97,15 +97,14 @@ export class MongoModelConfig {
|
|
|
97
97
|
if (!this.port || Number.isNaN(this.port)) {
|
|
98
98
|
this.port = 27017;
|
|
99
99
|
}
|
|
100
|
-
if (!this.hosts
|
|
100
|
+
if (!this.hosts?.length) {
|
|
101
101
|
this.hosts = ['localhost'];
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const options = this.options;
|
|
105
105
|
if (options.ssl) {
|
|
106
106
|
if (options.cert) {
|
|
107
|
-
options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
|
|
108
|
-
.map(BinaryUtil.binaryArrayToUint8Array);
|
|
107
|
+
options.cert = (await Promise.all([options.cert].flat(2).map(readCert))).map(BinaryUtil.binaryArrayToUint8Array);
|
|
109
108
|
}
|
|
110
109
|
if (options.tlsCertificateKeyFile) {
|
|
111
110
|
options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
|
|
@@ -128,15 +127,12 @@ export class MongoModelConfig {
|
|
|
128
127
|
* Build connection URLs
|
|
129
128
|
*/
|
|
130
129
|
get url(): string {
|
|
131
|
-
const hosts = this.hosts
|
|
132
|
-
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
133
|
-
.join(',');
|
|
130
|
+
const hosts = this.hosts!.map(host => (this.srvRecord || host.includes(':') ? host : `${host}:${this.port ?? 27017}`)).join(',');
|
|
134
131
|
const optionString = new URLSearchParams(
|
|
135
132
|
Object.entries(this.options)
|
|
136
133
|
.filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
|
|
137
134
|
.map(([k, v]) => [k, `${v}`])
|
|
138
|
-
)
|
|
139
|
-
.toString();
|
|
135
|
+
).toString();
|
|
140
136
|
let creds = '';
|
|
141
137
|
if (this.username) {
|
|
142
138
|
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
@@ -144,4 +140,4 @@ export class MongoModelConfig {
|
|
|
144
140
|
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
|
|
145
141
|
return url;
|
|
146
142
|
}
|
|
147
|
-
}
|
|
143
|
+
}
|
package/src/internal/util.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Binary,
|
|
3
|
-
type
|
|
2
|
+
Binary,
|
|
3
|
+
type CreateIndexesOptions,
|
|
4
|
+
type Filter,
|
|
5
|
+
type FindCursor,
|
|
6
|
+
type IndexDescriptionInfo,
|
|
7
|
+
type IndexDirection,
|
|
8
|
+
type WithId as MongoWithId,
|
|
9
|
+
ObjectId
|
|
4
10
|
} from 'mongodb';
|
|
5
11
|
|
|
6
|
-
import {
|
|
7
|
-
import { type DistanceUnit, type PageableModelQuery, type WhereClause, isModelQueryIndex, ModelQueryUtil } from '@travetto/model-query';
|
|
8
|
-
import { type ModelType, type IndexConfig, IndexNotSupported } from '@travetto/model';
|
|
9
|
-
import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
|
|
12
|
+
import { type IndexConfig, IndexNotSupported, type ModelType } from '@travetto/model';
|
|
10
13
|
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
14
|
+
import { type DistanceUnit, isModelQueryIndex, ModelQueryUtil, type PageableModelQuery, type WhereClause } from '@travetto/model-query';
|
|
15
|
+
import { BinaryUtil, type Class, CodecUtil, castTo, RuntimeError, toConcrete } from '@travetto/runtime';
|
|
16
|
+
import { DataUtil, type Point, SchemaRegistryIndex } from '@travetto/schema';
|
|
11
17
|
|
|
12
18
|
const PointConcrete = toConcrete<Point>();
|
|
13
19
|
|
|
@@ -42,19 +48,13 @@ function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string,
|
|
|
42
48
|
* Basic mongo utils for conforming to the model module
|
|
43
49
|
*/
|
|
44
50
|
export class MongoUtil {
|
|
45
|
-
|
|
46
51
|
static namespaceIndex(cls: Class, name: string): string {
|
|
47
52
|
return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
static uuid(value: string): Binary {
|
|
51
56
|
try {
|
|
52
|
-
return new Binary(
|
|
53
|
-
BinaryUtil.binaryArrayToUint8Array(
|
|
54
|
-
CodecUtil.fromHexString(value.replaceAll('-', ''))
|
|
55
|
-
),
|
|
56
|
-
Binary.SUBTYPE_UUID
|
|
57
|
-
);
|
|
57
|
+
return new Binary(BinaryUtil.binaryArrayToUint8Array(CodecUtil.fromHexString(value.replaceAll('-', ''))), Binary.SUBTYPE_UUID);
|
|
58
58
|
} catch (err) {
|
|
59
59
|
if (err instanceof RuntimeError && err.message === 'Invalid hex string') {
|
|
60
60
|
return null!;
|
|
@@ -94,7 +94,12 @@ export class MongoUtil {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**/
|
|
97
|
-
static extractSimple<T>(
|
|
97
|
+
static extractSimple<T>(
|
|
98
|
+
base: Class<T> | undefined,
|
|
99
|
+
item: Record<string, unknown>,
|
|
100
|
+
path: string = '',
|
|
101
|
+
recursive: boolean = true
|
|
102
|
+
): Record<string, unknown> {
|
|
98
103
|
const fields = base ? SchemaRegistryIndex.getOptional(base)?.getFields() : undefined;
|
|
99
104
|
const out: Record<string, unknown> = {};
|
|
100
105
|
const sub = item;
|
|
@@ -140,7 +145,7 @@ export class MongoUtil {
|
|
|
140
145
|
value.$regex = DataUtil.toRegex(castTo(value.$regex));
|
|
141
146
|
} else if (firstKey && '$near' in value) {
|
|
142
147
|
const dist: number = castTo(value.$maxDistance);
|
|
143
|
-
const distance = dist / RADIANS_TO[
|
|
148
|
+
const distance = dist / RADIANS_TO[castTo<DistanceUnit>(value.$unit) ?? 'km'];
|
|
144
149
|
value.$maxDistance = distance;
|
|
145
150
|
delete value.$unit;
|
|
146
151
|
} else if (firstKey && '$geoWithin' in value) {
|
|
@@ -187,8 +192,9 @@ export class MongoUtil {
|
|
|
187
192
|
const name = this.namespaceIndex(cls, idx.name);
|
|
188
193
|
if (isModelQueryIndex(idx)) {
|
|
189
194
|
const out = idx.fields.reduce(
|
|
190
|
-
(acc, field) => (
|
|
191
|
-
castTo<Record<string, -1 | 0 | 1>>({})
|
|
195
|
+
(acc, field) => Object.assign(acc, { ...flattenKeys(castTo(field)) }),
|
|
196
|
+
castTo<Record<string, -1 | 0 | 1>>({})
|
|
197
|
+
);
|
|
192
198
|
|
|
193
199
|
return [out, { name, unique: !!idx.unique }];
|
|
194
200
|
} else if (isModelIndexedIndex(idx)) {
|
|
@@ -197,23 +203,29 @@ export class MongoUtil {
|
|
|
197
203
|
...idx.sortTemplate.map(({ path, value }) => [path.join('.'), value === -1 ? -1 : 1])
|
|
198
204
|
]);
|
|
199
205
|
switch (idx.type) {
|
|
200
|
-
case 'indexed:keyed':
|
|
201
|
-
|
|
206
|
+
case 'indexed:keyed':
|
|
207
|
+
return [filter, { name, unique: idx.unique }];
|
|
208
|
+
case 'indexed:sorted':
|
|
209
|
+
return [filter, { name }];
|
|
202
210
|
}
|
|
203
211
|
} else {
|
|
204
212
|
throw new IndexNotSupported(cls, idx);
|
|
205
213
|
}
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
static prepareCursor<T extends ModelType>(
|
|
216
|
+
static prepareCursor<T extends ModelType>(
|
|
217
|
+
cls: Class<T>,
|
|
218
|
+
cursor: FindCursor<T | MongoWithId<T>>,
|
|
219
|
+
query: PageableModelQuery<T>
|
|
220
|
+
): FindCursor<T> {
|
|
209
221
|
if (query.select) {
|
|
210
222
|
const selectKey = Object.keys(query.select)[0];
|
|
211
223
|
const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
|
|
212
224
|
// Remove id if not explicitly defined, and selecting fields directly
|
|
213
|
-
if (!select
|
|
225
|
+
if (!select._id) {
|
|
214
226
|
const values = new Set([...Object.values(select)]);
|
|
215
227
|
if (values.has(1) || values.has(true)) {
|
|
216
|
-
select
|
|
228
|
+
select._id = false;
|
|
217
229
|
}
|
|
218
230
|
}
|
|
219
231
|
cursor.project(select);
|
|
@@ -241,9 +253,9 @@ export class MongoUtil {
|
|
|
241
253
|
existing.expireAfterSeconds !== pendingOptions.expireAfterSeconds ||
|
|
242
254
|
existing.bucketSize !== pendingOptions.bucketSize;
|
|
243
255
|
|
|
244
|
-
const existingFields = existing.textIndexVersion
|
|
245
|
-
Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
|
|
246
|
-
existing.key;
|
|
256
|
+
const existingFields = existing.textIndexVersion
|
|
257
|
+
? Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
|
|
258
|
+
: existing.key;
|
|
247
259
|
|
|
248
260
|
const pendingKeySet = new Set(Object.keys(pendingKey));
|
|
249
261
|
const existingKeySet = new Set(Object.keys(existingFields));
|
|
@@ -259,4 +271,4 @@ export class MongoUtil {
|
|
|
259
271
|
|
|
260
272
|
return changed;
|
|
261
273
|
}
|
|
262
|
-
}
|
|
274
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -1,40 +1,91 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type Binary,
|
|
3
|
-
type
|
|
2
|
+
type Binary,
|
|
3
|
+
type Collection,
|
|
4
|
+
type Db,
|
|
5
|
+
type Filter,
|
|
6
|
+
type FindCursor,
|
|
7
|
+
GridFSBucket,
|
|
8
|
+
type GridFSFile,
|
|
9
|
+
MongoClient,
|
|
4
10
|
MongoServerError,
|
|
11
|
+
type WithId as MongoWithId,
|
|
12
|
+
type ObjectId,
|
|
13
|
+
type RootFilterOperators
|
|
5
14
|
} from 'mongodb';
|
|
6
15
|
|
|
16
|
+
import { Injectable, PostConstruct } from '@travetto/di';
|
|
7
17
|
import {
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
18
|
+
type BulkOperation,
|
|
19
|
+
type BulkResponse,
|
|
20
|
+
ExistsError,
|
|
21
|
+
type ModelBlobSupport,
|
|
22
|
+
type ModelBulkSupport,
|
|
23
|
+
ModelBulkUtil,
|
|
24
|
+
type ModelCrudSupport,
|
|
25
|
+
ModelCrudUtil,
|
|
26
|
+
type ModelExpirySupport,
|
|
27
|
+
ModelExpiryUtil,
|
|
28
|
+
type ModelListOptions,
|
|
29
|
+
ModelRegistryIndex,
|
|
30
|
+
type ModelStorageSupport,
|
|
31
|
+
ModelStorageUtil,
|
|
32
|
+
type ModelType,
|
|
33
|
+
NotFoundError,
|
|
34
|
+
type OptionalId
|
|
11
35
|
} from '@travetto/model';
|
|
12
36
|
import {
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
|
|
37
|
+
type FullKeyedIndexBody,
|
|
38
|
+
type FullKeyedIndexWithPartialBody,
|
|
39
|
+
type KeyedIndexBody,
|
|
40
|
+
type KeyedIndexSelection,
|
|
41
|
+
ModelIndexedComputedIndex,
|
|
42
|
+
type ModelIndexedSearchOptions,
|
|
43
|
+
type ModelIndexedSupport,
|
|
44
|
+
ModelIndexedUtil,
|
|
45
|
+
type ModelPageOptions,
|
|
46
|
+
type ModelPageResult,
|
|
47
|
+
type SingleItemIndex,
|
|
48
|
+
type SortedIndex,
|
|
49
|
+
type SortedIndexSelection,
|
|
50
|
+
type SortedIndexSelectionType
|
|
21
51
|
} from '@travetto/model-indexed';
|
|
22
|
-
|
|
23
52
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
type ModelQuery,
|
|
54
|
+
type ModelQueryCrudSupport,
|
|
55
|
+
ModelQueryCrudUtil,
|
|
56
|
+
type ModelQueryFacet,
|
|
57
|
+
type ModelQueryFacetSupport,
|
|
58
|
+
type ModelQuerySuggestSupport,
|
|
59
|
+
ModelQuerySuggestUtil,
|
|
60
|
+
type ModelQuerySupport,
|
|
61
|
+
ModelQueryUtil,
|
|
62
|
+
type PageableModelQuery,
|
|
63
|
+
QueryVerifier,
|
|
64
|
+
type ValidStringFields,
|
|
65
|
+
type WhereClause
|
|
66
|
+
} from '@travetto/model-query';
|
|
67
|
+
import {
|
|
68
|
+
asFull,
|
|
69
|
+
type BinaryMetadata,
|
|
70
|
+
BinaryMetadataUtil,
|
|
71
|
+
type BinaryType,
|
|
72
|
+
BinaryUtil,
|
|
73
|
+
type ByteRange,
|
|
74
|
+
type Class,
|
|
75
|
+
castTo,
|
|
76
|
+
JSONUtil,
|
|
77
|
+
ShutdownManager,
|
|
78
|
+
TypedObject
|
|
26
79
|
} from '@travetto/runtime';
|
|
27
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
28
80
|
|
|
29
|
-
import { MongoUtil, type WithId } from './internal/util.ts';
|
|
30
81
|
import type { MongoModelConfig } from './config.ts';
|
|
82
|
+
import { MongoUtil, type WithId } from './internal/util.ts';
|
|
31
83
|
|
|
32
84
|
type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
|
|
33
85
|
|
|
34
86
|
type MongoTextSearch = RootFilterOperators<unknown>['$text'];
|
|
35
87
|
|
|
36
|
-
const isDuplicateKeyError = (error: unknown): boolean =>
|
|
37
|
-
error instanceof MongoServerError && error.message.includes('duplicate key error');
|
|
88
|
+
const isDuplicateKeyError = (error: unknown): boolean => error instanceof MongoServerError && error.message.includes('duplicate key error');
|
|
38
89
|
|
|
39
90
|
export const ModelBlobNamespace = '__blobs';
|
|
40
91
|
|
|
@@ -42,30 +93,41 @@ export const ModelBlobNamespace = '__blobs';
|
|
|
42
93
|
* Mongo-based model source
|
|
43
94
|
*/
|
|
44
95
|
@Injectable()
|
|
45
|
-
export class MongoModelService
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
export class MongoModelService
|
|
97
|
+
implements
|
|
98
|
+
ModelCrudSupport,
|
|
99
|
+
ModelStorageSupport,
|
|
100
|
+
ModelBulkSupport,
|
|
101
|
+
ModelBlobSupport,
|
|
102
|
+
ModelIndexedSupport,
|
|
103
|
+
ModelQuerySupport,
|
|
104
|
+
ModelQueryCrudSupport,
|
|
105
|
+
ModelQueryFacetSupport,
|
|
106
|
+
ModelQuerySuggestSupport,
|
|
107
|
+
ModelExpirySupport
|
|
108
|
+
{
|
|
52
109
|
#db: Db;
|
|
53
110
|
#bucket: GridFSBucket;
|
|
54
111
|
idSource = ModelCrudUtil.uuidSource();
|
|
55
112
|
client: MongoClient;
|
|
56
113
|
config: MongoModelConfig;
|
|
57
114
|
|
|
58
|
-
constructor(config: MongoModelConfig) {
|
|
115
|
+
constructor(config: MongoModelConfig) {
|
|
116
|
+
this.config = config;
|
|
117
|
+
}
|
|
59
118
|
|
|
60
|
-
async
|
|
119
|
+
async *#iterateCursor<T extends ModelType>(
|
|
120
|
+
cls: Class<T>,
|
|
121
|
+
cursor: FindCursor,
|
|
122
|
+
options?: ModelListOptions & ModelPageOptions<number>
|
|
123
|
+
): AsyncGenerator<T[]> {
|
|
61
124
|
const batchSize = options?.batchSizeHint ?? 100;
|
|
62
125
|
let batch: T[] = [];
|
|
63
126
|
const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
64
127
|
for await (const item of cursor
|
|
65
128
|
.batchSize(batchSize)
|
|
66
129
|
.limit(maxCount)
|
|
67
|
-
.skip(options?.offset ?? 0)
|
|
68
|
-
) {
|
|
130
|
+
.skip(options?.offset ?? 0)) {
|
|
69
131
|
if (options?.abort?.aborted) {
|
|
70
132
|
cursor.close();
|
|
71
133
|
break;
|
|
@@ -95,8 +157,7 @@ export class MongoModelService implements
|
|
|
95
157
|
}
|
|
96
158
|
|
|
97
159
|
const where = this.getWhereFilter(cls, whereClause);
|
|
98
|
-
let q = store.find(where, { timeout: true })
|
|
99
|
-
.batchSize(100);
|
|
160
|
+
let q = store.find(where, { timeout: true }).batchSize(100);
|
|
100
161
|
|
|
101
162
|
// TODO: We could cache this
|
|
102
163
|
if ('sort' in idx) {
|
|
@@ -105,7 +166,7 @@ export class MongoModelService implements
|
|
|
105
166
|
return q;
|
|
106
167
|
}
|
|
107
168
|
|
|
108
|
-
restoreId(item: { id?: string
|
|
169
|
+
restoreId(item: { id?: string; _id?: unknown }): void {
|
|
109
170
|
if (item._id) {
|
|
110
171
|
item.id ??= MongoUtil.idToString(castTo(item._id));
|
|
111
172
|
delete item._id;
|
|
@@ -125,11 +186,11 @@ export class MongoModelService implements
|
|
|
125
186
|
return item;
|
|
126
187
|
}
|
|
127
188
|
|
|
128
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
189
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: string }): string;
|
|
129
190
|
preUpdate<T extends OptionalId<ModelType>>(item: Omit<T, 'id'> & { _id?: Binary }): undefined;
|
|
130
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
131
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
132
|
-
if (item
|
|
191
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: undefined }): undefined;
|
|
192
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id?: string }): string | undefined {
|
|
193
|
+
if (item?.id) {
|
|
133
194
|
const id = item.id;
|
|
134
195
|
item._id = MongoUtil.uuid(id);
|
|
135
196
|
if (!this.config.storeId) {
|
|
@@ -153,7 +214,7 @@ export class MongoModelService implements
|
|
|
153
214
|
async initializeClient(): Promise<void> {
|
|
154
215
|
this.client = await MongoClient.connect(this.config.url, {
|
|
155
216
|
...this.config.connectionOptions,
|
|
156
|
-
useBigInt64: true
|
|
217
|
+
useBigInt64: true
|
|
157
218
|
});
|
|
158
219
|
this.#db = this.client.db(this.config.namespace);
|
|
159
220
|
this.#bucket = new GridFSBucket(this.#db, {
|
|
@@ -174,7 +235,7 @@ export class MongoModelService implements
|
|
|
174
235
|
}
|
|
175
236
|
|
|
176
237
|
// Storage
|
|
177
|
-
async createStorage(): Promise<void> {
|
|
238
|
+
async createStorage(): Promise<void> {}
|
|
178
239
|
|
|
179
240
|
async deleteStorage(): Promise<void> {
|
|
180
241
|
await this.#db.dropDatabase();
|
|
@@ -182,10 +243,7 @@ export class MongoModelService implements
|
|
|
182
243
|
|
|
183
244
|
async upsertModel(cls: Class): Promise<void> {
|
|
184
245
|
const col = await this.getStore(cls);
|
|
185
|
-
const indices = [
|
|
186
|
-
...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)),
|
|
187
|
-
...MongoUtil.getExtraIndices(cls)
|
|
188
|
-
];
|
|
246
|
+
const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
|
|
189
247
|
const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
|
|
190
248
|
|
|
191
249
|
const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
|
|
@@ -219,7 +277,7 @@ export class MongoModelService implements
|
|
|
219
277
|
}
|
|
220
278
|
|
|
221
279
|
async truncateBlob(): Promise<void> {
|
|
222
|
-
await this.#bucket.drop().catch(() => {
|
|
280
|
+
await this.#bucket.drop().catch(() => {});
|
|
223
281
|
}
|
|
224
282
|
|
|
225
283
|
/**
|
|
@@ -275,11 +333,7 @@ export class MongoModelService implements
|
|
|
275
333
|
const store = await this.getStore(cls);
|
|
276
334
|
|
|
277
335
|
try {
|
|
278
|
-
await store.updateOne(
|
|
279
|
-
this.getIdFilter(cls, id, false),
|
|
280
|
-
{ $set: cleaned },
|
|
281
|
-
{ upsert: true }
|
|
282
|
-
);
|
|
336
|
+
await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
|
|
283
337
|
} catch (error) {
|
|
284
338
|
throw isDuplicateKeyError(error) ? new ExistsError(cls, id) : error;
|
|
285
339
|
}
|
|
@@ -292,24 +346,23 @@ export class MongoModelService implements
|
|
|
292
346
|
const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
|
|
293
347
|
const simple = MongoUtil.extractSimple(cls, final, undefined, false);
|
|
294
348
|
|
|
295
|
-
const operation: Partial<T> = castTo(
|
|
296
|
-
.entries(simple)
|
|
297
|
-
.reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
349
|
+
const operation: Partial<T> = castTo(
|
|
350
|
+
Object.entries(simple).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
298
351
|
if (value === null || value === undefined) {
|
|
299
352
|
(document.$unset ??= {})[key] = value;
|
|
300
353
|
} else {
|
|
301
354
|
(document.$set ??= {})[key] = value;
|
|
302
355
|
}
|
|
303
356
|
return document;
|
|
304
|
-
}, {})
|
|
357
|
+
}, {})
|
|
358
|
+
);
|
|
305
359
|
|
|
306
360
|
const id = item.id;
|
|
307
361
|
|
|
308
|
-
const result = await store.findOneAndUpdate(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
362
|
+
const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
|
|
363
|
+
returnDocument: 'after',
|
|
364
|
+
includeResultMetadata: true
|
|
365
|
+
});
|
|
313
366
|
|
|
314
367
|
if (!result.value) {
|
|
315
368
|
throw new NotFoundError(cls, id);
|
|
@@ -326,7 +379,7 @@ export class MongoModelService implements
|
|
|
326
379
|
}
|
|
327
380
|
}
|
|
328
381
|
|
|
329
|
-
async *
|
|
382
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
330
383
|
const store = await this.getStore(cls);
|
|
331
384
|
const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
|
|
332
385
|
yield* this.#iterateCursor(cls, cursor, options);
|
|
@@ -334,7 +387,10 @@ export class MongoModelService implements
|
|
|
334
387
|
|
|
335
388
|
// Blob
|
|
336
389
|
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
337
|
-
const existing = await this.getBlobMetadata(location).then(
|
|
390
|
+
const existing = await this.getBlobMetadata(location).then(
|
|
391
|
+
() => true,
|
|
392
|
+
() => false
|
|
393
|
+
);
|
|
338
394
|
if (!overwrite && existing) {
|
|
339
395
|
return;
|
|
340
396
|
}
|
|
@@ -366,10 +422,9 @@ export class MongoModelService implements
|
|
|
366
422
|
}
|
|
367
423
|
|
|
368
424
|
async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
|
|
369
|
-
await this.#db
|
|
370
|
-
{
|
|
371
|
-
{ $set: { metadata, contentType: metadata.contentType! } }
|
|
372
|
-
);
|
|
425
|
+
await this.#db
|
|
426
|
+
.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
|
|
427
|
+
.findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
|
|
373
428
|
}
|
|
374
429
|
|
|
375
430
|
// Bulk
|
|
@@ -402,7 +457,10 @@ export class MongoModelService implements
|
|
|
402
457
|
bulk.insert(operation.insert);
|
|
403
458
|
} else if (operation.upsert) {
|
|
404
459
|
const id = this.preUpdate(operation.upsert);
|
|
405
|
-
bulk
|
|
460
|
+
bulk
|
|
461
|
+
.find({ _id: MongoUtil.uuid(id!) })
|
|
462
|
+
.upsert()
|
|
463
|
+
.updateOne({ $set: operation.upsert });
|
|
406
464
|
} else if (operation.update) {
|
|
407
465
|
const id = this.preUpdate(operation.update);
|
|
408
466
|
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
|
|
@@ -451,89 +509,77 @@ export class MongoModelService implements
|
|
|
451
509
|
}
|
|
452
510
|
|
|
453
511
|
// Indexed
|
|
454
|
-
async getByIndex<
|
|
455
|
-
T
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
512
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
513
|
+
cls: Class<T>,
|
|
514
|
+
idx: SingleItemIndex<T, K, S>,
|
|
515
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
516
|
+
): Promise<T> {
|
|
459
517
|
const store = await this.getStore(cls);
|
|
460
518
|
|
|
461
519
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
462
520
|
|
|
463
|
-
const result = await store.findOne(
|
|
464
|
-
this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
|
|
465
|
-
);
|
|
521
|
+
const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
466
522
|
if (!result) {
|
|
467
523
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
468
524
|
}
|
|
469
525
|
return await this.postLoad(cls, result);
|
|
470
526
|
}
|
|
471
527
|
|
|
472
|
-
async deleteByIndex<
|
|
473
|
-
T
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
528
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
529
|
+
cls: Class<T>,
|
|
530
|
+
idx: SingleItemIndex<T, K, S>,
|
|
531
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
532
|
+
): Promise<void> {
|
|
477
533
|
const store = await this.getStore(cls);
|
|
478
534
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
479
535
|
|
|
480
|
-
const result = await store.deleteOne(
|
|
481
|
-
this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
|
|
482
|
-
);
|
|
536
|
+
const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
483
537
|
if (!result.deletedCount) {
|
|
484
538
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
485
539
|
}
|
|
486
540
|
}
|
|
487
541
|
|
|
488
|
-
upsertByIndex<
|
|
489
|
-
T
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
542
|
+
upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
543
|
+
cls: Class<T>,
|
|
544
|
+
idx: SingleItemIndex<T, K, S>,
|
|
545
|
+
body: OptionalId<T>
|
|
546
|
+
): Promise<T> {
|
|
493
547
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
494
548
|
}
|
|
495
549
|
|
|
496
|
-
updateByIndex<
|
|
497
|
-
T
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
550
|
+
updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
551
|
+
cls: Class<T>,
|
|
552
|
+
idx: SingleItemIndex<T, K, S>,
|
|
553
|
+
body: T
|
|
554
|
+
): Promise<T> {
|
|
501
555
|
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
502
556
|
}
|
|
503
557
|
|
|
504
|
-
async updatePartialByIndex<
|
|
505
|
-
T
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
558
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
559
|
+
cls: Class<T>,
|
|
560
|
+
idx: SingleItemIndex<T, K>,
|
|
561
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
562
|
+
): Promise<T> {
|
|
509
563
|
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
510
564
|
return this.update(cls, item);
|
|
511
565
|
}
|
|
512
566
|
|
|
513
|
-
async pageByIndex<
|
|
514
|
-
T extends ModelType,
|
|
515
|
-
K extends KeyedIndexSelection<T>,
|
|
516
|
-
S extends SortedIndexSelection<T>
|
|
517
|
-
>(
|
|
567
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
518
568
|
cls: Class<T>,
|
|
519
569
|
idx: SortedIndex<T, K, S>,
|
|
520
570
|
body: KeyedIndexBody<T, K>,
|
|
521
|
-
options?: ModelPageOptions
|
|
571
|
+
options?: ModelPageOptions
|
|
522
572
|
): Promise<ModelPageResult<T>> {
|
|
523
573
|
{
|
|
524
574
|
const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
|
|
525
|
-
const cursor =
|
|
575
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body);
|
|
526
576
|
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
|
|
527
577
|
const items = batches.flat();
|
|
528
578
|
return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
|
|
529
579
|
}
|
|
530
580
|
}
|
|
531
581
|
|
|
532
|
-
async *
|
|
533
|
-
T extends ModelType,
|
|
534
|
-
K extends KeyedIndexSelection<T>,
|
|
535
|
-
S extends SortedIndexSelection<T>
|
|
536
|
-
>(
|
|
582
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
537
583
|
cls: Class<T>,
|
|
538
584
|
idx: SortedIndex<T, K, S>,
|
|
539
585
|
body: KeyedIndexBody<T, K>,
|
|
@@ -548,18 +594,17 @@ export class MongoModelService implements
|
|
|
548
594
|
S extends SortedIndexSelection<T>,
|
|
549
595
|
K extends KeyedIndexSelection<T>,
|
|
550
596
|
B extends SortedIndexSelectionType<T, S> & string
|
|
551
|
-
>(
|
|
552
|
-
cls
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
})));
|
|
597
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
598
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body, where =>
|
|
599
|
+
castTo({
|
|
600
|
+
$and: [
|
|
601
|
+
where,
|
|
602
|
+
{
|
|
603
|
+
[idx.sortTemplate[0].path.join('.')]: ModelIndexedUtil.getSuggestRegex(prefix)
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
})
|
|
607
|
+
);
|
|
563
608
|
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
|
|
564
609
|
|
|
565
610
|
return batches.flat();
|
|
@@ -622,15 +667,14 @@ export class MongoModelService implements
|
|
|
622
667
|
const item = await ModelCrudUtil.prePartialUpdate(cls, data);
|
|
623
668
|
const col = await this.getStore(cls);
|
|
624
669
|
const items = MongoUtil.extractSimple(cls, item);
|
|
625
|
-
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
|
|
626
|
-
(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}, {});
|
|
670
|
+
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
671
|
+
if (value === null || value === undefined) {
|
|
672
|
+
(document.$unset ??= {})[key] = value;
|
|
673
|
+
} else {
|
|
674
|
+
(document.$set ??= {})[key] = value;
|
|
675
|
+
}
|
|
676
|
+
return document;
|
|
677
|
+
}, {});
|
|
634
678
|
|
|
635
679
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
636
680
|
const result = await col.updateMany(filter, castTo(final));
|
|
@@ -664,7 +708,7 @@ export class MongoModelService implements
|
|
|
664
708
|
}
|
|
665
709
|
];
|
|
666
710
|
|
|
667
|
-
const result = await col.aggregate<{ _id: ObjectId
|
|
711
|
+
const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
|
|
668
712
|
|
|
669
713
|
return result
|
|
670
714
|
.map(item => ({
|
|
@@ -675,18 +719,28 @@ export class MongoModelService implements
|
|
|
675
719
|
}
|
|
676
720
|
|
|
677
721
|
// Suggest
|
|
678
|
-
async suggestValuesByQuery<T extends ModelType>(
|
|
722
|
+
async suggestValuesByQuery<T extends ModelType>(
|
|
723
|
+
cls: Class<T>,
|
|
724
|
+
field: ValidStringFields<T>,
|
|
725
|
+
prefix?: string,
|
|
726
|
+
query?: PageableModelQuery<T>
|
|
727
|
+
): Promise<string[]> {
|
|
679
728
|
await QueryVerifier.verify(cls, query);
|
|
680
729
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
|
|
681
730
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
682
|
-
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results,
|
|
731
|
+
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, a => a, query?.limit);
|
|
683
732
|
}
|
|
684
733
|
|
|
685
|
-
async suggestByQuery<T extends ModelType>(
|
|
734
|
+
async suggestByQuery<T extends ModelType>(
|
|
735
|
+
cls: Class<T>,
|
|
736
|
+
field: ValidStringFields<T>,
|
|
737
|
+
prefix?: string,
|
|
738
|
+
query?: PageableModelQuery<T>
|
|
739
|
+
): Promise<T[]> {
|
|
686
740
|
await QueryVerifier.verify(cls, query);
|
|
687
741
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
688
742
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
689
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query
|
|
743
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
|
|
690
744
|
}
|
|
691
745
|
|
|
692
746
|
// Other
|
|
@@ -699,12 +753,14 @@ export class MongoModelService implements
|
|
|
699
753
|
search = { $search: search, $language: 'en' };
|
|
700
754
|
}
|
|
701
755
|
|
|
702
|
-
(query.sort ??= []).unshift(
|
|
703
|
-
|
|
704
|
-
|
|
756
|
+
(query.sort ??= []).unshift(
|
|
757
|
+
castTo<(typeof query.sort)[0]>({
|
|
758
|
+
score: { $meta: 'textScore' }
|
|
759
|
+
})
|
|
760
|
+
);
|
|
705
761
|
|
|
706
762
|
const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
|
|
707
763
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
708
764
|
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
709
765
|
}
|
|
710
|
-
}
|
|
766
|
+
}
|
package/support/service.mongo.ts
CHANGED