@travetto/model-mongo 8.0.0-alpha.24 → 8.0.0-alpha.26
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 +39 -27
- package/src/service.ts +208 -141
- 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.26",
|
|
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.23",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.24",
|
|
31
|
+
"@travetto/model-indexed": "^8.0.0-alpha.26",
|
|
32
|
+
"@travetto/model-query": "^8.0.0-alpha.25",
|
|
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.29"
|
|
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));
|
|
@@ -253,10 +265,10 @@ export class MongoUtil {
|
|
|
253
265
|
const overlap = [...pendingKeySet.intersection(existingKeySet)];
|
|
254
266
|
changed ||= overlap.length !== pendingKeySet.size;
|
|
255
267
|
|
|
256
|
-
for (let i = 0; i < overlap.length && !changed; i
|
|
268
|
+
for (let i = 0; i < overlap.length && !changed; i += 1) {
|
|
257
269
|
changed ||= existingFields[overlap[i]] !== pendingKey[overlap[i]];
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
return changed;
|
|
261
273
|
}
|
|
262
|
-
}
|
|
274
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -1,40 +1,102 @@
|
|
|
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,
|
|
35
|
+
UniqueError
|
|
11
36
|
} from '@travetto/model';
|
|
12
37
|
import {
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
|
|
38
|
+
type FullKeyedIndexBody,
|
|
39
|
+
type FullKeyedIndexWithPartialBody,
|
|
40
|
+
type KeyedIndexBody,
|
|
41
|
+
type KeyedIndexSelection,
|
|
42
|
+
ModelIndexedComputedIndex,
|
|
43
|
+
type ModelIndexedSearchOptions,
|
|
44
|
+
type ModelIndexedSupport,
|
|
45
|
+
ModelIndexedUtil,
|
|
46
|
+
type ModelPageOptions,
|
|
47
|
+
type ModelPageResult,
|
|
48
|
+
type SingleItemIndex,
|
|
49
|
+
type SortedIndex,
|
|
50
|
+
type SortedIndexSelection,
|
|
51
|
+
type SortedIndexSelectionType
|
|
21
52
|
} from '@travetto/model-indexed';
|
|
22
|
-
|
|
23
53
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
type ModelQuery,
|
|
55
|
+
type ModelQueryCrudSupport,
|
|
56
|
+
ModelQueryCrudUtil,
|
|
57
|
+
type ModelQueryFacet,
|
|
58
|
+
type ModelQueryFacetSupport,
|
|
59
|
+
type ModelQuerySuggestSupport,
|
|
60
|
+
ModelQuerySuggestUtil,
|
|
61
|
+
type ModelQuerySupport,
|
|
62
|
+
ModelQueryUtil,
|
|
63
|
+
type PageableModelQuery,
|
|
64
|
+
QueryVerifier,
|
|
65
|
+
type ValidStringFields,
|
|
66
|
+
type WhereClause
|
|
67
|
+
} from '@travetto/model-query';
|
|
68
|
+
import {
|
|
69
|
+
asFull,
|
|
70
|
+
type BinaryMetadata,
|
|
71
|
+
BinaryMetadataUtil,
|
|
72
|
+
type BinaryType,
|
|
73
|
+
BinaryUtil,
|
|
74
|
+
type ByteRange,
|
|
75
|
+
type Class,
|
|
76
|
+
castTo,
|
|
77
|
+
JSONUtil,
|
|
78
|
+
ShutdownManager,
|
|
79
|
+
TypedObject
|
|
26
80
|
} from '@travetto/runtime';
|
|
27
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
28
81
|
|
|
29
|
-
import { MongoUtil, type WithId } from './internal/util.ts';
|
|
30
82
|
import type { MongoModelConfig } from './config.ts';
|
|
83
|
+
import { MongoUtil, type WithId } from './internal/util.ts';
|
|
31
84
|
|
|
32
85
|
type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
|
|
33
86
|
|
|
34
87
|
type MongoTextSearch = RootFilterOperators<unknown>['$text'];
|
|
35
88
|
|
|
36
|
-
const
|
|
37
|
-
error instanceof MongoServerError && error.message.includes('duplicate key error')
|
|
89
|
+
const handleDuplicateKeyError = (cls: Class, id: string, error: unknown): unknown => {
|
|
90
|
+
if (error instanceof MongoServerError && error.message.includes('duplicate key error')) {
|
|
91
|
+
if (error.message.includes('_id_') || (error.keyPattern && '_id' in error.keyPattern)) {
|
|
92
|
+
return new ExistsError(cls, id);
|
|
93
|
+
}
|
|
94
|
+
const match = error.message.match(/index: ([\w$]+)/);
|
|
95
|
+
const constraint = match ? match[1] : error.keyPattern ? Object.keys(error.keyPattern).join(',') : 'unique';
|
|
96
|
+
return new UniqueError(cls, constraint, { detail: error.message });
|
|
97
|
+
}
|
|
98
|
+
return error;
|
|
99
|
+
};
|
|
38
100
|
|
|
39
101
|
export const ModelBlobNamespace = '__blobs';
|
|
40
102
|
|
|
@@ -42,30 +104,41 @@ export const ModelBlobNamespace = '__blobs';
|
|
|
42
104
|
* Mongo-based model source
|
|
43
105
|
*/
|
|
44
106
|
@Injectable()
|
|
45
|
-
export class MongoModelService
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
107
|
+
export class MongoModelService
|
|
108
|
+
implements
|
|
109
|
+
ModelCrudSupport,
|
|
110
|
+
ModelStorageSupport,
|
|
111
|
+
ModelBulkSupport,
|
|
112
|
+
ModelBlobSupport,
|
|
113
|
+
ModelIndexedSupport,
|
|
114
|
+
ModelQuerySupport,
|
|
115
|
+
ModelQueryCrudSupport,
|
|
116
|
+
ModelQueryFacetSupport,
|
|
117
|
+
ModelQuerySuggestSupport,
|
|
118
|
+
ModelExpirySupport
|
|
119
|
+
{
|
|
52
120
|
#db: Db;
|
|
53
121
|
#bucket: GridFSBucket;
|
|
54
122
|
idSource = ModelCrudUtil.uuidSource();
|
|
55
123
|
client: MongoClient;
|
|
56
124
|
config: MongoModelConfig;
|
|
57
125
|
|
|
58
|
-
constructor(config: MongoModelConfig) {
|
|
126
|
+
constructor(config: MongoModelConfig) {
|
|
127
|
+
this.config = config;
|
|
128
|
+
}
|
|
59
129
|
|
|
60
|
-
async
|
|
130
|
+
async *#iterateCursor<T extends ModelType>(
|
|
131
|
+
cls: Class<T>,
|
|
132
|
+
cursor: FindCursor,
|
|
133
|
+
options?: ModelListOptions & ModelPageOptions<number>
|
|
134
|
+
): AsyncGenerator<T[]> {
|
|
61
135
|
const batchSize = options?.batchSizeHint ?? 100;
|
|
62
136
|
let batch: T[] = [];
|
|
63
137
|
const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
64
138
|
for await (const item of cursor
|
|
65
139
|
.batchSize(batchSize)
|
|
66
140
|
.limit(maxCount)
|
|
67
|
-
.skip(options?.offset ?? 0)
|
|
68
|
-
) {
|
|
141
|
+
.skip(options?.offset ?? 0)) {
|
|
69
142
|
if (options?.abort?.aborted) {
|
|
70
143
|
cursor.close();
|
|
71
144
|
break;
|
|
@@ -95,8 +168,7 @@ export class MongoModelService implements
|
|
|
95
168
|
}
|
|
96
169
|
|
|
97
170
|
const where = this.getWhereFilter(cls, whereClause);
|
|
98
|
-
let q = store.find(where, { timeout: true })
|
|
99
|
-
.batchSize(100);
|
|
171
|
+
let q = store.find(where, { timeout: true }).batchSize(100);
|
|
100
172
|
|
|
101
173
|
// TODO: We could cache this
|
|
102
174
|
if ('sort' in idx) {
|
|
@@ -105,7 +177,7 @@ export class MongoModelService implements
|
|
|
105
177
|
return q;
|
|
106
178
|
}
|
|
107
179
|
|
|
108
|
-
restoreId(item: { id?: string
|
|
180
|
+
restoreId(item: { id?: string; _id?: unknown }): void {
|
|
109
181
|
if (item._id) {
|
|
110
182
|
item.id ??= MongoUtil.idToString(castTo(item._id));
|
|
111
183
|
delete item._id;
|
|
@@ -125,11 +197,11 @@ export class MongoModelService implements
|
|
|
125
197
|
return item;
|
|
126
198
|
}
|
|
127
199
|
|
|
128
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
200
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: string }): string;
|
|
129
201
|
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
|
|
202
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: undefined }): undefined;
|
|
203
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id?: string }): string | undefined {
|
|
204
|
+
if (item?.id) {
|
|
133
205
|
const id = item.id;
|
|
134
206
|
item._id = MongoUtil.uuid(id);
|
|
135
207
|
if (!this.config.storeId) {
|
|
@@ -153,7 +225,7 @@ export class MongoModelService implements
|
|
|
153
225
|
async initializeClient(): Promise<void> {
|
|
154
226
|
this.client = await MongoClient.connect(this.config.url, {
|
|
155
227
|
...this.config.connectionOptions,
|
|
156
|
-
useBigInt64: true
|
|
228
|
+
useBigInt64: true
|
|
157
229
|
});
|
|
158
230
|
this.#db = this.client.db(this.config.namespace);
|
|
159
231
|
this.#bucket = new GridFSBucket(this.#db, {
|
|
@@ -174,7 +246,7 @@ export class MongoModelService implements
|
|
|
174
246
|
}
|
|
175
247
|
|
|
176
248
|
// Storage
|
|
177
|
-
async createStorage(): Promise<void> {
|
|
249
|
+
async createStorage(): Promise<void> {}
|
|
178
250
|
|
|
179
251
|
async deleteStorage(): Promise<void> {
|
|
180
252
|
await this.#db.dropDatabase();
|
|
@@ -182,10 +254,7 @@ export class MongoModelService implements
|
|
|
182
254
|
|
|
183
255
|
async upsertModel(cls: Class): Promise<void> {
|
|
184
256
|
const col = await this.getStore(cls);
|
|
185
|
-
const indices = [
|
|
186
|
-
...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)),
|
|
187
|
-
...MongoUtil.getExtraIndices(cls)
|
|
188
|
-
];
|
|
257
|
+
const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
|
|
189
258
|
const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
|
|
190
259
|
|
|
191
260
|
const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
|
|
@@ -219,7 +288,7 @@ export class MongoModelService implements
|
|
|
219
288
|
}
|
|
220
289
|
|
|
221
290
|
async truncateBlob(): Promise<void> {
|
|
222
|
-
await this.#bucket.drop().catch(() => {
|
|
291
|
+
await this.#bucket.drop().catch(() => {});
|
|
223
292
|
}
|
|
224
293
|
|
|
225
294
|
/**
|
|
@@ -254,7 +323,7 @@ export class MongoModelService implements
|
|
|
254
323
|
}
|
|
255
324
|
return this.postUpdate(cleaned, id);
|
|
256
325
|
} catch (error) {
|
|
257
|
-
throw
|
|
326
|
+
throw handleDuplicateKeyError(cls, id, error);
|
|
258
327
|
}
|
|
259
328
|
}
|
|
260
329
|
|
|
@@ -275,13 +344,9 @@ export class MongoModelService implements
|
|
|
275
344
|
const store = await this.getStore(cls);
|
|
276
345
|
|
|
277
346
|
try {
|
|
278
|
-
await store.updateOne(
|
|
279
|
-
this.getIdFilter(cls, id, false),
|
|
280
|
-
{ $set: cleaned },
|
|
281
|
-
{ upsert: true }
|
|
282
|
-
);
|
|
347
|
+
await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
|
|
283
348
|
} catch (error) {
|
|
284
|
-
throw
|
|
349
|
+
throw handleDuplicateKeyError(cls, id, error);
|
|
285
350
|
}
|
|
286
351
|
return this.postUpdate(cleaned, id);
|
|
287
352
|
}
|
|
@@ -292,24 +357,23 @@ export class MongoModelService implements
|
|
|
292
357
|
const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
|
|
293
358
|
const simple = MongoUtil.extractSimple(cls, final, undefined, false);
|
|
294
359
|
|
|
295
|
-
const operation: Partial<T> = castTo(
|
|
296
|
-
.entries(simple)
|
|
297
|
-
.reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
360
|
+
const operation: Partial<T> = castTo(
|
|
361
|
+
Object.entries(simple).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
298
362
|
if (value === null || value === undefined) {
|
|
299
363
|
(document.$unset ??= {})[key] = value;
|
|
300
364
|
} else {
|
|
301
365
|
(document.$set ??= {})[key] = value;
|
|
302
366
|
}
|
|
303
367
|
return document;
|
|
304
|
-
}, {})
|
|
368
|
+
}, {})
|
|
369
|
+
);
|
|
305
370
|
|
|
306
371
|
const id = item.id;
|
|
307
372
|
|
|
308
|
-
const result = await store.findOneAndUpdate(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
373
|
+
const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
|
|
374
|
+
returnDocument: 'after',
|
|
375
|
+
includeResultMetadata: true
|
|
376
|
+
});
|
|
313
377
|
|
|
314
378
|
if (!result.value) {
|
|
315
379
|
throw new NotFoundError(cls, id);
|
|
@@ -326,7 +390,7 @@ export class MongoModelService implements
|
|
|
326
390
|
}
|
|
327
391
|
}
|
|
328
392
|
|
|
329
|
-
async *
|
|
393
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
330
394
|
const store = await this.getStore(cls);
|
|
331
395
|
const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
|
|
332
396
|
yield* this.#iterateCursor(cls, cursor, options);
|
|
@@ -334,7 +398,10 @@ export class MongoModelService implements
|
|
|
334
398
|
|
|
335
399
|
// Blob
|
|
336
400
|
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
337
|
-
const existing = await this.getBlobMetadata(location).then(
|
|
401
|
+
const existing = await this.getBlobMetadata(location).then(
|
|
402
|
+
() => true,
|
|
403
|
+
() => false
|
|
404
|
+
);
|
|
338
405
|
if (!overwrite && existing) {
|
|
339
406
|
return;
|
|
340
407
|
}
|
|
@@ -366,10 +433,9 @@ export class MongoModelService implements
|
|
|
366
433
|
}
|
|
367
434
|
|
|
368
435
|
async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
|
|
369
|
-
await this.#db
|
|
370
|
-
{
|
|
371
|
-
{ $set: { metadata, contentType: metadata.contentType! } }
|
|
372
|
-
);
|
|
436
|
+
await this.#db
|
|
437
|
+
.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
|
|
438
|
+
.findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
|
|
373
439
|
}
|
|
374
440
|
|
|
375
441
|
// Bulk
|
|
@@ -402,7 +468,10 @@ export class MongoModelService implements
|
|
|
402
468
|
bulk.insert(operation.insert);
|
|
403
469
|
} else if (operation.upsert) {
|
|
404
470
|
const id = this.preUpdate(operation.upsert);
|
|
405
|
-
bulk
|
|
471
|
+
bulk
|
|
472
|
+
.find({ _id: MongoUtil.uuid(id!) })
|
|
473
|
+
.upsert()
|
|
474
|
+
.updateOne({ $set: operation.upsert });
|
|
406
475
|
} else if (operation.update) {
|
|
407
476
|
const id = this.preUpdate(operation.update);
|
|
408
477
|
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
|
|
@@ -451,89 +520,77 @@ export class MongoModelService implements
|
|
|
451
520
|
}
|
|
452
521
|
|
|
453
522
|
// Indexed
|
|
454
|
-
async getByIndex<
|
|
455
|
-
T
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
523
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
524
|
+
cls: Class<T>,
|
|
525
|
+
idx: SingleItemIndex<T, K, S>,
|
|
526
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
527
|
+
): Promise<T> {
|
|
459
528
|
const store = await this.getStore(cls);
|
|
460
529
|
|
|
461
530
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
462
531
|
|
|
463
|
-
const result = await store.findOne(
|
|
464
|
-
this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
|
|
465
|
-
);
|
|
532
|
+
const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
466
533
|
if (!result) {
|
|
467
534
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
468
535
|
}
|
|
469
536
|
return await this.postLoad(cls, result);
|
|
470
537
|
}
|
|
471
538
|
|
|
472
|
-
async deleteByIndex<
|
|
473
|
-
T
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
539
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
540
|
+
cls: Class<T>,
|
|
541
|
+
idx: SingleItemIndex<T, K, S>,
|
|
542
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
543
|
+
): Promise<void> {
|
|
477
544
|
const store = await this.getStore(cls);
|
|
478
545
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
479
546
|
|
|
480
|
-
const result = await store.deleteOne(
|
|
481
|
-
this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
|
|
482
|
-
);
|
|
547
|
+
const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
483
548
|
if (!result.deletedCount) {
|
|
484
549
|
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
485
550
|
}
|
|
486
551
|
}
|
|
487
552
|
|
|
488
|
-
upsertByIndex<
|
|
489
|
-
T
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
553
|
+
upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
554
|
+
cls: Class<T>,
|
|
555
|
+
idx: SingleItemIndex<T, K, S>,
|
|
556
|
+
body: OptionalId<T>
|
|
557
|
+
): Promise<T> {
|
|
493
558
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
494
559
|
}
|
|
495
560
|
|
|
496
|
-
updateByIndex<
|
|
497
|
-
T
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
561
|
+
updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
562
|
+
cls: Class<T>,
|
|
563
|
+
idx: SingleItemIndex<T, K, S>,
|
|
564
|
+
body: T
|
|
565
|
+
): Promise<T> {
|
|
501
566
|
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
502
567
|
}
|
|
503
568
|
|
|
504
|
-
async updatePartialByIndex<
|
|
505
|
-
T
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
569
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
570
|
+
cls: Class<T>,
|
|
571
|
+
idx: SingleItemIndex<T, K>,
|
|
572
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
573
|
+
): Promise<T> {
|
|
509
574
|
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
510
575
|
return this.update(cls, item);
|
|
511
576
|
}
|
|
512
577
|
|
|
513
|
-
async pageByIndex<
|
|
514
|
-
T extends ModelType,
|
|
515
|
-
K extends KeyedIndexSelection<T>,
|
|
516
|
-
S extends SortedIndexSelection<T>
|
|
517
|
-
>(
|
|
578
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
518
579
|
cls: Class<T>,
|
|
519
580
|
idx: SortedIndex<T, K, S>,
|
|
520
581
|
body: KeyedIndexBody<T, K>,
|
|
521
|
-
options?: ModelPageOptions
|
|
582
|
+
options?: ModelPageOptions
|
|
522
583
|
): Promise<ModelPageResult<T>> {
|
|
523
584
|
{
|
|
524
585
|
const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
|
|
525
|
-
const cursor =
|
|
586
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body);
|
|
526
587
|
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
|
|
527
588
|
const items = batches.flat();
|
|
528
589
|
return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
|
|
529
590
|
}
|
|
530
591
|
}
|
|
531
592
|
|
|
532
|
-
async *
|
|
533
|
-
T extends ModelType,
|
|
534
|
-
K extends KeyedIndexSelection<T>,
|
|
535
|
-
S extends SortedIndexSelection<T>
|
|
536
|
-
>(
|
|
593
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
537
594
|
cls: Class<T>,
|
|
538
595
|
idx: SortedIndex<T, K, S>,
|
|
539
596
|
body: KeyedIndexBody<T, K>,
|
|
@@ -548,18 +605,17 @@ export class MongoModelService implements
|
|
|
548
605
|
S extends SortedIndexSelection<T>,
|
|
549
606
|
K extends KeyedIndexSelection<T>,
|
|
550
607
|
B extends SortedIndexSelectionType<T, S> & string
|
|
551
|
-
>(
|
|
552
|
-
cls
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
})));
|
|
608
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
609
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body, where =>
|
|
610
|
+
castTo({
|
|
611
|
+
$and: [
|
|
612
|
+
where,
|
|
613
|
+
{
|
|
614
|
+
[idx.sortTemplate[0].path.join('.')]: ModelIndexedUtil.getSuggestRegex(prefix)
|
|
615
|
+
}
|
|
616
|
+
]
|
|
617
|
+
})
|
|
618
|
+
);
|
|
563
619
|
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
|
|
564
620
|
|
|
565
621
|
return batches.flat();
|
|
@@ -622,15 +678,14 @@ export class MongoModelService implements
|
|
|
622
678
|
const item = await ModelCrudUtil.prePartialUpdate(cls, data);
|
|
623
679
|
const col = await this.getStore(cls);
|
|
624
680
|
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
|
-
}, {});
|
|
681
|
+
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
682
|
+
if (value === null || value === undefined) {
|
|
683
|
+
(document.$unset ??= {})[key] = value;
|
|
684
|
+
} else {
|
|
685
|
+
(document.$set ??= {})[key] = value;
|
|
686
|
+
}
|
|
687
|
+
return document;
|
|
688
|
+
}, {});
|
|
634
689
|
|
|
635
690
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
636
691
|
const result = await col.updateMany(filter, castTo(final));
|
|
@@ -664,7 +719,7 @@ export class MongoModelService implements
|
|
|
664
719
|
}
|
|
665
720
|
];
|
|
666
721
|
|
|
667
|
-
const result = await col.aggregate<{ _id: ObjectId
|
|
722
|
+
const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
|
|
668
723
|
|
|
669
724
|
return result
|
|
670
725
|
.map(item => ({
|
|
@@ -675,18 +730,28 @@ export class MongoModelService implements
|
|
|
675
730
|
}
|
|
676
731
|
|
|
677
732
|
// Suggest
|
|
678
|
-
async suggestValuesByQuery<T extends ModelType>(
|
|
733
|
+
async suggestValuesByQuery<T extends ModelType>(
|
|
734
|
+
cls: Class<T>,
|
|
735
|
+
field: ValidStringFields<T>,
|
|
736
|
+
prefix?: string,
|
|
737
|
+
query?: PageableModelQuery<T>
|
|
738
|
+
): Promise<string[]> {
|
|
679
739
|
await QueryVerifier.verify(cls, query);
|
|
680
740
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
|
|
681
741
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
682
|
-
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results,
|
|
742
|
+
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, a => a, query?.limit);
|
|
683
743
|
}
|
|
684
744
|
|
|
685
|
-
async suggestByQuery<T extends ModelType>(
|
|
745
|
+
async suggestByQuery<T extends ModelType>(
|
|
746
|
+
cls: Class<T>,
|
|
747
|
+
field: ValidStringFields<T>,
|
|
748
|
+
prefix?: string,
|
|
749
|
+
query?: PageableModelQuery<T>
|
|
750
|
+
): Promise<T[]> {
|
|
686
751
|
await QueryVerifier.verify(cls, query);
|
|
687
752
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
688
753
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
689
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query
|
|
754
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
|
|
690
755
|
}
|
|
691
756
|
|
|
692
757
|
// Other
|
|
@@ -699,12 +764,14 @@ export class MongoModelService implements
|
|
|
699
764
|
search = { $search: search, $language: 'en' };
|
|
700
765
|
}
|
|
701
766
|
|
|
702
|
-
(query.sort ??= []).unshift(
|
|
703
|
-
|
|
704
|
-
|
|
767
|
+
(query.sort ??= []).unshift(
|
|
768
|
+
castTo<(typeof query.sort)[0]>({
|
|
769
|
+
score: { $meta: 'textScore' }
|
|
770
|
+
})
|
|
771
|
+
);
|
|
705
772
|
|
|
706
773
|
const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
|
|
707
774
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
708
775
|
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
709
776
|
}
|
|
710
|
-
}
|
|
777
|
+
}
|
package/support/service.mongo.ts
CHANGED