@travetto/model-mongo 7.0.0-rc.0 → 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 +55 -55
- package/src/service.ts +59 -62
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,
|
|
83
|
-
const fields = base ? SchemaRegistryIndex.
|
|
82
|
+
static extractSimple<T>(base: Class<T> | undefined, item: Record<string, unknown>, path: string = '', recursive: boolean = true): Record<string, unknown> {
|
|
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
|
}
|
|
@@ -279,10 +279,7 @@ export class MongoModelService implements
|
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
281
|
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
282
|
-
const writeStream = this.#bucket.openUploadStream(location, {
|
|
283
|
-
contentType: blobMeta.contentType,
|
|
284
|
-
metadata: blobMeta,
|
|
285
|
-
});
|
|
282
|
+
const writeStream = this.#bucket.openUploadStream(location, { metadata: blobMeta });
|
|
286
283
|
await pipeline(stream, writeStream);
|
|
287
284
|
|
|
288
285
|
if (existing) {
|
|
@@ -316,7 +313,7 @@ export class MongoModelService implements
|
|
|
316
313
|
}
|
|
317
314
|
|
|
318
315
|
// Bulk
|
|
319
|
-
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 }>> {
|
|
320
317
|
const out: BulkResponse<{ index: number }> = {
|
|
321
318
|
errors: [],
|
|
322
319
|
counts: {
|
|
@@ -339,26 +336,26 @@ export class MongoModelService implements
|
|
|
339
336
|
|
|
340
337
|
out.insertedIds = new Map([...upsertedIds.entries(), ...insertedIds.entries()]);
|
|
341
338
|
|
|
342
|
-
for (const
|
|
343
|
-
if (
|
|
344
|
-
this.preUpdate(
|
|
345
|
-
bulk.insert(
|
|
346
|
-
} else if (
|
|
347
|
-
const id = this.preUpdate(
|
|
348
|
-
bulk.find({ _id: MongoUtil.uuid(id!) }).upsert().updateOne({ $set:
|
|
349
|
-
} else if (
|
|
350
|
-
const id = this.preUpdate(
|
|
351
|
-
bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set:
|
|
352
|
-
} else if (
|
|
353
|
-
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();
|
|
354
351
|
}
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
const result = await bulk.execute({});
|
|
358
355
|
|
|
359
356
|
// Restore all ids
|
|
360
|
-
for (const
|
|
361
|
-
const core =
|
|
357
|
+
for (const operation of operations) {
|
|
358
|
+
const core = operation.insert ?? operation.upsert ?? operation.update;
|
|
362
359
|
if (core) {
|
|
363
360
|
this.postUpdate(asFull(core));
|
|
364
361
|
}
|
|
@@ -370,17 +367,17 @@ export class MongoModelService implements
|
|
|
370
367
|
|
|
371
368
|
if (out.counts) {
|
|
372
369
|
out.counts.delete = result.deletedCount;
|
|
373
|
-
out.counts.update = operations.filter(
|
|
370
|
+
out.counts.update = operations.filter(item => item.update).length;
|
|
374
371
|
out.counts.insert = result.insertedCount;
|
|
375
|
-
out.counts.upsert = operations.filter(
|
|
372
|
+
out.counts.upsert = operations.filter(item => item.upsert).length;
|
|
376
373
|
}
|
|
377
374
|
|
|
378
375
|
if (result.hasWriteErrors()) {
|
|
379
376
|
out.errors = result.getWriteErrors();
|
|
380
|
-
for (const
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
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;
|
|
384
381
|
}
|
|
385
382
|
out.counts.error = out.errors.length;
|
|
386
383
|
}
|
|
@@ -430,18 +427,18 @@ export class MongoModelService implements
|
|
|
430
427
|
|
|
431
428
|
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
432
429
|
const store = await this.getStore(cls);
|
|
433
|
-
const
|
|
430
|
+
const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
|
|
434
431
|
|
|
435
432
|
const where = this.getWhereFilter(
|
|
436
433
|
cls,
|
|
437
434
|
castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
|
|
438
435
|
);
|
|
439
436
|
|
|
440
|
-
const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(
|
|
437
|
+
const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(idxConfig)[ListIndexSymbol] ??= MongoUtil.getPlainIndex(idxConfig);
|
|
441
438
|
const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
|
|
442
439
|
|
|
443
|
-
for await (const
|
|
444
|
-
yield await this.postLoad(cls,
|
|
440
|
+
for await (const item of cursor) {
|
|
441
|
+
yield await this.postLoad(cls, item);
|
|
445
442
|
}
|
|
446
443
|
}
|
|
447
444
|
|
|
@@ -453,7 +450,7 @@ export class MongoModelService implements
|
|
|
453
450
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
454
451
|
const cursor = col.find(filter, {});
|
|
455
452
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
456
|
-
return await Promise.all(items.map(
|
|
453
|
+
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
457
454
|
}
|
|
458
455
|
|
|
459
456
|
async queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
|
|
@@ -503,13 +500,13 @@ export class MongoModelService implements
|
|
|
503
500
|
const col = await this.getStore(cls);
|
|
504
501
|
const items = MongoUtil.extractSimple(cls, item);
|
|
505
502
|
const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
|
|
506
|
-
(
|
|
507
|
-
if (
|
|
508
|
-
(
|
|
503
|
+
(document, [key, value]) => {
|
|
504
|
+
if (value === null || value === undefined) {
|
|
505
|
+
(document.$unset ??= {})[key] = value;
|
|
509
506
|
} else {
|
|
510
|
-
(
|
|
507
|
+
(document.$set ??= {})[key] = value;
|
|
511
508
|
}
|
|
512
|
-
return
|
|
509
|
+
return document;
|
|
513
510
|
}, {});
|
|
514
511
|
|
|
515
512
|
const filter = MongoUtil.extractWhereFilter(cls, query.where);
|
|
@@ -526,14 +523,14 @@ export class MongoModelService implements
|
|
|
526
523
|
await QueryVerifier.verify(cls, query);
|
|
527
524
|
}
|
|
528
525
|
|
|
529
|
-
let
|
|
526
|
+
let queryObject: Record<string, unknown> = { [field]: { $exists: true } };
|
|
530
527
|
|
|
531
528
|
if (query?.where) {
|
|
532
|
-
|
|
529
|
+
queryObject = { $and: [queryObject, MongoUtil.extractWhereFilter(cls, query.where)] };
|
|
533
530
|
}
|
|
534
531
|
|
|
535
532
|
const aggregations: object[] = [
|
|
536
|
-
{ $match:
|
|
533
|
+
{ $match: queryObject },
|
|
537
534
|
{
|
|
538
535
|
$group: {
|
|
539
536
|
_id: `$${field}`,
|
|
@@ -547,9 +544,9 @@ export class MongoModelService implements
|
|
|
547
544
|
const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
|
|
548
545
|
|
|
549
546
|
return result
|
|
550
|
-
.map(
|
|
551
|
-
key: MongoUtil.idToString(
|
|
552
|
-
count:
|
|
547
|
+
.map(item => ({
|
|
548
|
+
key: MongoUtil.idToString(item._id),
|
|
549
|
+
count: item.count
|
|
553
550
|
}))
|
|
554
551
|
.toSorted((a, b) => b.count - a.count);
|
|
555
552
|
}
|
|
@@ -557,15 +554,15 @@ export class MongoModelService implements
|
|
|
557
554
|
// Suggest
|
|
558
555
|
async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
559
556
|
await QueryVerifier.verify(cls, query);
|
|
560
|
-
const
|
|
561
|
-
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);
|
|
562
559
|
return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, (a) => a, query && query.limit);
|
|
563
560
|
}
|
|
564
561
|
|
|
565
562
|
async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|
|
566
563
|
await QueryVerifier.verify(cls, query);
|
|
567
|
-
const
|
|
568
|
-
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);
|
|
569
566
|
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
|
|
570
567
|
}
|
|
571
568
|
|
|
@@ -585,6 +582,6 @@ export class MongoModelService implements
|
|
|
585
582
|
|
|
586
583
|
const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
|
|
587
584
|
const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
|
|
588
|
-
return await Promise.all(items.map(
|
|
585
|
+
return await Promise.all(items.map(item => this.postLoad(cls, item)));
|
|
589
586
|
}
|
|
590
587
|
}
|