@travetto/model-mongo 7.0.0-rc.1 → 7.0.0-rc.2
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 +19 -19
- package/package.json +5 -5
- package/src/config.ts +17 -17
- package/src/internal/util.ts +54 -54
- package/src/service.ts +58 -58
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ export class Init {
|
|
|
37
37
|
@InjectableFactory({
|
|
38
38
|
primary: true
|
|
39
39
|
})
|
|
40
|
-
static getModelSource(
|
|
41
|
-
return new MongoModelService(
|
|
40
|
+
static getModelSource(config: MongoModelConfig) {
|
|
41
|
+
return new MongoModelService(config);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
```
|
|
@@ -107,11 +107,11 @@ export class MongoModelConfig {
|
|
|
107
107
|
* Load all the ssl certs as needed
|
|
108
108
|
*/
|
|
109
109
|
async postConstruct(): Promise<void> {
|
|
110
|
-
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).
|
|
110
|
+
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).catch(() => file);
|
|
111
111
|
|
|
112
112
|
if (this.connectionString) {
|
|
113
113
|
const details = new URL(this.connectionString);
|
|
114
|
-
this.hosts ??= details.hostname.split(',').filter(
|
|
114
|
+
this.hosts ??= details.hostname.split(',').filter(host => !!host);
|
|
115
115
|
this.srvRecord ??= details.protocol === 'mongodb+srv:';
|
|
116
116
|
this.namespace ??= details.pathname.replace('/', '');
|
|
117
117
|
Object.assign(this.options, Object.fromEntries(details.searchParams.entries()));
|
|
@@ -131,24 +131,24 @@ export class MongoModelConfig {
|
|
|
131
131
|
this.hosts = ['localhost'];
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
if (
|
|
137
|
-
|
|
134
|
+
const options = this.options;
|
|
135
|
+
if (options.ssl) {
|
|
136
|
+
if (options.cert) {
|
|
137
|
+
options.cert = await Promise.all([options.cert].flat(2).map(data => Buffer.isBuffer(data) ? data : resolve(data)));
|
|
138
138
|
}
|
|
139
|
-
if (
|
|
140
|
-
|
|
139
|
+
if (options.tlsCertificateKeyFile) {
|
|
140
|
+
options.tlsCertificateKeyFile = await resolve(options.tlsCertificateKeyFile);
|
|
141
141
|
}
|
|
142
|
-
if (
|
|
143
|
-
|
|
142
|
+
if (options.tlsCAFile) {
|
|
143
|
+
options.tlsCAFile = await resolve(options.tlsCAFile);
|
|
144
144
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
145
|
+
if (options.tlsCRLFile) {
|
|
146
|
+
options.tlsCRLFile = await resolve(options.tlsCRLFile);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
if (!Runtime.production) {
|
|
151
|
-
|
|
151
|
+
options.waitQueueTimeoutMS ??= TimeUtil.asMillis(1, 'd'); // Wait a day in dev mode
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -157,14 +157,14 @@ export class MongoModelConfig {
|
|
|
157
157
|
*/
|
|
158
158
|
get url(): string {
|
|
159
159
|
const hosts = this.hosts!
|
|
160
|
-
.map(
|
|
160
|
+
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
161
161
|
.join(',');
|
|
162
|
-
const
|
|
162
|
+
const optionString = Object.entries(this.options).map(([key, value]) => `${key}=${value}`).join('&');
|
|
163
163
|
let creds = '';
|
|
164
164
|
if (this.username) {
|
|
165
|
-
creds = `${[this.username, this.password].filter(
|
|
165
|
+
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
166
166
|
}
|
|
167
|
-
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${
|
|
167
|
+
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
|
|
168
168
|
return url;
|
|
169
169
|
}
|
|
170
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Mongo backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mongo",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"directory": "module/model-mongo"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/model": "^7.0.0-rc.
|
|
31
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
28
|
+
"@travetto/cli": "^7.0.0-rc.2",
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/model": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/model-query": "^7.0.0-rc.2",
|
|
32
32
|
"mongodb": "^7.0.0"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
package/src/config.ts
CHANGED
|
@@ -66,11 +66,11 @@ export class MongoModelConfig {
|
|
|
66
66
|
* Load all the ssl certs as needed
|
|
67
67
|
*/
|
|
68
68
|
async postConstruct(): Promise<void> {
|
|
69
|
-
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).
|
|
69
|
+
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).catch(() => file);
|
|
70
70
|
|
|
71
71
|
if (this.connectionString) {
|
|
72
72
|
const details = new URL(this.connectionString);
|
|
73
|
-
this.hosts ??= details.hostname.split(',').filter(
|
|
73
|
+
this.hosts ??= details.hostname.split(',').filter(host => !!host);
|
|
74
74
|
this.srvRecord ??= details.protocol === 'mongodb+srv:';
|
|
75
75
|
this.namespace ??= details.pathname.replace('/', '');
|
|
76
76
|
Object.assign(this.options, Object.fromEntries(details.searchParams.entries()));
|
|
@@ -90,24 +90,24 @@ export class MongoModelConfig {
|
|
|
90
90
|
this.hosts = ['localhost'];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
if (
|
|
96
|
-
|
|
93
|
+
const options = this.options;
|
|
94
|
+
if (options.ssl) {
|
|
95
|
+
if (options.cert) {
|
|
96
|
+
options.cert = await Promise.all([options.cert].flat(2).map(data => Buffer.isBuffer(data) ? data : resolve(data)));
|
|
97
97
|
}
|
|
98
|
-
if (
|
|
99
|
-
|
|
98
|
+
if (options.tlsCertificateKeyFile) {
|
|
99
|
+
options.tlsCertificateKeyFile = await resolve(options.tlsCertificateKeyFile);
|
|
100
100
|
}
|
|
101
|
-
if (
|
|
102
|
-
|
|
101
|
+
if (options.tlsCAFile) {
|
|
102
|
+
options.tlsCAFile = await resolve(options.tlsCAFile);
|
|
103
103
|
}
|
|
104
|
-
if (
|
|
105
|
-
|
|
104
|
+
if (options.tlsCRLFile) {
|
|
105
|
+
options.tlsCRLFile = await resolve(options.tlsCRLFile);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (!Runtime.production) {
|
|
110
|
-
|
|
110
|
+
options.waitQueueTimeoutMS ??= TimeUtil.asMillis(1, 'd'); // Wait a day in dev mode
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -116,14 +116,14 @@ export class MongoModelConfig {
|
|
|
116
116
|
*/
|
|
117
117
|
get url(): string {
|
|
118
118
|
const hosts = this.hosts!
|
|
119
|
-
.map(
|
|
119
|
+
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
120
120
|
.join(',');
|
|
121
|
-
const
|
|
121
|
+
const optionString = Object.entries(this.options).map(([key, value]) => `${key}=${value}`).join('&');
|
|
122
122
|
let creds = '';
|
|
123
123
|
if (this.username) {
|
|
124
|
-
creds = `${[this.username, this.password].filter(
|
|
124
|
+
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
|
125
125
|
}
|
|
126
|
-
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${
|
|
126
|
+
const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
|
|
127
127
|
return url;
|
|
128
128
|
}
|
|
129
129
|
}
|
package/src/internal/util.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { type DistanceUnit, type PageableModelQuery, type WhereClause, ModelQuer
|
|
|
7
7
|
import type { ModelType, IndexField, IndexConfig } from '@travetto/model';
|
|
8
8
|
import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const PointConcrete = toConcrete<Point>();
|
|
11
11
|
|
|
12
|
-
type
|
|
12
|
+
type IdxConfig = CreateIndexesOptions;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Converting units to various radians
|
|
@@ -31,21 +31,21 @@ export type PlainIdx = Record<string, -1 | 0 | 1>;
|
|
|
31
31
|
*/
|
|
32
32
|
export class MongoUtil {
|
|
33
33
|
|
|
34
|
-
static toIndex<T extends ModelType>(
|
|
34
|
+
static toIndex<T extends ModelType>(field: IndexField<T>): PlainIdx {
|
|
35
35
|
const keys = [];
|
|
36
|
-
while (typeof
|
|
37
|
-
const key = TypedObject.keys(
|
|
38
|
-
|
|
36
|
+
while (typeof field !== 'number' && typeof field !== 'boolean' && Object.keys(field)) {
|
|
37
|
+
const key = TypedObject.keys(field)[0];
|
|
38
|
+
field = castTo(field[key]);
|
|
39
39
|
keys.push(key);
|
|
40
40
|
}
|
|
41
|
-
const rf: number | boolean = castTo(
|
|
41
|
+
const rf: number | boolean = castTo(field);
|
|
42
42
|
return {
|
|
43
43
|
[keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : castTo<-1 | 1 | 0>(rf)
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
static uuid(
|
|
48
|
-
return new Binary(Buffer.from(
|
|
47
|
+
static uuid(value: string): Binary {
|
|
48
|
+
return new Binary(Buffer.from(value.replaceAll('-', ''), 'hex'), Binary.SUBTYPE_UUID);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
static idToString(id: string | ObjectId | Binary): string {
|
|
@@ -66,84 +66,84 @@ export class MongoUtil {
|
|
|
66
66
|
/**
|
|
67
67
|
* Build mongo where clause
|
|
68
68
|
*/
|
|
69
|
-
static extractWhereClause<T>(cls: Class<T>,
|
|
70
|
-
if (ModelQueryUtil.has$And(
|
|
71
|
-
return { $and:
|
|
72
|
-
} else if (ModelQueryUtil.has$Or(
|
|
73
|
-
return { $or:
|
|
74
|
-
} else if (ModelQueryUtil.has$Not(
|
|
75
|
-
return { $nor: [this.extractWhereClause<T>(cls,
|
|
69
|
+
static extractWhereClause<T>(cls: Class<T>, clause: WhereClause<T>): Record<string, unknown> {
|
|
70
|
+
if (ModelQueryUtil.has$And(clause)) {
|
|
71
|
+
return { $and: clause.$and.map(item => this.extractWhereClause<T>(cls, item)) };
|
|
72
|
+
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
73
|
+
return { $or: clause.$or.map(item => this.extractWhereClause<T>(cls, item)) };
|
|
74
|
+
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
75
|
+
return { $nor: [this.extractWhereClause<T>(cls, clause.$not)] };
|
|
76
76
|
} else {
|
|
77
|
-
return this.extractSimple(cls,
|
|
77
|
+
return this.extractSimple(cls, clause);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**/
|
|
82
|
-
static extractSimple<T>(base: Class<T> | undefined,
|
|
82
|
+
static extractSimple<T>(base: Class<T> | undefined, item: Record<string, unknown>, path: string = '', recursive: boolean = true): Record<string, unknown> {
|
|
83
83
|
const fields = base ? SchemaRegistryIndex.getOptional(base)?.getFields() : undefined;
|
|
84
84
|
const out: Record<string, unknown> = {};
|
|
85
|
-
const sub =
|
|
85
|
+
const sub = item;
|
|
86
86
|
const keys = Object.keys(sub);
|
|
87
87
|
for (const key of keys) {
|
|
88
88
|
const subpath = `${path}${key}`;
|
|
89
|
-
const
|
|
89
|
+
const value: Record<string, unknown> = castTo(sub[key]);
|
|
90
90
|
const subField = fields?.[key];
|
|
91
91
|
|
|
92
|
-
const isPlain =
|
|
93
|
-
const firstKey = isPlain ? Object.keys(
|
|
92
|
+
const isPlain = value && DataUtil.isPlainObject(value);
|
|
93
|
+
const firstKey = isPlain ? Object.keys(value)[0] : '';
|
|
94
94
|
|
|
95
95
|
if (subpath === 'id') {
|
|
96
96
|
if (!firstKey) {
|
|
97
|
-
out._id = Array.isArray(
|
|
97
|
+
out._id = Array.isArray(value) ? value.map(subValue => this.uuid(subValue)) : this.uuid(`${value}`);
|
|
98
98
|
} else if (firstKey === '$in' || firstKey === '$nin' || firstKey === '$eq' || firstKey === '$ne') {
|
|
99
|
-
const temp =
|
|
100
|
-
out._id = { [firstKey]: Array.isArray(temp) ? temp.map(
|
|
99
|
+
const temp = value[firstKey];
|
|
100
|
+
out._id = { [firstKey]: Array.isArray(temp) ? temp.map(subValue => this.uuid(subValue)) : this.uuid(`${temp}`) };
|
|
101
101
|
} else {
|
|
102
102
|
throw new AppError('Invalid id query');
|
|
103
103
|
}
|
|
104
|
-
} else if ((isPlain && !firstKey.startsWith('$')) ||
|
|
104
|
+
} else if ((isPlain && !firstKey.startsWith('$')) || value?.constructor?.Ⲑid) {
|
|
105
105
|
if (recursive) {
|
|
106
|
-
Object.assign(out, this.extractSimple(subField?.type,
|
|
106
|
+
Object.assign(out, this.extractSimple(subField?.type, value, `${subpath}.`, recursive));
|
|
107
107
|
} else {
|
|
108
|
-
out[subpath] =
|
|
108
|
+
out[subpath] = value;
|
|
109
109
|
}
|
|
110
110
|
} else {
|
|
111
111
|
if (firstKey === '$gt' || firstKey === '$lt' || firstKey === '$gte' || firstKey === '$lte') {
|
|
112
|
-
for (const [sk, sv] of Object.entries(
|
|
113
|
-
|
|
112
|
+
for (const [sk, sv] of Object.entries(value)) {
|
|
113
|
+
value[sk] = ModelQueryUtil.resolveComparator(sv);
|
|
114
114
|
}
|
|
115
115
|
} else if (firstKey === '$exists' && subField?.array) {
|
|
116
|
-
const exists =
|
|
116
|
+
const exists = value.$exists;
|
|
117
117
|
if (!exists) {
|
|
118
|
-
delete
|
|
119
|
-
|
|
118
|
+
delete value.$exists;
|
|
119
|
+
value.$in = [null, []];
|
|
120
120
|
} else {
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
value.$exists = true;
|
|
122
|
+
value.$nin = [null, []];
|
|
123
123
|
}
|
|
124
124
|
} else if (firstKey === '$regex') {
|
|
125
|
-
|
|
126
|
-
} else if (firstKey && '$near' in
|
|
127
|
-
const dist: number = castTo(
|
|
128
|
-
const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(
|
|
129
|
-
|
|
130
|
-
delete
|
|
131
|
-
} else if (firstKey && '$geoWithin' in
|
|
132
|
-
const coords: [number, number][] = castTo(
|
|
125
|
+
value.$regex = DataUtil.toRegex(castTo(value.$regex));
|
|
126
|
+
} else if (firstKey && '$near' in value) {
|
|
127
|
+
const dist: number = castTo(value.$maxDistance);
|
|
128
|
+
const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(value.$unit) ?? 'km')];
|
|
129
|
+
value.$maxDistance = distance;
|
|
130
|
+
delete value.$unit;
|
|
131
|
+
} else if (firstKey && '$geoWithin' in value) {
|
|
132
|
+
const coords: [number, number][] = castTo(value.$geoWithin);
|
|
133
133
|
const first = coords[0];
|
|
134
134
|
const last = coords.at(-1)!;
|
|
135
135
|
// Connect if not
|
|
136
136
|
if (first[0] !== last[0] || first[1] !== last[1]) {
|
|
137
137
|
coords.push(first);
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
value.$geoWithin = {
|
|
140
140
|
$geometry: {
|
|
141
141
|
type: 'Polygon',
|
|
142
142
|
coordinates: [coords]
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
|
-
out[subpath === 'id' ? '_id' : subpath] =
|
|
146
|
+
out[subpath === 'id' ? '_id' : subpath] = value;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
return out;
|
|
@@ -153,11 +153,11 @@ export class MongoUtil {
|
|
|
153
153
|
const out: BasicIdx[] = [];
|
|
154
154
|
const textFields: string[] = [];
|
|
155
155
|
SchemaRegistryIndex.visitFields(cls, (field, path) => {
|
|
156
|
-
if (field.type ===
|
|
157
|
-
const name = [...path, field].map(
|
|
156
|
+
if (field.type === PointConcrete) {
|
|
157
|
+
const name = [...path, field].map(schema => schema.name).join('.');
|
|
158
158
|
out.push({ [name]: '2d' });
|
|
159
159
|
} else if (field.specifiers?.includes('text') && (field.specifiers?.includes('long') || field.specifiers.includes('search'))) {
|
|
160
|
-
const name = [...path, field].map(
|
|
160
|
+
const name = [...path, field].map(schema => schema.name).join('.');
|
|
161
161
|
textFields.push(name);
|
|
162
162
|
}
|
|
163
163
|
});
|
|
@@ -173,17 +173,17 @@ export class MongoUtil {
|
|
|
173
173
|
|
|
174
174
|
static getPlainIndex(idx: IndexConfig<ModelType>): PlainIdx {
|
|
175
175
|
let out: PlainIdx = {};
|
|
176
|
-
for (const
|
|
177
|
-
out = Object.assign(out,
|
|
176
|
+
for (const config of idx.fields.map(value => this.toIndex(value))) {
|
|
177
|
+
out = Object.assign(out, config);
|
|
178
178
|
}
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx,
|
|
182
|
+
static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx, IdxConfig][] {
|
|
183
183
|
return [
|
|
184
184
|
...indices.map(idx => [this.getPlainIndex(idx), (idx.type === 'unique' ? { unique: true } : {})] as const),
|
|
185
|
-
...this.getExtraIndices(cls).map((
|
|
186
|
-
].map(
|
|
185
|
+
...this.getExtraIndices(cls).map((idx) => [idx, {}] as const)
|
|
186
|
+
].map(idx => [...idx]);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
static prepareCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor<T | MongoWithId<T>>, query: PageableModelQuery<T>): FindCursor<T> {
|
|
@@ -201,7 +201,7 @@ export class MongoUtil {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
if (query.sort) {
|
|
204
|
-
cursor = cursor.sort(Object.assign({}, ...query.sort.map(
|
|
204
|
+
cursor = cursor.sort(Object.assign({}, ...query.sort.map(item => this.extractSimple(cls, item))));
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
cursor = cursor.limit(Math.trunc(query.limit ?? 200));
|
package/src/service.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
ModelRegistryIndex, ModelType, OptionalId, ModelCrudSupport, ModelStorageSupport,
|
|
11
|
-
ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport,
|
|
11
|
+
ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport, BulkOperation, BulkResponse,
|
|
12
12
|
NotFoundError, ExistsError, ModelBlobSupport,
|
|
13
13
|
ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil, ModelBlobUtil,
|
|
14
14
|
} from '@travetto/model';
|
|
@@ -131,8 +131,8 @@ export class MongoModelService implements
|
|
|
131
131
|
const creating = MongoUtil.getIndices(cls, ModelRegistryIndex.getConfig(cls).indices);
|
|
132
132
|
if (creating.length) {
|
|
133
133
|
console.debug('Creating indexes', { indices: creating });
|
|
134
|
-
for (const
|
|
135
|
-
await col.createIndex(...
|
|
134
|
+
for (const toCreate of creating) {
|
|
135
|
+
await col.createIndex(...toCreate);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -208,11 +208,11 @@ export class MongoModelService implements
|
|
|
208
208
|
{ $set: cleaned },
|
|
209
209
|
{ upsert: true }
|
|
210
210
|
);
|
|
211
|
-
} catch (
|
|
212
|
-
if (
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error instanceof Error && error.message.includes('duplicate key error')) {
|
|
213
213
|
throw new ExistsError(cls, id);
|
|
214
214
|
} else {
|
|
215
|
-
throw
|
|
215
|
+
throw error;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
return this.postUpdate(cleaned, id);
|
|
@@ -226,13 +226,13 @@ export class MongoModelService implements
|
|
|
226
226
|
|
|
227
227
|
const operation: Partial<T> = castTo(Object
|
|
228
228
|
.entries(simple)
|
|
229
|
-
.reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((
|
|
230
|
-
if (
|
|
231
|
-
(
|
|
229
|
+
.reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
|
|
230
|
+
if (value === null || value === undefined) {
|
|
231
|
+
(document.$unset ??= {})[key] = value;
|
|
232
232
|
} else {
|
|
233
|
-
(
|
|
233
|
+
(document.$set ??= {})[key] = value;
|
|
234
234
|
}
|
|
235
|
-
return
|
|
235
|
+
return document;
|
|
236
236
|
}, {}));
|
|
237
237
|
|
|
238
238
|
const id = item.id;
|
|
@@ -261,12 +261,12 @@ export class MongoModelService implements
|
|
|
261
261
|
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
262
262
|
const store = await this.getStore(cls);
|
|
263
263
|
const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true }).batchSize(100);
|
|
264
|
-
for await (const
|
|
264
|
+
for await (const item of cursor) {
|
|
265
265
|
try {
|
|
266
|
-
yield await this.postLoad(cls,
|
|
267
|
-
} catch (
|
|
268
|
-
if (!(
|
|
269
|
-
throw
|
|
266
|
+
yield await this.postLoad(cls, item);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!(error instanceof NotFoundError)) {
|
|
269
|
+
throw error;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
}
|
|
@@ -313,7 +313,7 @@ export class MongoModelService implements
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
// Bulk
|
|
316
|
-
async processBulk<T extends ModelType>(cls: Class<T>, operations:
|
|
316
|
+
async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOperation<T>[]): Promise<BulkResponse<{ index: number }>> {
|
|
317
317
|
const out: BulkResponse<{ index: number }> = {
|
|
318
318
|
errors: [],
|
|
319
319
|
counts: {
|
|
@@ -336,26 +336,26 @@ export class MongoModelService implements
|
|
|
336
336
|
|
|
337
337
|
out.insertedIds = new Map([...upsertedIds.entries(), ...insertedIds.entries()]);
|
|
338
338
|
|
|
339
|
-
for (const
|
|
340
|
-
if (
|
|
341
|
-
this.preUpdate(
|
|
342
|
-
bulk.insert(
|
|
343
|
-
} else if (
|
|
344
|
-
const id = this.preUpdate(
|
|
345
|
-
bulk.find({ _id: MongoUtil.uuid(id!) }).upsert().updateOne({ $set:
|
|
346
|
-
} else if (
|
|
347
|
-
const id = this.preUpdate(
|
|
348
|
-
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set:
|
|
349
|
-
} else if (
|
|
350
|
-
bulk.find({ _id: MongoUtil.uuid(
|
|
339
|
+
for (const operation of operations) {
|
|
340
|
+
if (operation.insert) {
|
|
341
|
+
this.preUpdate(operation.insert);
|
|
342
|
+
bulk.insert(operation.insert);
|
|
343
|
+
} else if (operation.upsert) {
|
|
344
|
+
const id = this.preUpdate(operation.upsert);
|
|
345
|
+
bulk.find({ _id: MongoUtil.uuid(id!) }).upsert().updateOne({ $set: operation.upsert });
|
|
346
|
+
} else if (operation.update) {
|
|
347
|
+
const id = this.preUpdate(operation.update);
|
|
348
|
+
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
|
|
349
|
+
} else if (operation.delete) {
|
|
350
|
+
bulk.find({ _id: MongoUtil.uuid(operation.delete.id) }).deleteOne();
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
const result = await bulk.execute({});
|
|
355
355
|
|
|
356
356
|
// Restore all ids
|
|
357
|
-
for (const
|
|
358
|
-
const core =
|
|
357
|
+
for (const operation of operations) {
|
|
358
|
+
const core = operation.insert ?? operation.upsert ?? operation.update;
|
|
359
359
|
if (core) {
|
|
360
360
|
this.postUpdate(asFull(core));
|
|
361
361
|
}
|
|
@@ -367,17 +367,17 @@ export class MongoModelService implements
|
|
|
367
367
|
|
|
368
368
|
if (out.counts) {
|
|
369
369
|
out.counts.delete = result.deletedCount;
|
|
370
|
-
out.counts.update = operations.filter(
|
|
370
|
+
out.counts.update = operations.filter(item => item.update).length;
|
|
371
371
|
out.counts.insert = result.insertedCount;
|
|
372
|
-
out.counts.upsert = operations.filter(
|
|
372
|
+
out.counts.upsert = operations.filter(item => item.upsert).length;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
if (result.hasWriteErrors()) {
|
|
376
376
|
out.errors = result.getWriteErrors();
|
|
377
|
-
for (const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
out.counts[
|
|
377
|
+
for (const error of out.errors) {
|
|
378
|
+
const operation = operations[error.index];
|
|
379
|
+
const key = TypedObject.keys(operation)[0];
|
|
380
|
+
out.counts[key] -= 1;
|
|
381
381
|
}
|
|
382
382
|
out.counts.error = out.errors.length;
|
|
383
383
|
}
|
|
@@ -427,18 +427,18 @@ export class MongoModelService implements
|
|
|
427
427
|
|
|
428
428
|
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
429
429
|
const store = await this.getStore(cls);
|
|
430
|
-
const
|
|
430
|
+
const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
431
431
|
|
|
432
432
|
const where = this.getWhereFilter(
|
|
433
433
|
cls,
|
|
434
434
|
castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
435
435
|
);
|
|
436
436
|
|
|
437
|
-
const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(
|
|
437
|
+
const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(idxConfig)[ListIndexSymbol] ??= MongoUtil.getPlainIndex(idxConfig);
|
|
438
438
|
const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
|
|
439
439
|
|
|
440
|
-
for await (const
|
|
441
|
-
yield await this.postLoad(cls,
|
|
440
|
+
for await (const item of cursor) {
|
|
441
|
+
yield await this.postLoad(cls, item);
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
@@ -450,7 +450,7 @@ export class MongoModelService implements
|
|
|
450
450
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
451
451
|
const cursor = col.find(filter, {});
|
|
452
452
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
453
|
-
return await Promise.all(items.map(
|
|
453
|
+
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
async queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
|
|
@@ -500,13 +500,13 @@ export class MongoModelService implements
|
|
|
500
500
|
const col = await this.getStore(cls);
|
|
501
501
|
const items = MongoUtil.extractSimple(cls, item);
|
|
502
502
|
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
|
|
503
|
-
(
|
|
504
|
-
if (
|
|
505
|
-
(
|
|
503
|
+
(document, [key, value]) => {
|
|
504
|
+
if (value === null || value === undefined) {
|
|
505
|
+
(document.$unset ??= {})[key] = value;
|
|
506
506
|
} else {
|
|
507
|
-
(
|
|
507
|
+
(document.$set ??= {})[key] = value;
|
|
508
508
|
}
|
|
509
|
-
return
|
|
509
|
+
return document;
|
|
510
510
|
}, {});
|
|
511
511
|
|
|
512
512
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
@@ -523,14 +523,14 @@ export class MongoModelService implements
|
|
|
523
523
|
await QueryVerifier.verify(cls, query);
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
let
|
|
526
|
+
let queryObject: Record<string, unknown> = { [field]: { $exists: true } };
|
|
527
527
|
|
|
528
528
|
if (query?.where) {
|
|
529
|
-
|
|
529
|
+
queryObject = { $and: [queryObject, MongoUtil.extractWhereFilter(cls, query.where)] };
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
const aggregations: object[] = [
|
|
533
|
-
{ $match:
|
|
533
|
+
{ $match: queryObject },
|
|
534
534
|
{
|
|
535
535
|
$group: {
|
|
536
536
|
_id: `$${field}`,
|
|
@@ -544,9 +544,9 @@ export class MongoModelService implements
|
|
|
544
544
|
const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
|
|
545
545
|
|
|
546
546
|
return result
|
|
547
|
-
.map(
|
|
548
|
-
key: MongoUtil.idToString(
|
|
549
|
-
count:
|
|
547
|
+
.map(item => ({
|
|
548
|
+
key: MongoUtil.idToString(item._id),
|
|
549
|
+
count: item.count
|
|
550
550
|
}))
|
|
551
551
|
.toSorted((a, b) => b.count - a.count);
|
|
552
552
|
}
|
|
@@ -554,15 +554,15 @@ export class MongoModelService implements
|
|
|
554
554
|
// Suggest
|
|
555
555
|
async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
556
556
|
await QueryVerifier.verify(cls, query);
|
|
557
|
-
const
|
|
558
|
-
const results = await this.query<T>(cls,
|
|
557
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
|
|
558
|
+
const results = await this.query<T>(cls, resolvedQuery);
|
|
559
559
|
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, (a) => a, query && query.limit);
|
|
560
560
|
}
|
|
561
561
|
|
|
562
562
|
async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|
|
563
563
|
await QueryVerifier.verify(cls, query);
|
|
564
|
-
const
|
|
565
|
-
const results = await this.query<T>(cls,
|
|
564
|
+
const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
|
|
565
|
+
const results = await this.query<T>(cls, resolvedQuery);
|
|
566
566
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
|
|
567
567
|
}
|
|
568
568
|
|
|
@@ -582,6 +582,6 @@ export class MongoModelService implements
|
|
|
582
582
|
|
|
583
583
|
const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
|
|
584
584
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
585
|
-
return await Promise.all(items.map(
|
|
585
|
+
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
586
586
|
}
|
|
587
587
|
}
|