@travetto/model-mongo 8.0.0-alpha.3 → 8.0.0-alpha.31
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 +16 -20
- package/__index__.ts +1 -1
- package/package.json +7 -6
- package/src/config.ts +11 -15
- package/src/internal/util.ts +70 -53
- package/src/service.ts +301 -125
- package/support/service.mongo.ts +9 -3
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
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11)
|
|
23
19
|
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
|
|
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,23 +154,20 @@ 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
|
-
|
|
167
|
-
let creds = '';
|
|
162
|
+
).toString();
|
|
163
|
+
let credentials = '';
|
|
168
164
|
if (this.username) {
|
|
169
|
-
|
|
165
|
+
credentials = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
170
166
|
}
|
|
171
|
-
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${
|
|
167
|
+
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${credentials}${hosts}/${this.namespace}?${optionString}`;
|
|
172
168
|
return url;
|
|
173
169
|
}
|
|
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.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mongo backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,13 +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-
|
|
32
|
-
"
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.26",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.27",
|
|
31
|
+
"@travetto/model-indexed": "^8.0.0-alpha.29",
|
|
32
|
+
"@travetto/model-query": "^8.0.0-alpha.30",
|
|
33
|
+
"mongodb": "^7.5.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.32"
|
|
36
37
|
},
|
|
37
38
|
"peerDependenciesMeta": {
|
|
38
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,20 +127,17 @@ 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
|
-
|
|
140
|
-
let creds = '';
|
|
135
|
+
).toString();
|
|
136
|
+
let credentials = '';
|
|
141
137
|
if (this.username) {
|
|
142
|
-
|
|
138
|
+
credentials = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
143
139
|
}
|
|
144
|
-
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${
|
|
140
|
+
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${credentials}${hosts}/${this.namespace}?${optionString}`;
|
|
145
141
|
return url;
|
|
146
142
|
}
|
|
147
|
-
}
|
|
143
|
+
}
|
package/src/internal/util.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
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 {
|
|
8
|
-
import type
|
|
9
|
-
import {
|
|
12
|
+
import { type IndexConfig, IndexNotSupported, type ModelType } from '@travetto/model';
|
|
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';
|
|
10
17
|
|
|
11
18
|
const PointConcrete = toConcrete<Point>();
|
|
12
19
|
|
|
13
|
-
type IdxConfig = CreateIndexesOptions;
|
|
14
|
-
|
|
15
20
|
/**
|
|
16
21
|
* Converting units to various radians
|
|
17
22
|
*/
|
|
@@ -25,38 +30,31 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
|
|
|
25
30
|
|
|
26
31
|
export type WithId<T, I = unknown> = T & { _id?: I };
|
|
27
32
|
export type BasicIdx = Record<string, IndexDirection>;
|
|
28
|
-
|
|
33
|
+
|
|
34
|
+
function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, 1 | -1> {
|
|
35
|
+
const out: Record<string, 1 | -1> = {};
|
|
36
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
37
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
38
|
+
if (typeof value === 'object' && value !== null) {
|
|
39
|
+
Object.assign(out, flattenKeys(castTo(value), path));
|
|
40
|
+
} else {
|
|
41
|
+
out[path] = typeof value === 'boolean' ? (value ? 1 : -1) : castTo<-1 | 1>(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
/**
|
|
31
48
|
* Basic mongo utils for conforming to the model module
|
|
32
49
|
*/
|
|
33
50
|
export class MongoUtil {
|
|
34
|
-
|
|
35
51
|
static namespaceIndex(cls: Class, name: string): string {
|
|
36
52
|
return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
static toIndex<T extends ModelType>(field: IndexField<T>): PlainIdx {
|
|
40
|
-
const keys = [];
|
|
41
|
-
while (typeof field !== 'number' && typeof field !== 'boolean' && Object.keys(field)) {
|
|
42
|
-
const key = TypedObject.keys(field)[0];
|
|
43
|
-
field = castTo(field[key]);
|
|
44
|
-
keys.push(key);
|
|
45
|
-
}
|
|
46
|
-
const rf: number | boolean = castTo(field);
|
|
47
|
-
return {
|
|
48
|
-
[keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : castTo<-1 | 1 | 0>(rf)
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
55
|
static uuid(value: string): Binary {
|
|
53
56
|
try {
|
|
54
|
-
return new Binary(
|
|
55
|
-
BinaryUtil.binaryArrayToUint8Array(
|
|
56
|
-
CodecUtil.fromHexString(value.replaceAll('-', ''))
|
|
57
|
-
),
|
|
58
|
-
Binary.SUBTYPE_UUID
|
|
59
|
-
);
|
|
57
|
+
return new Binary(BinaryUtil.binaryArrayToUint8Array(CodecUtil.fromHexString(value.replaceAll('-', ''))), Binary.SUBTYPE_UUID);
|
|
60
58
|
} catch (err) {
|
|
61
59
|
if (err instanceof RuntimeError && err.message === 'Invalid hex string') {
|
|
62
60
|
return null!;
|
|
@@ -96,7 +94,12 @@ export class MongoUtil {
|
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
/**/
|
|
99
|
-
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> {
|
|
100
103
|
const fields = base ? SchemaRegistryIndex.getOptional(base)?.getFields() : undefined;
|
|
101
104
|
const out: Record<string, unknown> = {};
|
|
102
105
|
const sub = item;
|
|
@@ -142,7 +145,7 @@ export class MongoUtil {
|
|
|
142
145
|
value.$regex = DataUtil.toRegex(castTo(value.$regex));
|
|
143
146
|
} else if (firstKey && '$near' in value) {
|
|
144
147
|
const dist: number = castTo(value.$maxDistance);
|
|
145
|
-
const distance = dist / RADIANS_TO[
|
|
148
|
+
const distance = dist / RADIANS_TO[castTo<DistanceUnit>(value.$unit) ?? 'km'];
|
|
146
149
|
value.$maxDistance = distance;
|
|
147
150
|
delete value.$unit;
|
|
148
151
|
} else if (firstKey && '$geoWithin' in value) {
|
|
@@ -166,8 +169,8 @@ export class MongoUtil {
|
|
|
166
169
|
return out;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx,
|
|
170
|
-
const out: [BasicIdx,
|
|
172
|
+
static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, CreateIndexesOptions][] {
|
|
173
|
+
const out: [BasicIdx, CreateIndexesOptions][] = [];
|
|
171
174
|
const textFields: string[] = [];
|
|
172
175
|
SchemaRegistryIndex.visitFields(cls, (field, path) => {
|
|
173
176
|
if (field.type === PointConcrete) {
|
|
@@ -185,30 +188,44 @@ export class MongoUtil {
|
|
|
185
188
|
return out;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
|
-
static
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
out =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
static getIndex(cls: Class, idx: IndexConfig): [BasicIdx, CreateIndexesOptions] {
|
|
192
|
+
const name = this.namespaceIndex(cls, idx.name);
|
|
193
|
+
if (isModelQueryIndex(idx)) {
|
|
194
|
+
const out = idx.fields.reduce(
|
|
195
|
+
(acc, field) => Object.assign(acc, { ...flattenKeys(castTo(field)) }),
|
|
196
|
+
castTo<Record<string, -1 | 0 | 1>>({})
|
|
197
|
+
);
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
return [out, { name, unique: !!idx.unique }];
|
|
200
|
+
} else if (isModelIndexedIndex(idx)) {
|
|
201
|
+
const filter = Object.fromEntries([
|
|
202
|
+
...idx.keyTemplate.map(({ path }) => [path.join('.'), 1]),
|
|
203
|
+
...idx.sortTemplate.map(({ path, value }) => [path.join('.'), value === -1 ? -1 : 1])
|
|
204
|
+
]);
|
|
205
|
+
switch (idx.type) {
|
|
206
|
+
case 'indexed:keyed':
|
|
207
|
+
return [filter, { name, unique: idx.unique }];
|
|
208
|
+
case 'indexed:sorted':
|
|
209
|
+
return [filter, { name }];
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
throw new IndexNotSupported(cls, idx);
|
|
213
|
+
}
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
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> {
|
|
204
221
|
if (query.select) {
|
|
205
222
|
const selectKey = Object.keys(query.select)[0];
|
|
206
223
|
const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
|
|
207
224
|
// Remove id if not explicitly defined, and selecting fields directly
|
|
208
|
-
if (!select
|
|
225
|
+
if (!select._id) {
|
|
209
226
|
const values = new Set([...Object.values(select)]);
|
|
210
227
|
if (values.has(1) || values.has(true)) {
|
|
211
|
-
select
|
|
228
|
+
select._id = false;
|
|
212
229
|
}
|
|
213
230
|
}
|
|
214
231
|
cursor.project(select);
|
|
@@ -236,9 +253,9 @@ export class MongoUtil {
|
|
|
236
253
|
existing.expireAfterSeconds !== pendingOptions.expireAfterSeconds ||
|
|
237
254
|
existing.bucketSize !== pendingOptions.bucketSize;
|
|
238
255
|
|
|
239
|
-
const existingFields = existing.textIndexVersion
|
|
240
|
-
Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
|
|
241
|
-
existing.key;
|
|
256
|
+
const existingFields = existing.textIndexVersion
|
|
257
|
+
? Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
|
|
258
|
+
: existing.key;
|
|
242
259
|
|
|
243
260
|
const pendingKeySet = new Set(Object.keys(pendingKey));
|
|
244
261
|
const existingKeySet = new Set(Object.keys(existingFields));
|
|
@@ -248,10 +265,10 @@ export class MongoUtil {
|
|
|
248
265
|
const overlap = [...pendingKeySet.intersection(existingKeySet)];
|
|
249
266
|
changed ||= overlap.length !== pendingKeySet.size;
|
|
250
267
|
|
|
251
|
-
for (let i = 0; i < overlap.length && !changed; i
|
|
268
|
+
for (let i = 0; i < overlap.length && !changed; i += 1) {
|
|
252
269
|
changed ||= existingFields[overlap[i]] !== pendingKey[overlap[i]];
|
|
253
270
|
}
|
|
254
271
|
|
|
255
272
|
return changed;
|
|
256
273
|
}
|
|
257
|
-
}
|
|
274
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -1,60 +1,183 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Binary,
|
|
3
|
-
type
|
|
4
|
-
type
|
|
3
|
+
type Collection,
|
|
4
|
+
type Db,
|
|
5
|
+
type Filter,
|
|
6
|
+
type FindCursor,
|
|
7
|
+
GridFSBucket,
|
|
8
|
+
type GridFSFile,
|
|
9
|
+
MongoClient,
|
|
10
|
+
MongoServerError,
|
|
5
11
|
type WithId as MongoWithId,
|
|
12
|
+
type ObjectId,
|
|
13
|
+
type RootFilterOperators
|
|
6
14
|
} from 'mongodb';
|
|
7
15
|
|
|
16
|
+
import { Injectable, PostConstruct } from '@travetto/di';
|
|
8
17
|
import {
|
|
9
|
-
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
36
|
} from '@travetto/model';
|
|
14
37
|
import {
|
|
15
|
-
type
|
|
16
|
-
type
|
|
17
|
-
|
|
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
|
|
52
|
+
} from '@travetto/model-indexed';
|
|
53
|
+
import {
|
|
54
|
+
type ModelQuery,
|
|
55
|
+
type ModelQueryCrudSupport,
|
|
56
|
+
ModelQueryCrudUtil,
|
|
18
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
|
|
19
67
|
} from '@travetto/model-query';
|
|
20
|
-
|
|
21
68
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
80
|
} from '@travetto/runtime';
|
|
25
|
-
import { Injectable, PostConstruct } from '@travetto/di';
|
|
26
81
|
|
|
27
|
-
import { MongoUtil, type PlainIdx, type WithId } from './internal/util.ts';
|
|
28
82
|
import type { MongoModelConfig } from './config.ts';
|
|
29
|
-
|
|
30
|
-
const ListIndexSymbol = Symbol();
|
|
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
|
|
|
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
|
+
};
|
|
100
|
+
|
|
36
101
|
export const ModelBlobNamespace = '__blobs';
|
|
37
102
|
|
|
38
103
|
/**
|
|
39
104
|
* Mongo-based model source
|
|
40
105
|
*/
|
|
41
106
|
@Injectable()
|
|
42
|
-
export class MongoModelService
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
{
|
|
49
120
|
#db: Db;
|
|
50
121
|
#bucket: GridFSBucket;
|
|
51
122
|
idSource = ModelCrudUtil.uuidSource();
|
|
52
123
|
client: MongoClient;
|
|
53
124
|
config: MongoModelConfig;
|
|
54
125
|
|
|
55
|
-
constructor(config: MongoModelConfig) {
|
|
126
|
+
constructor(config: MongoModelConfig) {
|
|
127
|
+
this.config = config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async *#iterateCursor<T extends ModelType>(
|
|
131
|
+
cls: Class<T>,
|
|
132
|
+
cursor: FindCursor,
|
|
133
|
+
options?: ModelListOptions & ModelPageOptions<number>
|
|
134
|
+
): AsyncGenerator<T[]> {
|
|
135
|
+
const batchSize = options?.batchSizeHint ?? 100;
|
|
136
|
+
let batch: T[] = [];
|
|
137
|
+
const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
138
|
+
for await (const item of cursor
|
|
139
|
+
.batchSize(batchSize)
|
|
140
|
+
.limit(maxCount)
|
|
141
|
+
.skip(options?.offset ?? 0)) {
|
|
142
|
+
if (options?.abort?.aborted) {
|
|
143
|
+
cursor.close();
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
batch.push(item);
|
|
147
|
+
if (batch.length >= batchSize) {
|
|
148
|
+
yield await ModelCrudUtil.filterOutNotFound(batch.map(i => this.postLoad(cls, i)));
|
|
149
|
+
batch = [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (batch.length) {
|
|
153
|
+
yield await ModelCrudUtil.filterOutNotFound(batch.map(i => this.postLoad(cls, i)));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
56
156
|
|
|
57
|
-
|
|
157
|
+
async #buildIndexQuery<T extends ModelType>(
|
|
158
|
+
cls: Class<T>,
|
|
159
|
+
idx: SortedIndex<T>,
|
|
160
|
+
body: KeyedIndexBody<T>,
|
|
161
|
+
transformWhere?: (where: WhereClause<T>) => WhereClause<T>
|
|
162
|
+
): Promise<FindCursor> {
|
|
163
|
+
const store = await this.getStore(cls);
|
|
164
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
165
|
+
let whereClause: WhereClause<T> = castTo(computed.project());
|
|
166
|
+
if (transformWhere) {
|
|
167
|
+
whereClause = transformWhere(whereClause);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const where = this.getWhereFilter(cls, whereClause);
|
|
171
|
+
let q = store.find(where, { timeout: true }).batchSize(100);
|
|
172
|
+
|
|
173
|
+
// TODO: We could cache this
|
|
174
|
+
if ('sort' in idx) {
|
|
175
|
+
q = q.sort(idx.sortTemplate.map(({ path, value }) => [path.join('.'), value] as const));
|
|
176
|
+
}
|
|
177
|
+
return q;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
restoreId(item: { id?: string; _id?: unknown }): void {
|
|
58
181
|
if (item._id) {
|
|
59
182
|
item.id ??= MongoUtil.idToString(castTo(item._id));
|
|
60
183
|
delete item._id;
|
|
@@ -74,11 +197,11 @@ export class MongoModelService implements
|
|
|
74
197
|
return item;
|
|
75
198
|
}
|
|
76
199
|
|
|
77
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
200
|
+
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: string }): string;
|
|
78
201
|
preUpdate<T extends OptionalId<ModelType>>(item: Omit<T, 'id'> & { _id?: Binary }): undefined;
|
|
79
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
80
|
-
preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary
|
|
81
|
-
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) {
|
|
82
205
|
const id = item.id;
|
|
83
206
|
item._id = MongoUtil.uuid(id);
|
|
84
207
|
if (!this.config.storeId) {
|
|
@@ -102,7 +225,7 @@ export class MongoModelService implements
|
|
|
102
225
|
async initializeClient(): Promise<void> {
|
|
103
226
|
this.client = await MongoClient.connect(this.config.url, {
|
|
104
227
|
...this.config.connectionOptions,
|
|
105
|
-
useBigInt64: true
|
|
228
|
+
useBigInt64: true
|
|
106
229
|
});
|
|
107
230
|
this.#db = this.client.db(this.config.namespace);
|
|
108
231
|
this.#bucket = new GridFSBucket(this.#db, {
|
|
@@ -123,7 +246,7 @@ export class MongoModelService implements
|
|
|
123
246
|
}
|
|
124
247
|
|
|
125
248
|
// Storage
|
|
126
|
-
async createStorage(): Promise<void> {
|
|
249
|
+
async createStorage(): Promise<void> {}
|
|
127
250
|
|
|
128
251
|
async deleteStorage(): Promise<void> {
|
|
129
252
|
await this.#db.dropDatabase();
|
|
@@ -131,7 +254,7 @@ export class MongoModelService implements
|
|
|
131
254
|
|
|
132
255
|
async upsertModel(cls: Class): Promise<void> {
|
|
133
256
|
const col = await this.getStore(cls);
|
|
134
|
-
const indices =
|
|
257
|
+
const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
|
|
135
258
|
const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
|
|
136
259
|
|
|
137
260
|
const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
|
|
@@ -165,7 +288,7 @@ export class MongoModelService implements
|
|
|
165
288
|
}
|
|
166
289
|
|
|
167
290
|
async truncateBlob(): Promise<void> {
|
|
168
|
-
await this.#bucket.drop().catch(() => {
|
|
291
|
+
await this.#bucket.drop().catch(() => {});
|
|
169
292
|
}
|
|
170
293
|
|
|
171
294
|
/**
|
|
@@ -193,11 +316,15 @@ export class MongoModelService implements
|
|
|
193
316
|
const id = this.preUpdate(cleaned);
|
|
194
317
|
|
|
195
318
|
const store = await this.getStore(cls);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
319
|
+
try {
|
|
320
|
+
const result = await store.insertOne(castTo(cleaned));
|
|
321
|
+
if (!result.insertedId) {
|
|
322
|
+
throw new ExistsError(cls, id);
|
|
323
|
+
}
|
|
324
|
+
return this.postUpdate(cleaned, id);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw handleDuplicateKeyError(cls, id, error);
|
|
199
327
|
}
|
|
200
|
-
return this.postUpdate(cleaned, id);
|
|
201
328
|
}
|
|
202
329
|
|
|
203
330
|
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
@@ -217,17 +344,9 @@ export class MongoModelService implements
|
|
|
217
344
|
const store = await this.getStore(cls);
|
|
218
345
|
|
|
219
346
|
try {
|
|
220
|
-
await store.updateOne(
|
|
221
|
-
this.getIdFilter(cls, id, false),
|
|
222
|
-
{ $set: cleaned },
|
|
223
|
-
{ upsert: true }
|
|
224
|
-
);
|
|
347
|
+
await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
|
|
225
348
|
} catch (error) {
|
|
226
|
-
|
|
227
|
-
throw new ExistsError(cls, id);
|
|
228
|
-
} else {
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
349
|
+
throw handleDuplicateKeyError(cls, id, error);
|
|
231
350
|
}
|
|
232
351
|
return this.postUpdate(cleaned, id);
|
|
233
352
|
}
|
|
@@ -238,24 +357,23 @@ export class MongoModelService implements
|
|
|
238
357
|
const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
|
|
239
358
|
const simple = MongoUtil.extractSimple(cls, final, undefined, false);
|
|
240
359
|
|
|
241
|
-
const operation: Partial<T> = castTo(
|
|
242
|
-
.entries(simple)
|
|
243
|
-
.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]) => {
|
|
244
362
|
if (value === null || value === undefined) {
|
|
245
363
|
(document.$unset ??= {})[key] = value;
|
|
246
364
|
} else {
|
|
247
365
|
(document.$set ??= {})[key] = value;
|
|
248
366
|
}
|
|
249
367
|
return document;
|
|
250
|
-
}, {})
|
|
368
|
+
}, {})
|
|
369
|
+
);
|
|
251
370
|
|
|
252
371
|
const id = item.id;
|
|
253
372
|
|
|
254
|
-
const result = await store.findOneAndUpdate(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
373
|
+
const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
|
|
374
|
+
returnDocument: 'after',
|
|
375
|
+
includeResultMetadata: true
|
|
376
|
+
});
|
|
259
377
|
|
|
260
378
|
if (!result.value) {
|
|
261
379
|
throw new NotFoundError(cls, id);
|
|
@@ -272,23 +390,18 @@ export class MongoModelService implements
|
|
|
272
390
|
}
|
|
273
391
|
}
|
|
274
392
|
|
|
275
|
-
async *
|
|
393
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
276
394
|
const store = await this.getStore(cls);
|
|
277
|
-
const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true })
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
yield await this.postLoad(cls, item);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
if (!(error instanceof NotFoundError)) {
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
395
|
+
const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
|
|
396
|
+
yield* this.#iterateCursor(cls, cursor, options);
|
|
287
397
|
}
|
|
288
398
|
|
|
289
399
|
// Blob
|
|
290
400
|
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
291
|
-
const existing = await this.getBlobMetadata(location).then(
|
|
401
|
+
const existing = await this.getBlobMetadata(location).then(
|
|
402
|
+
() => true,
|
|
403
|
+
() => false
|
|
404
|
+
);
|
|
292
405
|
if (!overwrite && existing) {
|
|
293
406
|
return;
|
|
294
407
|
}
|
|
@@ -320,10 +433,9 @@ export class MongoModelService implements
|
|
|
320
433
|
}
|
|
321
434
|
|
|
322
435
|
async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
|
|
323
|
-
await this.#db
|
|
324
|
-
{
|
|
325
|
-
{ $set: { metadata, contentType: metadata.contentType! } }
|
|
326
|
-
);
|
|
436
|
+
await this.#db
|
|
437
|
+
.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
|
|
438
|
+
.findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
|
|
327
439
|
}
|
|
328
440
|
|
|
329
441
|
// Bulk
|
|
@@ -356,7 +468,10 @@ export class MongoModelService implements
|
|
|
356
468
|
bulk.insert(operation.insert);
|
|
357
469
|
} else if (operation.upsert) {
|
|
358
470
|
const id = this.preUpdate(operation.upsert);
|
|
359
|
-
bulk
|
|
471
|
+
bulk
|
|
472
|
+
.find({ _id: MongoUtil.uuid(id!) })
|
|
473
|
+
.upsert()
|
|
474
|
+
.updateOne({ $set: operation.upsert });
|
|
360
475
|
} else if (operation.update) {
|
|
361
476
|
const id = this.preUpdate(operation.update);
|
|
362
477
|
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
|
|
@@ -405,55 +520,105 @@ export class MongoModelService implements
|
|
|
405
520
|
}
|
|
406
521
|
|
|
407
522
|
// Indexed
|
|
408
|
-
async getByIndex<T extends ModelType
|
|
409
|
-
|
|
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> {
|
|
410
528
|
const store = await this.getStore(cls);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
);
|
|
529
|
+
|
|
530
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
531
|
+
|
|
532
|
+
const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
417
533
|
if (!result) {
|
|
418
|
-
throw new NotFoundError(`${cls.name}: ${idx}`,
|
|
534
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
419
535
|
}
|
|
420
536
|
return await this.postLoad(cls, result);
|
|
421
537
|
}
|
|
422
538
|
|
|
423
|
-
async deleteByIndex<T extends ModelType
|
|
424
|
-
|
|
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> {
|
|
425
544
|
const store = await this.getStore(cls);
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
)
|
|
431
|
-
);
|
|
432
|
-
if (result.deletedCount) {
|
|
433
|
-
return;
|
|
545
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
546
|
+
|
|
547
|
+
const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
|
|
548
|
+
if (!result.deletedCount) {
|
|
549
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
434
550
|
}
|
|
435
|
-
throw new NotFoundError(`${cls.name}: ${idx}`, key);
|
|
436
551
|
}
|
|
437
552
|
|
|
438
|
-
|
|
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> {
|
|
439
558
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
440
559
|
}
|
|
441
560
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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> {
|
|
566
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
567
|
+
}
|
|
568
|
+
|
|
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> {
|
|
574
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
575
|
+
return this.update(cls, item);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
579
|
+
cls: Class<T>,
|
|
580
|
+
idx: SortedIndex<T, K, S>,
|
|
581
|
+
body: KeyedIndexBody<T, K>,
|
|
582
|
+
options?: ModelPageOptions
|
|
583
|
+
): Promise<ModelPageResult<T>> {
|
|
584
|
+
{
|
|
585
|
+
const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
|
|
586
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body);
|
|
587
|
+
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
|
|
588
|
+
const items = batches.flat();
|
|
589
|
+
return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
445
592
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
593
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
594
|
+
cls: Class<T>,
|
|
595
|
+
idx: SortedIndex<T, K, S>,
|
|
596
|
+
body: KeyedIndexBody<T, K>,
|
|
597
|
+
options?: ModelListOptions
|
|
598
|
+
): AsyncIterable<T[]> {
|
|
599
|
+
const cursor = await this.#buildIndexQuery(cls, idx, body);
|
|
600
|
+
yield* this.#iterateCursor(cls, cursor, options);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async suggestByIndex<
|
|
604
|
+
T extends ModelType,
|
|
605
|
+
S extends SortedIndexSelection<T>,
|
|
606
|
+
K extends KeyedIndexSelection<T>,
|
|
607
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
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
|
+
})
|
|
449
618
|
);
|
|
619
|
+
const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
|
|
450
620
|
|
|
451
|
-
|
|
452
|
-
const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
|
|
453
|
-
|
|
454
|
-
for await (const item of cursor) {
|
|
455
|
-
yield await this.postLoad(cls, item);
|
|
456
|
-
}
|
|
621
|
+
return batches.flat();
|
|
457
622
|
}
|
|
458
623
|
|
|
459
624
|
// Query
|
|
@@ -513,15 +678,14 @@ export class MongoModelService implements
|
|
|
513
678
|
const item = await ModelCrudUtil.prePartialUpdate(cls, data);
|
|
514
679
|
const col = await this.getStore(cls);
|
|
515
680
|
const items = MongoUtil.extractSimple(cls, item);
|
|
516
|
-
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
|
|
517
|
-
(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}, {});
|
|
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
|
+
}, {});
|
|
525
689
|
|
|
526
690
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
527
691
|
const result = await col.updateMany(filter, castTo(final));
|
|
@@ -529,7 +693,7 @@ export class MongoModelService implements
|
|
|
529
693
|
}
|
|
530
694
|
|
|
531
695
|
// Facet
|
|
532
|
-
async
|
|
696
|
+
async facetByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
|
|
533
697
|
await QueryVerifier.verify(cls, query);
|
|
534
698
|
|
|
535
699
|
const col = await this.getStore(cls);
|
|
@@ -555,7 +719,7 @@ export class MongoModelService implements
|
|
|
555
719
|
}
|
|
556
720
|
];
|
|
557
721
|
|
|
558
|
-
const result = await col.aggregate<{ _id: ObjectId
|
|
722
|
+
const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
|
|
559
723
|
|
|
560
724
|
return result
|
|
561
725
|
.map(item => ({
|
|
@@ -566,18 +730,28 @@ export class MongoModelService implements
|
|
|
566
730
|
}
|
|
567
731
|
|
|
568
732
|
// Suggest
|
|
569
|
-
async
|
|
733
|
+
async suggestValuesByQuery<T extends ModelType>(
|
|
734
|
+
cls: Class<T>,
|
|
735
|
+
field: ValidStringFields<T>,
|
|
736
|
+
prefix?: string,
|
|
737
|
+
query?: PageableModelQuery<T>
|
|
738
|
+
): Promise<string[]> {
|
|
570
739
|
await QueryVerifier.verify(cls, query);
|
|
571
740
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
|
|
572
741
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
573
|
-
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results,
|
|
742
|
+
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, a => a, query?.limit);
|
|
574
743
|
}
|
|
575
744
|
|
|
576
|
-
async
|
|
745
|
+
async suggestByQuery<T extends ModelType>(
|
|
746
|
+
cls: Class<T>,
|
|
747
|
+
field: ValidStringFields<T>,
|
|
748
|
+
prefix?: string,
|
|
749
|
+
query?: PageableModelQuery<T>
|
|
750
|
+
): Promise<T[]> {
|
|
577
751
|
await QueryVerifier.verify(cls, query);
|
|
578
752
|
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
579
753
|
const results = await this.query<T>(cls, resolvedQuery);
|
|
580
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query
|
|
754
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
|
|
581
755
|
}
|
|
582
756
|
|
|
583
757
|
// Other
|
|
@@ -590,12 +764,14 @@ export class MongoModelService implements
|
|
|
590
764
|
search = { $search: search, $language: 'en' };
|
|
591
765
|
}
|
|
592
766
|
|
|
593
|
-
(query.sort ??= []).unshift(
|
|
594
|
-
|
|
595
|
-
|
|
767
|
+
(query.sort ??= []).unshift(
|
|
768
|
+
castTo<(typeof query.sort)[0]>({
|
|
769
|
+
score: { $meta: 'textScore' }
|
|
770
|
+
})
|
|
771
|
+
);
|
|
596
772
|
|
|
597
773
|
const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
|
|
598
774
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
599
775
|
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
600
776
|
}
|
|
601
|
-
}
|
|
777
|
+
}
|
package/support/service.mongo.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ServiceDescriptor } from '@travetto/cli';
|
|
2
2
|
|
|
3
|
-
const version = process.env.MONGO_VERSION || '8.
|
|
3
|
+
const version = process.env.MONGO_VERSION || '8.3';
|
|
4
|
+
|
|
5
|
+
/* cspell:words orbstack pthread rseq glibc TUNABLES */
|
|
4
6
|
|
|
5
7
|
export const service: ServiceDescriptor = {
|
|
6
8
|
name: 'mongodb',
|
|
7
9
|
version,
|
|
8
10
|
port: 27017,
|
|
9
|
-
image: `mongo:${version}
|
|
10
|
-
|
|
11
|
+
image: `mongo:${version}`,
|
|
12
|
+
env: {
|
|
13
|
+
// Temp until mongo image fixes orbstack issue
|
|
14
|
+
GLIBC_TUNABLES: 'glibc.pthread.rseq=1'
|
|
15
|
+
}
|
|
16
|
+
};
|