@travetto/model-mongo 7.1.3 → 8.0.0-alpha.0
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 +17 -10
- package/package.json +6 -6
- package/src/config.ts +30 -10
- package/src/internal/util.ts +16 -4
- package/src/service.ts +22 -20
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ export class Init {
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
where the [MongoModelConfig](https://github.com/travetto/travetto/tree/main/module/model-mongo/src/config.ts#
|
|
46
|
+
where the [MongoModelConfig](https://github.com/travetto/travetto/tree/main/module/model-mongo/src/config.ts#L23) is defined by:
|
|
47
47
|
|
|
48
48
|
**Code: Structure of MongoModelConfig**
|
|
49
49
|
```typescript
|
|
@@ -80,7 +80,10 @@ export class MongoModelConfig {
|
|
|
80
80
|
/**
|
|
81
81
|
* Mongo client options
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
@Field({ type: Object })
|
|
84
|
+
options: Omit<mongo.MongoClientOptions, 'cert'> & {
|
|
85
|
+
cert?: | Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
|
|
86
|
+
} = {};
|
|
84
87
|
/**
|
|
85
88
|
* Allow storage modification at runtime
|
|
86
89
|
*/
|
|
@@ -102,8 +105,6 @@ export class MongoModelConfig {
|
|
|
102
105
|
* Load all the ssl certs as needed
|
|
103
106
|
*/
|
|
104
107
|
async postConstruct(): Promise<void> {
|
|
105
|
-
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).catch(() => file);
|
|
106
|
-
|
|
107
108
|
if (this.connectionString) {
|
|
108
109
|
const details = new URL(this.connectionString);
|
|
109
110
|
this.hosts ??= details.hostname.split(',').filter(host => !!host);
|
|
@@ -129,22 +130,23 @@ export class MongoModelConfig {
|
|
|
129
130
|
const options = this.options;
|
|
130
131
|
if (options.ssl) {
|
|
131
132
|
if (options.cert) {
|
|
132
|
-
options.cert = await Promise.all([options.cert].flat(2).map(
|
|
133
|
+
options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
|
|
134
|
+
.map(BinaryUtil.binaryArrayToUint8Array);
|
|
133
135
|
}
|
|
134
136
|
if (options.tlsCertificateKeyFile) {
|
|
135
|
-
options.tlsCertificateKeyFile = await resolve(options.tlsCertificateKeyFile);
|
|
137
|
+
options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
|
|
136
138
|
}
|
|
137
139
|
if (options.tlsCAFile) {
|
|
138
|
-
options.tlsCAFile = await resolve(options.tlsCAFile);
|
|
140
|
+
options.tlsCAFile = await RuntimeResources.resolve(options.tlsCAFile);
|
|
139
141
|
}
|
|
140
142
|
if (options.tlsCRLFile) {
|
|
141
|
-
options.tlsCRLFile = await resolve(options.tlsCRLFile);
|
|
143
|
+
options.tlsCRLFile = await RuntimeResources.resolve(options.tlsCRLFile);
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (!Runtime.production) {
|
|
146
148
|
options.waitQueueTimeoutMS = 0;
|
|
147
|
-
options.serverSelectionTimeoutMS =
|
|
149
|
+
options.serverSelectionTimeoutMS = 1000;
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -155,7 +157,12 @@ export class MongoModelConfig {
|
|
|
155
157
|
const hosts = this.hosts!
|
|
156
158
|
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
157
159
|
.join(',');
|
|
158
|
-
const optionString = new URLSearchParams(
|
|
160
|
+
const optionString = new URLSearchParams(
|
|
161
|
+
Object.entries(this.options)
|
|
162
|
+
.filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
|
|
163
|
+
.map(([k, v]) => [k, `${v}`])
|
|
164
|
+
)
|
|
165
|
+
.toString();
|
|
159
166
|
let creds = '';
|
|
160
167
|
if (this.username) {
|
|
161
168
|
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mongo backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"directory": "module/model-mongo"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^
|
|
30
|
-
"@travetto/model": "^
|
|
31
|
-
"@travetto/model-query": "^
|
|
32
|
-
"mongodb": "^7.
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.0",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.0",
|
|
31
|
+
"@travetto/model-query": "^8.0.0-alpha.0",
|
|
32
|
+
"mongodb": "^7.1.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@travetto/cli": {
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type mongo from 'mongodb';
|
|
2
2
|
|
|
3
|
-
import { type TimeSpan,
|
|
3
|
+
import { type TimeSpan, Runtime, RuntimeResources, BinaryUtil, CodecUtil, type BinaryType, type BinaryArray } from '@travetto/runtime';
|
|
4
4
|
import { Config } from '@travetto/config';
|
|
5
|
+
import { Field } from '@travetto/schema';
|
|
6
|
+
|
|
7
|
+
const readCert = async (input: BinaryType | string): Promise<BinaryArray> => {
|
|
8
|
+
if (BinaryUtil.isBinaryType(input)) {
|
|
9
|
+
return BinaryUtil.toBinaryArray(input);
|
|
10
|
+
} else {
|
|
11
|
+
try {
|
|
12
|
+
return await RuntimeResources.readBinaryArray(input);
|
|
13
|
+
} catch {
|
|
14
|
+
return CodecUtil.fromUTF8String(input);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
20
|
* Mongo model config
|
|
@@ -39,7 +52,10 @@ export class MongoModelConfig {
|
|
|
39
52
|
/**
|
|
40
53
|
* Mongo client options
|
|
41
54
|
*/
|
|
42
|
-
|
|
55
|
+
@Field({ type: Object })
|
|
56
|
+
options: Omit<mongo.MongoClientOptions, 'cert'> & {
|
|
57
|
+
cert?: | Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
|
|
58
|
+
} = {};
|
|
43
59
|
/**
|
|
44
60
|
* Allow storage modification at runtime
|
|
45
61
|
*/
|
|
@@ -61,8 +77,6 @@ export class MongoModelConfig {
|
|
|
61
77
|
* Load all the ssl certs as needed
|
|
62
78
|
*/
|
|
63
79
|
async postConstruct(): Promise<void> {
|
|
64
|
-
const resolve = (file: string): Promise<string> => RuntimeResources.resolve(file).catch(() => file);
|
|
65
|
-
|
|
66
80
|
if (this.connectionString) {
|
|
67
81
|
const details = new URL(this.connectionString);
|
|
68
82
|
this.hosts ??= details.hostname.split(',').filter(host => !!host);
|
|
@@ -88,22 +102,23 @@ export class MongoModelConfig {
|
|
|
88
102
|
const options = this.options;
|
|
89
103
|
if (options.ssl) {
|
|
90
104
|
if (options.cert) {
|
|
91
|
-
options.cert = await Promise.all([options.cert].flat(2).map(
|
|
105
|
+
options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
|
|
106
|
+
.map(BinaryUtil.binaryArrayToUint8Array);
|
|
92
107
|
}
|
|
93
108
|
if (options.tlsCertificateKeyFile) {
|
|
94
|
-
options.tlsCertificateKeyFile = await resolve(options.tlsCertificateKeyFile);
|
|
109
|
+
options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
|
|
95
110
|
}
|
|
96
111
|
if (options.tlsCAFile) {
|
|
97
|
-
options.tlsCAFile = await resolve(options.tlsCAFile);
|
|
112
|
+
options.tlsCAFile = await RuntimeResources.resolve(options.tlsCAFile);
|
|
98
113
|
}
|
|
99
114
|
if (options.tlsCRLFile) {
|
|
100
|
-
options.tlsCRLFile = await resolve(options.tlsCRLFile);
|
|
115
|
+
options.tlsCRLFile = await RuntimeResources.resolve(options.tlsCRLFile);
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
if (!Runtime.production) {
|
|
105
120
|
options.waitQueueTimeoutMS = 0;
|
|
106
|
-
options.serverSelectionTimeoutMS =
|
|
121
|
+
options.serverSelectionTimeoutMS = 1000;
|
|
107
122
|
}
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -114,7 +129,12 @@ export class MongoModelConfig {
|
|
|
114
129
|
const hosts = this.hosts!
|
|
115
130
|
.map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
|
|
116
131
|
.join(',');
|
|
117
|
-
const optionString = new URLSearchParams(
|
|
132
|
+
const optionString = new URLSearchParams(
|
|
133
|
+
Object.entries(this.options)
|
|
134
|
+
.filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
|
|
135
|
+
.map(([k, v]) => [k, `${v}`])
|
|
136
|
+
)
|
|
137
|
+
.toString();
|
|
118
138
|
let creds = '';
|
|
119
139
|
if (this.username) {
|
|
120
140
|
creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
|
package/src/internal/util.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type IndexDescriptionInfo
|
|
4
4
|
} from 'mongodb';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { RuntimeError, CodecUtil, castTo, type Class, toConcrete, TypedObject, BinaryUtil } from '@travetto/runtime';
|
|
7
7
|
import { type DistanceUnit, type PageableModelQuery, type WhereClause, ModelQueryUtil } from '@travetto/model-query';
|
|
8
8
|
import type { ModelType, IndexField, IndexConfig } from '@travetto/model';
|
|
9
9
|
import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
|
|
@@ -50,7 +50,19 @@ export class MongoUtil {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
static uuid(value: string): Binary {
|
|
53
|
-
|
|
53
|
+
try {
|
|
54
|
+
return new Binary(
|
|
55
|
+
BinaryUtil.binaryArrayToUint8Array(
|
|
56
|
+
CodecUtil.fromHexString(value.replaceAll('-', ''))
|
|
57
|
+
),
|
|
58
|
+
Binary.SUBTYPE_UUID
|
|
59
|
+
);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof RuntimeError && err.message === 'Invalid hex string') {
|
|
62
|
+
return null!;
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
static idToString(id: string | ObjectId | Binary): string {
|
|
@@ -59,7 +71,7 @@ export class MongoUtil {
|
|
|
59
71
|
} else if (id instanceof ObjectId) {
|
|
60
72
|
return id.toHexString();
|
|
61
73
|
} else {
|
|
62
|
-
return
|
|
74
|
+
return CodecUtil.toHexString(id.buffer);
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
|
|
@@ -104,7 +116,7 @@ export class MongoUtil {
|
|
|
104
116
|
const temp = value[firstKey];
|
|
105
117
|
out._id = { [firstKey]: Array.isArray(temp) ? temp.map(subValue => this.uuid(subValue)) : this.uuid(`${temp}`) };
|
|
106
118
|
} else {
|
|
107
|
-
throw new
|
|
119
|
+
throw new RuntimeError('Invalid id query');
|
|
108
120
|
}
|
|
109
121
|
} else if ((isPlain && !firstKey.startsWith('$')) || value?.constructor?.Ⲑid) {
|
|
110
122
|
if (recursive) {
|
package/src/service.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { pipeline } from 'node:stream/promises';
|
|
2
|
-
|
|
3
1
|
import {
|
|
2
|
+
type Binary,
|
|
4
3
|
type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection,
|
|
5
|
-
type ObjectId, type
|
|
4
|
+
type ObjectId, type RootFilterOperators, type Filter,
|
|
6
5
|
type WithId as MongoWithId,
|
|
7
6
|
} from 'mongodb';
|
|
8
7
|
|
|
@@ -10,7 +9,7 @@ import {
|
|
|
10
9
|
ModelRegistryIndex, type ModelType, type OptionalId, type ModelCrudSupport, type ModelStorageSupport,
|
|
11
10
|
type ModelExpirySupport, type ModelBulkSupport, type ModelIndexedSupport, type BulkOperation, type BulkResponse,
|
|
12
11
|
NotFoundError, ExistsError, type ModelBlobSupport,
|
|
13
|
-
ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
|
|
12
|
+
ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
|
|
14
13
|
} from '@travetto/model';
|
|
15
14
|
import {
|
|
16
15
|
type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport,
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
|
|
22
21
|
import {
|
|
23
22
|
ShutdownManager, type Class, type DeepPartial, TypedObject,
|
|
24
|
-
castTo, asFull, type
|
|
23
|
+
castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType, BinaryUtil, BinaryMetadataUtil,
|
|
25
24
|
} from '@travetto/runtime';
|
|
26
25
|
import { Injectable } from '@travetto/di';
|
|
27
26
|
|
|
@@ -30,7 +29,7 @@ import type { MongoModelConfig } from './config.ts';
|
|
|
30
29
|
|
|
31
30
|
const ListIndexSymbol = Symbol();
|
|
32
31
|
|
|
33
|
-
type BlobRaw = GridFSFile & { metadata?:
|
|
32
|
+
type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
|
|
34
33
|
|
|
35
34
|
type MongoTextSearch = RootFilterOperators<unknown>['$text'];
|
|
36
35
|
|
|
@@ -100,7 +99,10 @@ export class MongoModelService implements
|
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
async postConstruct(): Promise<void> {
|
|
103
|
-
this.client = await MongoClient.connect(this.config.url,
|
|
102
|
+
this.client = await MongoClient.connect(this.config.url, {
|
|
103
|
+
...this.config.connectionOptions,
|
|
104
|
+
useBigInt64: true,
|
|
105
|
+
});
|
|
104
106
|
this.#db = this.client.db(this.config.namespace);
|
|
105
107
|
this.#bucket = new GridFSBucket(this.#db, {
|
|
106
108
|
bucketName: ModelBlobNamespace,
|
|
@@ -284,14 +286,14 @@ export class MongoModelService implements
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
// Blob
|
|
287
|
-
async upsertBlob(location: string, input:
|
|
288
|
-
const existing = await this.
|
|
289
|
+
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
290
|
+
const existing = await this.getBlobMetadata(location).then(() => true, () => false);
|
|
289
291
|
if (!overwrite && existing) {
|
|
290
292
|
return;
|
|
291
293
|
}
|
|
292
|
-
const
|
|
293
|
-
const writeStream = this.#bucket.openUploadStream(location, { metadata:
|
|
294
|
-
await pipeline(
|
|
294
|
+
const resolved = await BinaryMetadataUtil.compute(input, metadata);
|
|
295
|
+
const writeStream = this.#bucket.openUploadStream(location, { metadata: resolved });
|
|
296
|
+
await BinaryUtil.pipeline(input, writeStream);
|
|
295
297
|
|
|
296
298
|
if (existing) {
|
|
297
299
|
const [read] = await this.#bucket.find({ filename: location, _id: { $ne: writeStream.id } }).toArray();
|
|
@@ -300,14 +302,14 @@ export class MongoModelService implements
|
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
303
|
-
const
|
|
304
|
-
const final = range ?
|
|
305
|
+
const metadata = await this.getBlobMetadata(location);
|
|
306
|
+
const final = range ? BinaryMetadataUtil.enforceRange(range, metadata) : undefined;
|
|
305
307
|
const mongoRange = final ? { start: final.start, end: final.end + 1 } : undefined;
|
|
306
|
-
return
|
|
308
|
+
return BinaryMetadataUtil.makeBlob(() => this.#bucket.openDownloadStreamByName(location, mongoRange), { ...metadata, range: final });
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
async
|
|
310
|
-
const result = await this.#db.collection<{ metadata:
|
|
311
|
+
async getBlobMetadata(location: string): Promise<BinaryMetadata> {
|
|
312
|
+
const result = await this.#db.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`).findOne({ filename: location });
|
|
311
313
|
return result!.metadata;
|
|
312
314
|
}
|
|
313
315
|
|
|
@@ -316,10 +318,10 @@ export class MongoModelService implements
|
|
|
316
318
|
await this.#bucket.delete(fileId);
|
|
317
319
|
}
|
|
318
320
|
|
|
319
|
-
async
|
|
320
|
-
await this.#db.collection<{ metadata:
|
|
321
|
+
async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
|
|
322
|
+
await this.#db.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`).findOneAndUpdate(
|
|
321
323
|
{ filename: location },
|
|
322
|
-
{ $set: { metadata
|
|
324
|
+
{ $set: { metadata, contentType: metadata.contentType! } },
|
|
323
325
|
);
|
|
324
326
|
}
|
|
325
327
|
|