@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 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#L10) is defined by:
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
- options: mongo.MongoClientOptions = {};
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(data => Buffer.isBuffer(data) ? data : resolve(data)));
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 = TimeUtil.asMillis(1, 's');
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(Object.entries(this.options)).toString();
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": "7.1.3",
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": "^7.1.3",
30
- "@travetto/model": "^7.1.3",
31
- "@travetto/model-query": "^7.1.3",
32
- "mongodb": "^7.0.0"
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": "^7.1.3"
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, TimeUtil, RuntimeResources, Runtime } from '@travetto/runtime';
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
- options: mongo.MongoClientOptions = {};
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(data => Buffer.isBuffer(data) ? data : resolve(data)));
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 = TimeUtil.asMillis(1, 's');
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(Object.entries(this.options)).toString();
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(':')}@`;
@@ -3,7 +3,7 @@ import {
3
3
  type IndexDescriptionInfo
4
4
  } from 'mongodb';
5
5
 
6
- import { AppError, castTo, type Class, toConcrete, TypedObject } from '@travetto/runtime';
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
- return new Binary(Buffer.from(value.replaceAll('-', ''), 'hex'), Binary.SUBTYPE_UUID);
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 Buffer.from(id.buffer).toString('hex');
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 AppError('Invalid id query');
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 Binary, type RootFilterOperators, type Filter,
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, ModelBlobUtil
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 BlobMeta, type ByteRange, type BinaryInput, BinaryUtil
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?: BlobMeta };
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, this.config.connectionOptions);
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: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
288
- const existing = await this.getBlobMeta(location).then(() => true, () => false);
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 [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
293
- const writeStream = this.#bucket.openUploadStream(location, { metadata: blobMeta });
294
- await pipeline(stream, writeStream);
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 meta = await this.getBlobMeta(location);
304
- const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
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 BinaryUtil.readableBlob(() => this.#bucket.openDownloadStreamByName(location, mongoRange), { ...meta, range: final });
308
+ return BinaryMetadataUtil.makeBlob(() => this.#bucket.openDownloadStreamByName(location, mongoRange), { ...metadata, range: final });
307
309
  }
308
310
 
309
- async getBlobMeta(location: string): Promise<BlobMeta> {
310
- const result = await this.#db.collection<{ metadata: BlobMeta }>(`${ModelBlobNamespace}.files`).findOne({ filename: location });
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 updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
320
- await this.#db.collection<{ metadata: BlobMeta }>(`${ModelBlobNamespace}.files`).findOneAndUpdate(
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: meta, contentType: meta.contentType! } },
324
+ { $set: { metadata, contentType: metadata.contentType! } },
323
325
  );
324
326
  }
325
327