mongodb-data-service 22.23.2 → 22.23.5

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.
@@ -68,6 +68,11 @@ const maybePickNs = ([ns]) => {
68
68
  const isPromiseLike = (val) => {
69
69
  return 'then' in val && typeof val.then === 'function';
70
70
  };
71
+ /**
72
+ * Translates the error message to something human readable.
73
+ * @param error - The error.
74
+ * @returns The error with message translated.
75
+ */
71
76
  const translateErrorMessage = (error) => {
72
77
  if (typeof error === 'string') {
73
78
  error = { message: error };
@@ -77,6 +82,13 @@ const translateErrorMessage = (error) => {
77
82
  }
78
83
  return error;
79
84
  };
85
+ /**
86
+ * Decorator to do standard op handling that is applied to every public method
87
+ * of the data service
88
+ *
89
+ * - transform error message before throwing
90
+ * - log method success / failure
91
+ */
80
92
  function op(logId, pickLogAttrs = maybePickNs) {
81
93
  return function (target, context) {
82
94
  const opName = String(context.name);
@@ -166,6 +178,10 @@ let DataServiceImpl = (() => {
166
178
  this._isConnecting = false;
167
179
  this._useCRUDClient = true;
168
180
  this._reauthenticationHandlers = new Set();
181
+ /**
182
+ * Stores the most recent topology description from the server's SDAM events:
183
+ * https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring-monitoring.rst#events
184
+ */
169
185
  this._lastSeenTopology = null;
170
186
  this._isWritable = false;
171
187
  this._emitter = new events_1.EventEmitter();
@@ -215,6 +231,9 @@ let DataServiceImpl = (() => {
215
231
  return this;
216
232
  }
217
233
  getMongoClientConnectionOptions() {
234
+ // `notifyDeviceFlow` is a function which cannot be serialized for inclusion
235
+ // in the shell, `signal` is an abortSignal, and `allowedFlows` is turned
236
+ // into a function by the connection code.
218
237
  return (0, lodash_1.omit)(this._mongoClientConnectionOptions, 'options.oidc.notifyDeviceFlow', 'options.oidc.signal', 'options.oidc.allowedFlows', 'options.oidc.customHttpOptions.agent');
219
238
  }
220
239
  getConnectionOptions() {
@@ -277,6 +296,7 @@ let DataServiceImpl = (() => {
277
296
  },
278
297
  {
279
298
  $addFields: {
299
+ // `avgObjSize` is the average of per-shard `avgObjSize` weighted by `count`
280
300
  avgObjSize: {
281
301
  $cond: {
282
302
  if: { $ne: ['$count', 0] },
@@ -298,6 +318,8 @@ let DataServiceImpl = (() => {
298
318
  }
299
319
  catch (error) {
300
320
  const message = error.message;
321
+ // We ignore errors for fetching collStats when requesting on an
322
+ // unsupported collection type: either a view or a ADF
301
323
  if (message.includes('not valid for Data Lake') ||
302
324
  message.includes('is a view, not a collection')) {
303
325
  return this._buildCollectionStats(databaseName, collectionName, {});
@@ -346,6 +368,8 @@ let DataServiceImpl = (() => {
346
368
  async _listCollections(databaseName, filter = {}, { nameOnly } = {}) {
347
369
  try {
348
370
  const cursor = this._database(databaseName, 'CRUD').listCollections(filter, { nameOnly });
371
+ // Iterate instead of using .toArray() so we can emit
372
+ // collection info update events as they come in.
349
373
  const results = [];
350
374
  for await (const result of cursor) {
351
375
  if (!nameOnly) {
@@ -356,6 +380,15 @@ let DataServiceImpl = (() => {
356
380
  return results;
357
381
  }
358
382
  catch (err) {
383
+ // Currently Compass should not fail if listCollections failed for
384
+ // any possible reason to preserve current behavior. We probably
385
+ // want this to check at least that what we got back is a server
386
+ // error and not a weird runtime issue on our side that can be
387
+ // swallowed in this case, ideally we know exactly what server
388
+ // errors we want to handle here and only avoid throwing in these
389
+ // cases
390
+ //
391
+ // TODO: https://jira.mongodb.org/browse/COMPASS-5275
359
392
  this._logger.warn((0, logger_1.mongoLogId)(1001000099), 'Failed to run listCollections', { message: err.message });
360
393
  return [];
361
394
  }
@@ -363,15 +396,29 @@ let DataServiceImpl = (() => {
363
396
  async listCollections(databaseName, filter = {}, { nameOnly, privileges = null, } = {}) {
364
397
  const getCollectionsFromPrivileges = async () => {
365
398
  const databases = (0, instance_detail_helper_2.getPrivilegesByDatabaseAndCollection)(await this._getPrivilegesOrFallback(privileges), ['find']);
366
- return Object.keys(databases[databaseName] || {})
367
- .filter(Boolean)
399
+ return Object.keys(
400
+ // Privileges might not have a database we are looking for
401
+ databases[databaseName] || {})
402
+ .filter(
403
+ // Privileges can have collection name '' that indicates
404
+ // privileges on all collections in the database, we don't want
405
+ // those registered as "real" collection names
406
+ Boolean)
368
407
  .map((name) => ({ name }));
369
408
  };
370
409
  const [listedCollections, collectionsFromPrivileges] = await Promise.all([
371
410
  this._listCollections(databaseName, filter, { nameOnly }),
411
+ // If the filter is not empty, we can't meaningfully derive collections
412
+ // from privileges and filter them as the criteria might include any key
413
+ // from the listCollections result object and there is no such info in
414
+ // privileges. Because of that we are ignoring privileges completely if
415
+ // listCollections was called with a filter.
372
416
  isEmptyObject(filter) ? getCollectionsFromPrivileges() : [],
373
417
  ]);
374
- const collections = uniqueBy([...collectionsFromPrivileges, ...listedCollections], 'name').map((coll) => (0, instance_detail_helper_2.adaptCollectionInfo)({ db: databaseName, ...coll }));
418
+ const collections = uniqueBy(
419
+ // NB: Order is important, we want listed collections to take precedence
420
+ // if they were fetched successfully
421
+ [...collectionsFromPrivileges, ...listedCollections], 'name').map((coll) => (0, instance_detail_helper_2.adaptCollectionInfo)({ db: databaseName, ...coll }));
375
422
  return collections;
376
423
  }
377
424
  async listDatabases({ nameOnly, privileges = null, roles = null, } = {}) {
@@ -385,6 +432,14 @@ let DataServiceImpl = (() => {
385
432
  return databases;
386
433
  }
387
434
  catch (err) {
435
+ // Currently Compass should not fail if listDatabase failed for any
436
+ // possible reason to preserve current behavior. We probably want this
437
+ // to check at least that what we got back is a server error and not a
438
+ // weird runtime issue on our side that can be swallowed in this case,
439
+ // ideally we know exactly what server errors we want to handle here
440
+ // and only avoid throwing in these cases
441
+ //
442
+ // TODO: https://jira.mongodb.org/browse/COMPASS-5275
388
443
  this._logger.warn((0, logger_1.mongoLogId)(1001000098), 'Failed to run listDatabases', { message: err.message });
389
444
  return [];
390
445
  }
@@ -392,11 +447,24 @@ let DataServiceImpl = (() => {
392
447
  const getDatabasesFromPrivileges = async () => {
393
448
  const databases = (0, instance_detail_helper_2.getPrivilegesByDatabaseAndCollection)(await this._getPrivilegesOrFallback(privileges), ['find']);
394
449
  return Object.keys(databases)
395
- .filter(Boolean)
450
+ .filter(
451
+ // For the roles created in admin database, the database name
452
+ // can be '' meaning that it applies to all databases. We can't
453
+ // meaningfully handle this in the UI so we are filtering these
454
+ // out
455
+ Boolean)
396
456
  .map((name) => ({ name }));
397
457
  };
398
458
  const getDatabasesFromRoles = async () => {
399
- const databases = (0, instance_detail_helper_2.getDatabasesByRoles)(await this._getRolesOrFallback(roles), ['read', 'readWrite', 'dbAdmin', 'dbOwner']);
459
+ const databases = (0, instance_detail_helper_2.getDatabasesByRoles)(await this._getRolesOrFallback(roles),
460
+ // https://jira.mongodb.org/browse/HELP-32199
461
+ // Atlas shared tier MongoDB server version v5+ does not return
462
+ // `authenticatedUserPrivileges` as part of the `connectionStatus`.
463
+ // As a workaround we show the databases the user has
464
+ // certain general built-in roles for.
465
+ // This does not cover custom user roles which can
466
+ // have custom privileges that we can't currently fetch.
467
+ ['read', 'readWrite', 'dbAdmin', 'dbOwner']);
400
468
  return databases.map((name) => ({ name }));
401
469
  };
402
470
  const [listedDatabases, databasesFromPrivileges, databasesFromRoles] = await Promise.all([
@@ -404,7 +472,10 @@ let DataServiceImpl = (() => {
404
472
  getDatabasesFromPrivileges(),
405
473
  getDatabasesFromRoles(),
406
474
  ]);
407
- const databases = uniqueBy([...databasesFromRoles, ...databasesFromPrivileges, ...listedDatabases], 'name').map((db) => {
475
+ const databases = uniqueBy(
476
+ // NB: Order is important, we want listed collections to take precedence
477
+ // if they were fetched successfully
478
+ [...databasesFromRoles, ...databasesFromPrivileges, ...listedDatabases], 'name').map((db) => {
408
479
  return { _id: db.name, name: db.name, ...(0, instance_detail_helper_2.adaptDatabaseInfo)(db) };
409
480
  });
410
481
  return databases;
@@ -549,6 +620,7 @@ let DataServiceImpl = (() => {
549
620
  .toArray();
550
621
  }
551
622
  catch {
623
+ // ignore
552
624
  }
553
625
  const options = {};
554
626
  const encryptedFieldsInfo = result?.options?.encryptedFields;
@@ -600,6 +672,8 @@ let DataServiceImpl = (() => {
600
672
  aggregateCursor(ns, pipeline, options = {}) {
601
673
  return this._collection(ns, 'CRUD').aggregate(pipeline, options);
602
674
  }
675
+ // @ts-expect-error generic in the method trips up TS here resulting in
676
+ // Promise<unknown> is not assignable to Promise<Document[]>
603
677
  aggregate(ns, pipeline, options = {}, executionOptions) {
604
678
  let cursor;
605
679
  return this._cancellableOperation(async (session) => {
@@ -644,7 +718,7 @@ let DataServiceImpl = (() => {
644
718
  return await coll.replaceOne(filter, replacement, options);
645
719
  }
646
720
  explainFind(ns, filter, options = {}, executionOptions) {
647
- const verbosity = executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.allPlansExecution;
721
+ const verbosity = executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.executionStats;
648
722
  let cursor;
649
723
  return this._cancellableOperation(async (session) => {
650
724
  cursor = this._collection(ns, 'CRUD').find(filter, {
@@ -657,7 +731,7 @@ let DataServiceImpl = (() => {
657
731
  }, () => cursor?.close(), executionOptions?.abortSignal);
658
732
  }
659
733
  explainAggregate(ns, pipeline, options, executionOptions) {
660
- const verbosity = executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.queryPlanner;
734
+ const verbosity = executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.executionStats;
661
735
  let cursor;
662
736
  return this._cancellableOperation(async (session) => {
663
737
  cursor = this._collection(ns, 'CRUD').aggregate(pipeline, {
@@ -737,6 +811,7 @@ let DataServiceImpl = (() => {
737
811
  async instance() {
738
812
  return {
739
813
  ...(await (0, instance_detail_helper_2.getInstance)(this._initializedClient('META'), this._connectionOptions.connectionString)),
814
+ // Need to get the CSFLE flag from the CRUD client, not the META one
740
815
  csfleMode: this.getCSFLEMode(),
741
816
  };
742
817
  }
@@ -748,13 +823,21 @@ let DataServiceImpl = (() => {
748
823
  const coll = this._collection(ns, 'CRUD');
749
824
  return await coll.insertMany(docs, options);
750
825
  }
751
- async updateCollection(ns, flags = {}) {
826
+ async updateCollection(ns,
827
+ // Collection name to update that will be passed to the collMod command will
828
+ // be derived from the provided namespace, this is why we are explicitly
829
+ // prohibiting to pass collMod flag here
830
+ flags = {}) {
752
831
  const collectionName = this._collectionName(ns);
753
832
  const db = this._database(ns, 'CRUD');
754
833
  const result = await (0, run_command_1.runCommand)(db, {
834
+ // Order of arguments is important here, collMod is a command name and it
835
+ // should always be the first one in the object
755
836
  collMod: collectionName,
756
837
  ...flags,
757
838
  });
839
+ // Reset the CSFLE-enabled client (if any) to clear any collection
840
+ // metadata caches that might still be active.
758
841
  await this._resetCRUDClient();
759
842
  return result;
760
843
  }
@@ -807,6 +890,7 @@ let DataServiceImpl = (() => {
807
890
  size: size === 0 ? 0 : size || 1000,
808
891
  },
809
892
  });
893
+ // add $project stage if projection (fields) was specified
810
894
  if (fields && Object.keys(fields).length > 0) {
811
895
  pipeline.push({
812
896
  $project: fields,
@@ -836,6 +920,9 @@ let DataServiceImpl = (() => {
836
920
  });
837
921
  }
838
922
  isConnected() {
923
+ // This is better than just returning internal `_isConnecting` as this
924
+ // actually shows when the client is available on the NativeClient instance
925
+ // and connected
839
926
  return !!this._metadataClient;
840
927
  }
841
928
  async _cancellableOperation(start, stop = () => Promise.resolve(), abortSignal) {
@@ -843,6 +930,7 @@ let DataServiceImpl = (() => {
843
930
  return await start();
844
931
  }
845
932
  if (abortSignal.aborted) {
933
+ // AbortSignal.reason is supported from node v17.2.0.
846
934
  throw abortSignal.reason ?? (0, compass_utils_1.createCancelError)();
847
935
  }
848
936
  const session = this._startSession('CRUD');
@@ -904,10 +992,15 @@ let DataServiceImpl = (() => {
904
992
  this._lastSeenTopology = evt.newDescription;
905
993
  this._emitter.emit('topologyDescriptionChanged', evt);
906
994
  });
995
+ // Heartbeat events are fairly noisy. We put some effort into
996
+ // making sure that we only log events that actually contain
997
+ // meaningful information.
907
998
  const heartbeatStatusMap = new Map();
908
999
  const isSignificantDurationChange = (before, after) => {
1000
+ // Changes in pings below 200ms are not particularly concerning here.
909
1001
  before = Math.max(before, 200);
910
1002
  after = Math.max(after, 200);
1003
+ // Check if changes is at least 2× (in either direction).
911
1004
  return Math.abs(Math.log2(before / after)) >= 1;
912
1005
  };
913
1006
  client.on('serverHeartbeatSucceeded', (evt) => {
@@ -1007,6 +1100,8 @@ let DataServiceImpl = (() => {
1007
1100
  .find(filter, {
1008
1101
  session,
1009
1102
  maxTimeMS: remainingTimeoutMS(),
1103
+ // by using promoteValues: false we can spot BSON type changes
1104
+ // when diffing. ie. new Double(1) -> new Int32(1)
1010
1105
  promoteValues: false,
1011
1106
  })
1012
1107
  .sort({ _id: 1 })
@@ -1038,8 +1133,14 @@ let DataServiceImpl = (() => {
1038
1133
  }
1039
1134
  catch (err) {
1040
1135
  if (isTransactionAbortError(err)) {
1136
+ // The transaction was aborted while it was still calculating the
1137
+ // preview. Just return something here rather than erroring. No
1138
+ // reason to abort the transaction or end the session again because
1139
+ // we only got here because that already happened.
1041
1140
  return { changes: [] };
1042
1141
  }
1142
+ // The only way this if statement would be true is if aborting the
1143
+ // transaction and ending the session itself failed above. Unlikely.
1043
1144
  if (session.inTransaction()) {
1044
1145
  await session.abortTransaction();
1045
1146
  await session.endSession();
@@ -1047,8 +1148,19 @@ let DataServiceImpl = (() => {
1047
1148
  throw err;
1048
1149
  }
1049
1150
  }, async () => {
1151
+ // Rely on the session being killed when we cancel the operation. It can
1152
+ // take a few seconds before the driver reacts, but the Promise.race()
1153
+ // against the abort signal should cause the UI to immediately be
1154
+ // notified that the operation was cancelled. We don't abort the
1155
+ // transaction or end the session as well because the code we run inside
1156
+ // session.withTransaction() above would experience race conditions.
1050
1157
  }, abortSignal);
1051
1158
  }
1159
+ /**
1160
+ * @param databaseName - The name of the database.
1161
+ * @param collectionName - The name of the collection.
1162
+ * @param data - The result of the collStats command.
1163
+ */
1052
1164
  _buildCollectionStats(databaseName, collectionName, data) {
1053
1165
  return {
1054
1166
  ns: databaseName + '.' + collectionName,
@@ -1064,20 +1176,47 @@ let DataServiceImpl = (() => {
1064
1176
  index_size: data.totalIndexSize ?? 0,
1065
1177
  };
1066
1178
  }
1179
+ /**
1180
+ * Get the collection to operate on.
1181
+ *
1182
+ * @param ns - The namespace.
1183
+ */
1067
1184
  _collection(ns, type) {
1068
1185
  return this._initializedClient(type)
1069
1186
  .db(this._databaseName(ns))
1070
1187
  .collection(this._collectionName(ns));
1071
1188
  }
1189
+ /**
1190
+ * Get the database to operate on.
1191
+ *
1192
+ * @param ns - The namespace.
1193
+ */
1072
1194
  _database(ns, type) {
1073
1195
  return this._initializedClient(type).db(this._databaseName(ns));
1074
1196
  }
1197
+ /**
1198
+ * Get the collection name from a namespace.
1199
+ *
1200
+ * @param ns - The namespace in database.collection format.
1201
+ */
1075
1202
  _collectionName(ns) {
1076
1203
  return (0, mongodb_ns_1.default)(ns).collection;
1077
1204
  }
1205
+ /**
1206
+ * Get the database name from a namespace.
1207
+ *
1208
+ * @param ns - The namespace in database.collection format.
1209
+ */
1078
1210
  _databaseName(ns) {
1079
1211
  return (0, mongodb_ns_1.default)(ns).database;
1080
1212
  }
1213
+ /**
1214
+ * Determine if the hello response indicates a writable server.
1215
+ *
1216
+ * @param evt - The topology description changed event.
1217
+ *
1218
+ * @returns If the server is writable.
1219
+ */
1081
1220
  _checkIsWritable(evt) {
1082
1221
  return [...evt.newDescription.servers.values()].some((server) => server.isWritable);
1083
1222
  }
@@ -1095,10 +1234,12 @@ let DataServiceImpl = (() => {
1095
1234
  }
1096
1235
  async _resetCRUDClient() {
1097
1236
  if (this.getCSFLEMode() === 'unavailable') {
1237
+ // No separate client in use, don't do anything
1098
1238
  return;
1099
1239
  }
1100
1240
  const crudClient = this._crudClient;
1101
1241
  if (!crudClient) {
1242
+ // In disconnected state, don't do anything
1102
1243
  return;
1103
1244
  }
1104
1245
  const newClient = await crudClient[connect_mongo_client_1.createClonedClient]();
@@ -1107,6 +1248,9 @@ let DataServiceImpl = (() => {
1107
1248
  await crudClient.close();
1108
1249
  }
1109
1250
  else {
1251
+ // If _crudClient has changed between start and end of
1252
+ // connection establishment, don't do anything, just
1253
+ // close and discard the new client.
1110
1254
  await newClient.close();
1111
1255
  }
1112
1256
  }
@@ -1255,15 +1399,13 @@ let DataServiceImpl = (() => {
1255
1399
  _explainFind_decorators = [op((0, logger_1.mongoLogId)(1001000046), ([ns, , , executionOptions]) => {
1256
1400
  return {
1257
1401
  ns,
1258
- verbosity: executionOptions?.explainVerbosity ||
1259
- mongodb_1.ExplainVerbosity.allPlansExecution,
1402
+ verbosity: executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.executionStats,
1260
1403
  };
1261
1404
  })];
1262
1405
  _explainAggregate_decorators = [op((0, logger_1.mongoLogId)(1001000177), ([ns, , , executionOptions]) => {
1263
1406
  return {
1264
1407
  ns,
1265
- verbosity: executionOptions?.explainVerbosity ||
1266
- mongodb_1.ExplainVerbosity.allPlansExecution,
1408
+ verbosity: executionOptions?.explainVerbosity || mongodb_1.ExplainVerbosity.executionStats,
1267
1409
  };
1268
1410
  })];
1269
1411
  _indexes_decorators = [op((0, logger_1.mongoLogId)(1001000047))];
@@ -1368,7 +1510,12 @@ let DataServiceImpl = (() => {
1368
1510
  __esDecorate(_a, null, _dropStreamProcessor_decorators, { kind: "method", name: "dropStreamProcessor", static: false, private: false, access: { has: obj => "dropStreamProcessor" in obj, get: obj => obj.dropStreamProcessor } }, null, _instanceExtraInitializers);
1369
1511
  })(),
1370
1512
  (() => {
1371
- const assertNoExtraProps = (_cls) => {
1513
+ const assertNoExtraProps = (
1514
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1515
+ _cls) => {
1516
+ // Checking that we are not exposing anything unexpected on our data service
1517
+ // implementation. This file will not compile if there are more public methods
1518
+ // on the DataServiceImpl than DataService interface allows
1372
1519
  };
1373
1520
  assertNoExtraProps(_a);
1374
1521
  })(),