mongo_client.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.MongoClient = exports.ServerApiVersion = void 0;
  4. const fs_1 = require("fs");
  5. const util_1 = require("util");
  6. const bson_1 = require("./bson");
  7. const change_stream_1 = require("./change_stream");
  8. const mongo_credentials_1 = require("./cmap/auth/mongo_credentials");
  9. const providers_1 = require("./cmap/auth/providers");
  10. const connection_string_1 = require("./connection_string");
  11. const constants_1 = require("./constants");
  12. const db_1 = require("./db");
  13. const error_1 = require("./error");
  14. const mongo_logger_1 = require("./mongo_logger");
  15. const mongo_types_1 = require("./mongo_types");
  16. const execute_operation_1 = require("./operations/execute_operation");
  17. const run_command_1 = require("./operations/run_command");
  18. const read_preference_1 = require("./read_preference");
  19. const server_selection_1 = require("./sdam/server_selection");
  20. const topology_1 = require("./sdam/topology");
  21. const sessions_1 = require("./sessions");
  22. const utils_1 = require("./utils");
  23. /** @public */
  24. exports.ServerApiVersion = Object.freeze({
  25. v1: '1'
  26. });
  27. /** @internal */
  28. const kOptions = Symbol('options');
  29. /**
  30. * The **MongoClient** class is a class that allows for making Connections to MongoDB.
  31. * @public
  32. *
  33. * @remarks
  34. * The programmatically provided options take precedence over the URI options.
  35. *
  36. * @example
  37. * ```ts
  38. * import { MongoClient } from 'mongodb';
  39. *
  40. * // Enable command monitoring for debugging
  41. * const client = new MongoClient('mongodb://localhost:27017', { monitorCommands: true });
  42. *
  43. * client.on('commandStarted', started => console.log(started));
  44. * client.db().collection('pets');
  45. * await client.insertOne({ name: 'spot', kind: 'dog' });
  46. * ```
  47. */
  48. class MongoClient extends mongo_types_1.TypedEventEmitter {
  49. constructor(url, options) {
  50. super();
  51. this[kOptions] = (0, connection_string_1.parseOptions)(url, this, options);
  52. this.mongoLogger = new mongo_logger_1.MongoLogger(this[kOptions].mongoLoggerOptions);
  53. // eslint-disable-next-line @typescript-eslint/no-this-alias
  54. const client = this;
  55. // The internal state
  56. this.s = {
  57. url,
  58. bsonOptions: (0, bson_1.resolveBSONOptions)(this[kOptions]),
  59. namespace: (0, utils_1.ns)('admin'),
  60. hasBeenClosed: false,
  61. sessionPool: new sessions_1.ServerSessionPool(this),
  62. activeSessions: new Set(),
  63. get options() {
  64. return client[kOptions];
  65. },
  66. get readConcern() {
  67. return client[kOptions].readConcern;
  68. },
  69. get writeConcern() {
  70. return client[kOptions].writeConcern;
  71. },
  72. get readPreference() {
  73. return client[kOptions].readPreference;
  74. },
  75. get isMongoClient() {
  76. return true;
  77. }
  78. };
  79. }
  80. /** @see MongoOptions */
  81. get options() {
  82. return Object.freeze({ ...this[kOptions] });
  83. }
  84. get serverApi() {
  85. return this[kOptions].serverApi && Object.freeze({ ...this[kOptions].serverApi });
  86. }
  87. /**
  88. * Intended for APM use only
  89. * @internal
  90. */
  91. get monitorCommands() {
  92. return this[kOptions].monitorCommands;
  93. }
  94. set monitorCommands(value) {
  95. this[kOptions].monitorCommands = value;
  96. }
  97. /** @internal */
  98. get autoEncrypter() {
  99. return this[kOptions].autoEncrypter;
  100. }
  101. get readConcern() {
  102. return this.s.readConcern;
  103. }
  104. get writeConcern() {
  105. return this.s.writeConcern;
  106. }
  107. get readPreference() {
  108. return this.s.readPreference;
  109. }
  110. get bsonOptions() {
  111. return this.s.bsonOptions;
  112. }
  113. /**
  114. * Connect to MongoDB using a url
  115. *
  116. * @see docs.mongodb.org/manual/reference/connection-string/
  117. */
  118. async connect() {
  119. if (this.connectionLock) {
  120. return this.connectionLock;
  121. }
  122. try {
  123. this.connectionLock = this._connect();
  124. await this.connectionLock;
  125. }
  126. finally {
  127. // release
  128. this.connectionLock = undefined;
  129. }
  130. return this;
  131. }
  132. /**
  133. * Create a topology to open the connection, must be locked to avoid topology leaks in concurrency scenario.
  134. * Locking is enforced by the connect method.
  135. *
  136. * @internal
  137. */
  138. async _connect() {
  139. if (this.topology && this.topology.isConnected()) {
  140. return this;
  141. }
  142. const options = this[kOptions];
  143. if (options.tls) {
  144. if (typeof options.tlsCAFile === 'string') {
  145. options.ca ??= await fs_1.promises.readFile(options.tlsCAFile);
  146. }
  147. if (typeof options.tlsCRLFile === 'string') {
  148. options.crl ??= await fs_1.promises.readFile(options.tlsCRLFile);
  149. }
  150. if (typeof options.tlsCertificateKeyFile === 'string') {
  151. if (!options.key || !options.cert) {
  152. const contents = await fs_1.promises.readFile(options.tlsCertificateKeyFile);
  153. options.key ??= contents;
  154. options.cert ??= contents;
  155. }
  156. }
  157. }
  158. if (typeof options.srvHost === 'string') {
  159. const hosts = await (0, connection_string_1.resolveSRVRecord)(options);
  160. for (const [index, host] of hosts.entries()) {
  161. options.hosts[index] = host;
  162. }
  163. }
  164. // It is important to perform validation of hosts AFTER SRV resolution, to check the real hostname,
  165. // but BEFORE we even attempt connecting with a potentially not allowed hostname
  166. if (options.credentials?.mechanism === providers_1.AuthMechanism.MONGODB_OIDC) {
  167. const allowedHosts = options.credentials?.mechanismProperties?.ALLOWED_HOSTS || mongo_credentials_1.DEFAULT_ALLOWED_HOSTS;
  168. const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME;
  169. if (!isServiceAuth) {
  170. for (const host of options.hosts) {
  171. if (!(0, utils_1.hostMatchesWildcards)(host.toHostPort().host, allowedHosts)) {
  172. throw new error_1.MongoInvalidArgumentError(`Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join(',')}'`);
  173. }
  174. }
  175. }
  176. }
  177. this.topology = new topology_1.Topology(this, options.hosts, options);
  178. // Events can be emitted before initialization is complete so we have to
  179. // save the reference to the topology on the client ASAP if the event handlers need to access it
  180. this.topology.once(topology_1.Topology.OPEN, () => this.emit('open', this));
  181. for (const event of constants_1.MONGO_CLIENT_EVENTS) {
  182. this.topology.on(event, (...args) => this.emit(event, ...args));
  183. }
  184. const topologyConnect = async () => {
  185. try {
  186. await (0, util_1.promisify)(callback => this.topology?.connect(options, callback))();
  187. }
  188. catch (error) {
  189. this.topology?.close({ force: true });
  190. throw error;
  191. }
  192. };
  193. if (this.autoEncrypter) {
  194. await this.autoEncrypter?.init();
  195. await topologyConnect();
  196. await options.encrypter.connectInternalClient();
  197. }
  198. else {
  199. await topologyConnect();
  200. }
  201. return this;
  202. }
  203. /**
  204. * Close the client and its underlying connections
  205. *
  206. * @param force - Force close, emitting no events
  207. */
  208. async close(force = false) {
  209. // There's no way to set hasBeenClosed back to false
  210. Object.defineProperty(this.s, 'hasBeenClosed', {
  211. value: true,
  212. enumerable: true,
  213. configurable: false,
  214. writable: false
  215. });
  216. const activeSessionEnds = Array.from(this.s.activeSessions, session => session.endSession());
  217. this.s.activeSessions.clear();
  218. await Promise.all(activeSessionEnds);
  219. if (this.topology == null) {
  220. return;
  221. }
  222. // If we would attempt to select a server and get nothing back we short circuit
  223. // to avoid the server selection timeout.
  224. const selector = (0, server_selection_1.readPreferenceServerSelector)(read_preference_1.ReadPreference.primaryPreferred);
  225. const topologyDescription = this.topology.description;
  226. const serverDescriptions = Array.from(topologyDescription.servers.values());
  227. const servers = selector(topologyDescription, serverDescriptions);
  228. if (servers.length !== 0) {
  229. const endSessions = Array.from(this.s.sessionPool.sessions, ({ id }) => id);
  230. if (endSessions.length !== 0) {
  231. await (0, execute_operation_1.executeOperation)(this, new run_command_1.RunAdminCommandOperation({ endSessions }, { readPreference: read_preference_1.ReadPreference.primaryPreferred, noResponse: true })).catch(() => null); // outcome does not matter;
  232. }
  233. }
  234. // clear out references to old topology
  235. const topology = this.topology;
  236. this.topology = undefined;
  237. await new Promise((resolve, reject) => {
  238. topology.close({ force }, error => {
  239. if (error)
  240. return reject(error);
  241. const { encrypter } = this[kOptions];
  242. if (encrypter) {
  243. return encrypter.closeCallback(this, force, error => {
  244. if (error)
  245. return reject(error);
  246. resolve();
  247. });
  248. }
  249. resolve();
  250. });
  251. });
  252. }
  253. /**
  254. * Create a new Db instance sharing the current socket connections.
  255. *
  256. * @param dbName - The name of the database we want to use. If not provided, use database name from connection string.
  257. * @param options - Optional settings for Db construction
  258. */
  259. db(dbName, options) {
  260. options = options ?? {};
  261. // Default to db from connection string if not provided
  262. if (!dbName) {
  263. dbName = this.options.dbName;
  264. }
  265. // Copy the options and add out internal override of the not shared flag
  266. const finalOptions = Object.assign({}, this[kOptions], options);
  267. // Return the db object
  268. const db = new db_1.Db(this, dbName, finalOptions);
  269. // Return the database
  270. return db;
  271. }
  272. /**
  273. * Connect to MongoDB using a url
  274. *
  275. * @remarks
  276. * The programmatically provided options take precedence over the URI options.
  277. *
  278. * @see https://www.mongodb.com/docs/manual/reference/connection-string/
  279. */
  280. static async connect(url, options) {
  281. const client = new this(url, options);
  282. return client.connect();
  283. }
  284. /**
  285. * Creates a new ClientSession. When using the returned session in an operation
  286. * a corresponding ServerSession will be created.
  287. *
  288. * @remarks
  289. * A ClientSession instance may only be passed to operations being performed on the same
  290. * MongoClient it was started from.
  291. */
  292. startSession(options) {
  293. const session = new sessions_1.ClientSession(this, this.s.sessionPool, { explicit: true, ...options }, this[kOptions]);
  294. this.s.activeSessions.add(session);
  295. session.once('ended', () => {
  296. this.s.activeSessions.delete(session);
  297. });
  298. return session;
  299. }
  300. async withSession(optionsOrExecutor, executor) {
  301. const options = {
  302. // Always define an owner
  303. owner: Symbol(),
  304. // If it's an object inherit the options
  305. ...(typeof optionsOrExecutor === 'object' ? optionsOrExecutor : {})
  306. };
  307. const withSessionCallback = typeof optionsOrExecutor === 'function' ? optionsOrExecutor : executor;
  308. if (withSessionCallback == null) {
  309. throw new error_1.MongoInvalidArgumentError('Missing required callback parameter');
  310. }
  311. const session = this.startSession(options);
  312. try {
  313. return await withSessionCallback(session);
  314. }
  315. finally {
  316. try {
  317. await session.endSession();
  318. }
  319. catch {
  320. // We are not concerned with errors from endSession()
  321. }
  322. }
  323. }
  324. /**
  325. * Create a new Change Stream, watching for new changes (insertions, updates,
  326. * replacements, deletions, and invalidations) in this cluster. Will ignore all
  327. * changes to system collections, as well as the local, admin, and config databases.
  328. *
  329. * @remarks
  330. * watch() accepts two generic arguments for distinct use cases:
  331. * - The first is to provide the schema that may be defined for all the data within the current cluster
  332. * - The second is to override the shape of the change stream document entirely, if it is not provided the type will default to ChangeStreamDocument of the first argument
  333. *
  334. * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
  335. * @param options - Optional settings for the command
  336. * @typeParam TSchema - Type of the data being detected by the change stream
  337. * @typeParam TChange - Type of the whole change stream document emitted
  338. */
  339. watch(pipeline = [], options = {}) {
  340. // Allow optionally not specifying a pipeline
  341. if (!Array.isArray(pipeline)) {
  342. options = pipeline;
  343. pipeline = [];
  344. }
  345. return new change_stream_1.ChangeStream(this, pipeline, (0, utils_1.resolveOptions)(this, options));
  346. }
  347. }
  348. exports.MongoClient = MongoClient;
  349. //# sourceMappingURL=mongo_client.js.map