utils.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.matchesParentDomain = exports.parseUnsignedInteger = exports.parseInteger = exports.compareObjectId = exports.getMongoDBClientEncryption = exports.commandSupportsReadConcern = exports.shuffle = exports.supportsRetryableWrites = exports.enumToString = exports.emitWarningOnce = exports.emitWarning = exports.MONGODB_WARNING_CODE = exports.DEFAULT_PK_FACTORY = exports.HostAddress = exports.BufferPool = exports.List = exports.deepCopy = exports.isRecord = exports.setDifference = exports.isHello = exports.isSuperset = exports.resolveOptions = exports.hasAtomicOperators = exports.calculateDurationInMs = exports.now = exports.makeStateMachine = exports.errorStrictEqual = exports.arrayStrictEqual = exports.eachAsync = exports.maxWireVersion = exports.uuidV4 = exports.databaseNamespace = exports.maybeCallback = exports.makeCounter = exports.MongoDBCollectionNamespace = exports.MongoDBNamespace = exports.ns = exports.getTopology = exports.decorateWithExplain = exports.decorateWithReadConcern = exports.decorateWithCollation = exports.isPromiseLike = exports.applyRetryableWrites = exports.filterOptions = exports.mergeOptions = exports.isObject = exports.normalizeHintField = exports.checkCollectionName = exports.hostMatchesWildcards = exports.ByteUtils = void 0;
  4. exports.request = void 0;
  5. const crypto = require("crypto");
  6. const http = require("http");
  7. const url = require("url");
  8. const url_1 = require("url");
  9. const bson_1 = require("./bson");
  10. const constants_1 = require("./cmap/wire_protocol/constants");
  11. const constants_2 = require("./constants");
  12. const error_1 = require("./error");
  13. const read_concern_1 = require("./read_concern");
  14. const read_preference_1 = require("./read_preference");
  15. const common_1 = require("./sdam/common");
  16. const write_concern_1 = require("./write_concern");
  17. exports.ByteUtils = {
  18. toLocalBufferType(buffer) {
  19. return Buffer.isBuffer(buffer)
  20. ? buffer
  21. : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
  22. },
  23. equals(seqA, seqB) {
  24. return exports.ByteUtils.toLocalBufferType(seqA).equals(seqB);
  25. },
  26. compare(seqA, seqB) {
  27. return exports.ByteUtils.toLocalBufferType(seqA).compare(seqB);
  28. },
  29. toBase64(uint8array) {
  30. return exports.ByteUtils.toLocalBufferType(uint8array).toString('base64');
  31. }
  32. };
  33. /**
  34. * Determines if a connection's address matches a user provided list
  35. * of domain wildcards.
  36. */
  37. function hostMatchesWildcards(host, wildcards) {
  38. for (const wildcard of wildcards) {
  39. if (host === wildcard ||
  40. (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) ||
  41. (wildcard.startsWith('*/') && host?.endsWith(wildcard.substring(2, wildcard.length)))) {
  42. return true;
  43. }
  44. }
  45. return false;
  46. }
  47. exports.hostMatchesWildcards = hostMatchesWildcards;
  48. /**
  49. * Throws if collectionName is not a valid mongodb collection namespace.
  50. * @internal
  51. */
  52. function checkCollectionName(collectionName) {
  53. if ('string' !== typeof collectionName) {
  54. throw new error_1.MongoInvalidArgumentError('Collection name must be a String');
  55. }
  56. if (!collectionName || collectionName.indexOf('..') !== -1) {
  57. throw new error_1.MongoInvalidArgumentError('Collection names cannot be empty');
  58. }
  59. if (collectionName.indexOf('$') !== -1 &&
  60. collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null) {
  61. // TODO(NODE-3483): Use MongoNamespace static method
  62. throw new error_1.MongoInvalidArgumentError("Collection names must not contain '$'");
  63. }
  64. if (collectionName.match(/^\.|\.$/) != null) {
  65. // TODO(NODE-3483): Use MongoNamespace static method
  66. throw new error_1.MongoInvalidArgumentError("Collection names must not start or end with '.'");
  67. }
  68. // Validate that we are not passing 0x00 in the collection name
  69. if (collectionName.indexOf('\x00') !== -1) {
  70. // TODO(NODE-3483): Use MongoNamespace static method
  71. throw new error_1.MongoInvalidArgumentError('Collection names cannot contain a null character');
  72. }
  73. }
  74. exports.checkCollectionName = checkCollectionName;
  75. /**
  76. * Ensure Hint field is in a shape we expect:
  77. * - object of index names mapping to 1 or -1
  78. * - just an index name
  79. * @internal
  80. */
  81. function normalizeHintField(hint) {
  82. let finalHint = undefined;
  83. if (typeof hint === 'string') {
  84. finalHint = hint;
  85. }
  86. else if (Array.isArray(hint)) {
  87. finalHint = {};
  88. hint.forEach(param => {
  89. finalHint[param] = 1;
  90. });
  91. }
  92. else if (hint != null && typeof hint === 'object') {
  93. finalHint = {};
  94. for (const name in hint) {
  95. finalHint[name] = hint[name];
  96. }
  97. }
  98. return finalHint;
  99. }
  100. exports.normalizeHintField = normalizeHintField;
  101. const TO_STRING = (object) => Object.prototype.toString.call(object);
  102. /**
  103. * Checks if arg is an Object:
  104. * - **NOTE**: the check is based on the `[Symbol.toStringTag]() === 'Object'`
  105. * @internal
  106. */
  107. function isObject(arg) {
  108. return '[object Object]' === TO_STRING(arg);
  109. }
  110. exports.isObject = isObject;
  111. /** @internal */
  112. function mergeOptions(target, source) {
  113. return { ...target, ...source };
  114. }
  115. exports.mergeOptions = mergeOptions;
  116. /** @internal */
  117. function filterOptions(options, names) {
  118. const filterOptions = {};
  119. for (const name in options) {
  120. if (names.includes(name)) {
  121. filterOptions[name] = options[name];
  122. }
  123. }
  124. // Filtered options
  125. return filterOptions;
  126. }
  127. exports.filterOptions = filterOptions;
  128. /**
  129. * Applies retryWrites: true to a command if retryWrites is set on the command's database.
  130. * @internal
  131. *
  132. * @param target - The target command to which we will apply retryWrites.
  133. * @param db - The database from which we can inherit a retryWrites value.
  134. */
  135. function applyRetryableWrites(target, db) {
  136. if (db && db.s.options?.retryWrites) {
  137. target.retryWrites = true;
  138. }
  139. return target;
  140. }
  141. exports.applyRetryableWrites = applyRetryableWrites;
  142. /**
  143. * Applies a write concern to a command based on well defined inheritance rules, optionally
  144. * detecting support for the write concern in the first place.
  145. * @internal
  146. *
  147. * @param target - the target command we will be applying the write concern to
  148. * @param sources - sources where we can inherit default write concerns from
  149. * @param options - optional settings passed into a command for write concern overrides
  150. */
  151. /**
  152. * Checks if a given value is a Promise
  153. *
  154. * @typeParam T - The resolution type of the possible promise
  155. * @param value - An object that could be a promise
  156. * @returns true if the provided value is a Promise
  157. */
  158. function isPromiseLike(value) {
  159. return !!value && typeof value.then === 'function';
  160. }
  161. exports.isPromiseLike = isPromiseLike;
  162. /**
  163. * Applies collation to a given command.
  164. * @internal
  165. *
  166. * @param command - the command on which to apply collation
  167. * @param target - target of command
  168. * @param options - options containing collation settings
  169. */
  170. function decorateWithCollation(command, target, options) {
  171. const capabilities = getTopology(target).capabilities;
  172. if (options.collation && typeof options.collation === 'object') {
  173. if (capabilities && capabilities.commandsTakeCollation) {
  174. command.collation = options.collation;
  175. }
  176. else {
  177. throw new error_1.MongoCompatibilityError(`Current topology does not support collation`);
  178. }
  179. }
  180. }
  181. exports.decorateWithCollation = decorateWithCollation;
  182. /**
  183. * Applies a read concern to a given command.
  184. * @internal
  185. *
  186. * @param command - the command on which to apply the read concern
  187. * @param coll - the parent collection of the operation calling this method
  188. */
  189. function decorateWithReadConcern(command, coll, options) {
  190. if (options && options.session && options.session.inTransaction()) {
  191. return;
  192. }
  193. const readConcern = Object.assign({}, command.readConcern || {});
  194. if (coll.s.readConcern) {
  195. Object.assign(readConcern, coll.s.readConcern);
  196. }
  197. if (Object.keys(readConcern).length > 0) {
  198. Object.assign(command, { readConcern: readConcern });
  199. }
  200. }
  201. exports.decorateWithReadConcern = decorateWithReadConcern;
  202. /**
  203. * Applies an explain to a given command.
  204. * @internal
  205. *
  206. * @param command - the command on which to apply the explain
  207. * @param options - the options containing the explain verbosity
  208. */
  209. function decorateWithExplain(command, explain) {
  210. if (command.explain) {
  211. return command;
  212. }
  213. return { explain: command, verbosity: explain.verbosity };
  214. }
  215. exports.decorateWithExplain = decorateWithExplain;
  216. /**
  217. * A helper function to get the topology from a given provider. Throws
  218. * if the topology cannot be found.
  219. * @throws MongoNotConnectedError
  220. * @internal
  221. */
  222. function getTopology(provider) {
  223. // MongoClient or ClientSession or AbstractCursor
  224. if ('topology' in provider && provider.topology) {
  225. return provider.topology;
  226. }
  227. else if ('client' in provider && provider.client.topology) {
  228. return provider.client.topology;
  229. }
  230. throw new error_1.MongoNotConnectedError('MongoClient must be connected to perform this operation');
  231. }
  232. exports.getTopology = getTopology;
  233. /** @internal */
  234. function ns(ns) {
  235. return MongoDBNamespace.fromString(ns);
  236. }
  237. exports.ns = ns;
  238. /** @public */
  239. class MongoDBNamespace {
  240. /**
  241. * Create a namespace object
  242. *
  243. * @param db - database name
  244. * @param collection - collection name
  245. */
  246. constructor(db, collection) {
  247. this.db = db;
  248. this.collection = collection;
  249. this.collection = collection === '' ? undefined : collection;
  250. }
  251. toString() {
  252. return this.collection ? `${this.db}.${this.collection}` : this.db;
  253. }
  254. withCollection(collection) {
  255. return new MongoDBCollectionNamespace(this.db, collection);
  256. }
  257. static fromString(namespace) {
  258. if (typeof namespace !== 'string' || namespace === '') {
  259. // TODO(NODE-3483): Replace with MongoNamespaceError
  260. throw new error_1.MongoRuntimeError(`Cannot parse namespace from "${namespace}"`);
  261. }
  262. const [db, ...collectionParts] = namespace.split('.');
  263. const collection = collectionParts.join('.');
  264. return new MongoDBNamespace(db, collection === '' ? undefined : collection);
  265. }
  266. }
  267. exports.MongoDBNamespace = MongoDBNamespace;
  268. /**
  269. * @public
  270. *
  271. * A class representing a collection's namespace. This class enforces (through Typescript) that
  272. * the `collection` portion of the namespace is defined and should only be
  273. * used in scenarios where this can be guaranteed.
  274. */
  275. class MongoDBCollectionNamespace extends MongoDBNamespace {
  276. constructor(db, collection) {
  277. super(db, collection);
  278. this.collection = collection;
  279. }
  280. }
  281. exports.MongoDBCollectionNamespace = MongoDBCollectionNamespace;
  282. /** @internal */
  283. function* makeCounter(seed = 0) {
  284. let count = seed;
  285. while (true) {
  286. const newCount = count;
  287. count += 1;
  288. yield newCount;
  289. }
  290. }
  291. exports.makeCounter = makeCounter;
  292. function maybeCallback(promiseFn, callback) {
  293. const promise = promiseFn();
  294. if (callback == null) {
  295. return promise;
  296. }
  297. promise.then(result => callback(undefined, result), error => callback(error));
  298. return;
  299. }
  300. exports.maybeCallback = maybeCallback;
  301. /** @internal */
  302. function databaseNamespace(ns) {
  303. return ns.split('.')[0];
  304. }
  305. exports.databaseNamespace = databaseNamespace;
  306. /**
  307. * Synchronously Generate a UUIDv4
  308. * @internal
  309. */
  310. function uuidV4() {
  311. const result = crypto.randomBytes(16);
  312. result[6] = (result[6] & 0x0f) | 0x40;
  313. result[8] = (result[8] & 0x3f) | 0x80;
  314. return result;
  315. }
  316. exports.uuidV4 = uuidV4;
  317. /**
  318. * A helper function for determining `maxWireVersion` between legacy and new topology instances
  319. * @internal
  320. */
  321. function maxWireVersion(topologyOrServer) {
  322. if (topologyOrServer) {
  323. if (topologyOrServer.loadBalanced) {
  324. // Since we do not have a monitor, we assume the load balanced server is always
  325. // pointed at the latest mongodb version. There is a risk that for on-prem
  326. // deployments that don't upgrade immediately that this could alert to the
  327. // application that a feature is available that is actually not.
  328. return constants_1.MAX_SUPPORTED_WIRE_VERSION;
  329. }
  330. if (topologyOrServer.hello) {
  331. return topologyOrServer.hello.maxWireVersion;
  332. }
  333. if ('lastHello' in topologyOrServer && typeof topologyOrServer.lastHello === 'function') {
  334. const lastHello = topologyOrServer.lastHello();
  335. if (lastHello) {
  336. return lastHello.maxWireVersion;
  337. }
  338. }
  339. if (topologyOrServer.description &&
  340. 'maxWireVersion' in topologyOrServer.description &&
  341. topologyOrServer.description.maxWireVersion != null) {
  342. return topologyOrServer.description.maxWireVersion;
  343. }
  344. }
  345. return 0;
  346. }
  347. exports.maxWireVersion = maxWireVersion;
  348. /**
  349. * Applies the function `eachFn` to each item in `arr`, in parallel.
  350. * @internal
  351. *
  352. * @param arr - An array of items to asynchronously iterate over
  353. * @param eachFn - A function to call on each item of the array. The callback signature is `(item, callback)`, where the callback indicates iteration is complete.
  354. * @param callback - The callback called after every item has been iterated
  355. */
  356. function eachAsync(arr, eachFn, callback) {
  357. arr = arr || [];
  358. let idx = 0;
  359. let awaiting = 0;
  360. for (idx = 0; idx < arr.length; ++idx) {
  361. awaiting++;
  362. eachFn(arr[idx], eachCallback);
  363. }
  364. if (awaiting === 0) {
  365. callback();
  366. return;
  367. }
  368. function eachCallback(err) {
  369. awaiting--;
  370. if (err) {
  371. callback(err);
  372. return;
  373. }
  374. if (idx === arr.length && awaiting <= 0) {
  375. callback();
  376. }
  377. }
  378. }
  379. exports.eachAsync = eachAsync;
  380. /** @internal */
  381. function arrayStrictEqual(arr, arr2) {
  382. if (!Array.isArray(arr) || !Array.isArray(arr2)) {
  383. return false;
  384. }
  385. return arr.length === arr2.length && arr.every((elt, idx) => elt === arr2[idx]);
  386. }
  387. exports.arrayStrictEqual = arrayStrictEqual;
  388. /** @internal */
  389. function errorStrictEqual(lhs, rhs) {
  390. if (lhs === rhs) {
  391. return true;
  392. }
  393. if (!lhs || !rhs) {
  394. return lhs === rhs;
  395. }
  396. if ((lhs == null && rhs != null) || (lhs != null && rhs == null)) {
  397. return false;
  398. }
  399. if (lhs.constructor.name !== rhs.constructor.name) {
  400. return false;
  401. }
  402. if (lhs.message !== rhs.message) {
  403. return false;
  404. }
  405. return true;
  406. }
  407. exports.errorStrictEqual = errorStrictEqual;
  408. /** @internal */
  409. function makeStateMachine(stateTable) {
  410. return function stateTransition(target, newState) {
  411. const legalStates = stateTable[target.s.state];
  412. if (legalStates && legalStates.indexOf(newState) < 0) {
  413. throw new error_1.MongoRuntimeError(`illegal state transition from [${target.s.state}] => [${newState}], allowed: [${legalStates}]`);
  414. }
  415. target.emit('stateChanged', target.s.state, newState);
  416. target.s.state = newState;
  417. };
  418. }
  419. exports.makeStateMachine = makeStateMachine;
  420. /** @internal */
  421. function now() {
  422. const hrtime = process.hrtime();
  423. return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000);
  424. }
  425. exports.now = now;
  426. /** @internal */
  427. function calculateDurationInMs(started) {
  428. if (typeof started !== 'number') {
  429. throw new error_1.MongoInvalidArgumentError('Numeric value required to calculate duration');
  430. }
  431. const elapsed = now() - started;
  432. return elapsed < 0 ? 0 : elapsed;
  433. }
  434. exports.calculateDurationInMs = calculateDurationInMs;
  435. /** @internal */
  436. function hasAtomicOperators(doc) {
  437. if (Array.isArray(doc)) {
  438. for (const document of doc) {
  439. if (hasAtomicOperators(document)) {
  440. return true;
  441. }
  442. }
  443. return false;
  444. }
  445. const keys = Object.keys(doc);
  446. return keys.length > 0 && keys[0][0] === '$';
  447. }
  448. exports.hasAtomicOperators = hasAtomicOperators;
  449. /**
  450. * Merge inherited properties from parent into options, prioritizing values from options,
  451. * then values from parent.
  452. * @internal
  453. */
  454. function resolveOptions(parent, options) {
  455. const result = Object.assign({}, options, (0, bson_1.resolveBSONOptions)(options, parent));
  456. // Users cannot pass a readConcern/writeConcern to operations in a transaction
  457. const session = options?.session;
  458. if (!session?.inTransaction()) {
  459. const readConcern = read_concern_1.ReadConcern.fromOptions(options) ?? parent?.readConcern;
  460. if (readConcern) {
  461. result.readConcern = readConcern;
  462. }
  463. const writeConcern = write_concern_1.WriteConcern.fromOptions(options) ?? parent?.writeConcern;
  464. if (writeConcern) {
  465. result.writeConcern = writeConcern;
  466. }
  467. }
  468. const readPreference = read_preference_1.ReadPreference.fromOptions(options) ?? parent?.readPreference;
  469. if (readPreference) {
  470. result.readPreference = readPreference;
  471. }
  472. return result;
  473. }
  474. exports.resolveOptions = resolveOptions;
  475. function isSuperset(set, subset) {
  476. set = Array.isArray(set) ? new Set(set) : set;
  477. subset = Array.isArray(subset) ? new Set(subset) : subset;
  478. for (const elem of subset) {
  479. if (!set.has(elem)) {
  480. return false;
  481. }
  482. }
  483. return true;
  484. }
  485. exports.isSuperset = isSuperset;
  486. /**
  487. * Checks if the document is a Hello request
  488. * @internal
  489. */
  490. function isHello(doc) {
  491. return doc[constants_2.LEGACY_HELLO_COMMAND] || doc.hello ? true : false;
  492. }
  493. exports.isHello = isHello;
  494. /** Returns the items that are uniquely in setA */
  495. function setDifference(setA, setB) {
  496. const difference = new Set(setA);
  497. for (const elem of setB) {
  498. difference.delete(elem);
  499. }
  500. return difference;
  501. }
  502. exports.setDifference = setDifference;
  503. const HAS_OWN = (object, prop) => Object.prototype.hasOwnProperty.call(object, prop);
  504. function isRecord(value, requiredKeys = undefined) {
  505. if (!isObject(value)) {
  506. return false;
  507. }
  508. const ctor = value.constructor;
  509. if (ctor && ctor.prototype) {
  510. if (!isObject(ctor.prototype)) {
  511. return false;
  512. }
  513. // Check to see if some method exists from the Object exists
  514. if (!HAS_OWN(ctor.prototype, 'isPrototypeOf')) {
  515. return false;
  516. }
  517. }
  518. if (requiredKeys) {
  519. const keys = Object.keys(value);
  520. return isSuperset(keys, requiredKeys);
  521. }
  522. return true;
  523. }
  524. exports.isRecord = isRecord;
  525. /**
  526. * Make a deep copy of an object
  527. *
  528. * NOTE: This is not meant to be the perfect implementation of a deep copy,
  529. * but instead something that is good enough for the purposes of
  530. * command monitoring.
  531. */
  532. function deepCopy(value) {
  533. if (value == null) {
  534. return value;
  535. }
  536. else if (Array.isArray(value)) {
  537. return value.map(item => deepCopy(item));
  538. }
  539. else if (isRecord(value)) {
  540. const res = {};
  541. for (const key in value) {
  542. res[key] = deepCopy(value[key]);
  543. }
  544. return res;
  545. }
  546. const ctor = value.constructor;
  547. if (ctor) {
  548. switch (ctor.name.toLowerCase()) {
  549. case 'date':
  550. return new ctor(Number(value));
  551. case 'map':
  552. return new Map(value);
  553. case 'set':
  554. return new Set(value);
  555. case 'buffer':
  556. return Buffer.from(value);
  557. }
  558. }
  559. return value;
  560. }
  561. exports.deepCopy = deepCopy;
  562. /**
  563. * A sequential list of items in a circularly linked list
  564. * @remarks
  565. * The head node is special, it is always defined and has a value of null.
  566. * It is never "included" in the list, in that, it is not returned by pop/shift or yielded by the iterator.
  567. * The circular linkage and always defined head node are to reduce checks for null next/prev references to zero.
  568. * New nodes are declared as object literals with keys always in the same order: next, prev, value.
  569. * @internal
  570. */
  571. class List {
  572. get length() {
  573. return this.count;
  574. }
  575. get [Symbol.toStringTag]() {
  576. return 'List';
  577. }
  578. constructor() {
  579. this.count = 0;
  580. // this is carefully crafted:
  581. // declaring a complete and consistently key ordered
  582. // object is beneficial to the runtime optimizations
  583. this.head = {
  584. next: null,
  585. prev: null,
  586. value: null
  587. };
  588. this.head.next = this.head;
  589. this.head.prev = this.head;
  590. }
  591. toArray() {
  592. return Array.from(this);
  593. }
  594. toString() {
  595. return `head <=> ${this.toArray().join(' <=> ')} <=> head`;
  596. }
  597. *[Symbol.iterator]() {
  598. for (const node of this.nodes()) {
  599. yield node.value;
  600. }
  601. }
  602. *nodes() {
  603. let ptr = this.head.next;
  604. while (ptr !== this.head) {
  605. // Save next before yielding so that we make removing within iteration safe
  606. const { next } = ptr;
  607. yield ptr;
  608. ptr = next;
  609. }
  610. }
  611. /** Insert at end of list */
  612. push(value) {
  613. this.count += 1;
  614. const newNode = {
  615. next: this.head,
  616. prev: this.head.prev,
  617. value
  618. };
  619. this.head.prev.next = newNode;
  620. this.head.prev = newNode;
  621. }
  622. /** Inserts every item inside an iterable instead of the iterable itself */
  623. pushMany(iterable) {
  624. for (const value of iterable) {
  625. this.push(value);
  626. }
  627. }
  628. /** Insert at front of list */
  629. unshift(value) {
  630. this.count += 1;
  631. const newNode = {
  632. next: this.head.next,
  633. prev: this.head,
  634. value
  635. };
  636. this.head.next.prev = newNode;
  637. this.head.next = newNode;
  638. }
  639. remove(node) {
  640. if (node === this.head || this.length === 0) {
  641. return null;
  642. }
  643. this.count -= 1;
  644. const prevNode = node.prev;
  645. const nextNode = node.next;
  646. prevNode.next = nextNode;
  647. nextNode.prev = prevNode;
  648. return node.value;
  649. }
  650. /** Removes the first node at the front of the list */
  651. shift() {
  652. return this.remove(this.head.next);
  653. }
  654. /** Removes the last node at the end of the list */
  655. pop() {
  656. return this.remove(this.head.prev);
  657. }
  658. /** Iterates through the list and removes nodes where filter returns true */
  659. prune(filter) {
  660. for (const node of this.nodes()) {
  661. if (filter(node.value)) {
  662. this.remove(node);
  663. }
  664. }
  665. }
  666. clear() {
  667. this.count = 0;
  668. this.head.next = this.head;
  669. this.head.prev = this.head;
  670. }
  671. /** Returns the first item in the list, does not remove */
  672. first() {
  673. // If the list is empty, value will be the head's null
  674. return this.head.next.value;
  675. }
  676. /** Returns the last item in the list, does not remove */
  677. last() {
  678. // If the list is empty, value will be the head's null
  679. return this.head.prev.value;
  680. }
  681. }
  682. exports.List = List;
  683. /**
  684. * A pool of Buffers which allow you to read them as if they were one
  685. * @internal
  686. */
  687. class BufferPool {
  688. constructor() {
  689. this.buffers = new List();
  690. this.totalByteLength = 0;
  691. }
  692. get length() {
  693. return this.totalByteLength;
  694. }
  695. /** Adds a buffer to the internal buffer pool list */
  696. append(buffer) {
  697. this.buffers.push(buffer);
  698. this.totalByteLength += buffer.length;
  699. }
  700. /**
  701. * If BufferPool contains 4 bytes or more construct an int32 from the leading bytes,
  702. * otherwise return null. Size can be negative, caller should error check.
  703. */
  704. getInt32() {
  705. if (this.totalByteLength < 4) {
  706. return null;
  707. }
  708. const firstBuffer = this.buffers.first();
  709. if (firstBuffer != null && firstBuffer.byteLength >= 4) {
  710. return firstBuffer.readInt32LE(0);
  711. }
  712. // Unlikely case: an int32 is split across buffers.
  713. // Use read and put the returned buffer back on top
  714. const top4Bytes = this.read(4);
  715. const value = top4Bytes.readInt32LE(0);
  716. // Put it back.
  717. this.totalByteLength += 4;
  718. this.buffers.unshift(top4Bytes);
  719. return value;
  720. }
  721. /** Reads the requested number of bytes, optionally consuming them */
  722. read(size) {
  723. if (typeof size !== 'number' || size < 0) {
  724. throw new error_1.MongoInvalidArgumentError('Argument "size" must be a non-negative number');
  725. }
  726. // oversized request returns empty buffer
  727. if (size > this.totalByteLength) {
  728. return Buffer.alloc(0);
  729. }
  730. // We know we have enough, we just don't know how it is spread across chunks
  731. // TODO(NODE-4732): alloc API should change based on raw option
  732. const result = Buffer.allocUnsafe(size);
  733. for (let bytesRead = 0; bytesRead < size;) {
  734. const buffer = this.buffers.shift();
  735. if (buffer == null) {
  736. break;
  737. }
  738. const bytesRemaining = size - bytesRead;
  739. const bytesReadable = Math.min(bytesRemaining, buffer.byteLength);
  740. const bytes = buffer.subarray(0, bytesReadable);
  741. result.set(bytes, bytesRead);
  742. bytesRead += bytesReadable;
  743. this.totalByteLength -= bytesReadable;
  744. if (bytesReadable < buffer.byteLength) {
  745. this.buffers.unshift(buffer.subarray(bytesReadable));
  746. }
  747. }
  748. return result;
  749. }
  750. }
  751. exports.BufferPool = BufferPool;
  752. /** @public */
  753. class HostAddress {
  754. constructor(hostString) {
  755. this.host = undefined;
  756. this.port = undefined;
  757. this.socketPath = undefined;
  758. this.isIPv6 = false;
  759. const escapedHost = hostString.split(' ').join('%20'); // escape spaces, for socket path hosts
  760. if (escapedHost.endsWith('.sock')) {
  761. // heuristically determine if we're working with a domain socket
  762. this.socketPath = decodeURIComponent(escapedHost);
  763. return;
  764. }
  765. const urlString = `iLoveJS://${escapedHost}`;
  766. let url;
  767. try {
  768. url = new url_1.URL(urlString);
  769. }
  770. catch (urlError) {
  771. const runtimeError = new error_1.MongoRuntimeError(`Unable to parse ${escapedHost} with URL`);
  772. runtimeError.cause = urlError;
  773. throw runtimeError;
  774. }
  775. const hostname = url.hostname;
  776. const port = url.port;
  777. let normalized = decodeURIComponent(hostname).toLowerCase();
  778. if (normalized.startsWith('[') && normalized.endsWith(']')) {
  779. this.isIPv6 = true;
  780. normalized = normalized.substring(1, hostname.length - 1);
  781. }
  782. this.host = normalized.toLowerCase();
  783. if (typeof port === 'number') {
  784. this.port = port;
  785. }
  786. else if (typeof port === 'string' && port !== '') {
  787. this.port = Number.parseInt(port, 10);
  788. }
  789. else {
  790. this.port = 27017;
  791. }
  792. if (this.port === 0) {
  793. throw new error_1.MongoParseError('Invalid port (zero) with hostname');
  794. }
  795. Object.freeze(this);
  796. }
  797. [Symbol.for('nodejs.util.inspect.custom')]() {
  798. return this.inspect();
  799. }
  800. inspect() {
  801. return `new HostAddress('${this.toString()}')`;
  802. }
  803. toString() {
  804. if (typeof this.host === 'string') {
  805. if (this.isIPv6) {
  806. return `[${this.host}]:${this.port}`;
  807. }
  808. return `${this.host}:${this.port}`;
  809. }
  810. return `${this.socketPath}`;
  811. }
  812. static fromString(s) {
  813. return new HostAddress(s);
  814. }
  815. static fromHostPort(host, port) {
  816. if (host.includes(':')) {
  817. host = `[${host}]`; // IPv6 address
  818. }
  819. return HostAddress.fromString(`${host}:${port}`);
  820. }
  821. static fromSrvRecord({ name, port }) {
  822. return HostAddress.fromHostPort(name, port);
  823. }
  824. toHostPort() {
  825. if (this.socketPath) {
  826. return { host: this.socketPath, port: 0 };
  827. }
  828. const host = this.host ?? '';
  829. const port = this.port ?? 0;
  830. return { host, port };
  831. }
  832. }
  833. exports.HostAddress = HostAddress;
  834. exports.DEFAULT_PK_FACTORY = {
  835. // We prefer not to rely on ObjectId having a createPk method
  836. createPk() {
  837. return new bson_1.ObjectId();
  838. }
  839. };
  840. /**
  841. * When the driver used emitWarning the code will be equal to this.
  842. * @public
  843. *
  844. * @example
  845. * ```ts
  846. * process.on('warning', (warning) => {
  847. * if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)')
  848. * })
  849. * ```
  850. */
  851. exports.MONGODB_WARNING_CODE = 'MONGODB DRIVER';
  852. /** @internal */
  853. function emitWarning(message) {
  854. return process.emitWarning(message, { code: exports.MONGODB_WARNING_CODE });
  855. }
  856. exports.emitWarning = emitWarning;
  857. const emittedWarnings = new Set();
  858. /**
  859. * Will emit a warning once for the duration of the application.
  860. * Uses the message to identify if it has already been emitted
  861. * so using string interpolation can cause multiple emits
  862. * @internal
  863. */
  864. function emitWarningOnce(message) {
  865. if (!emittedWarnings.has(message)) {
  866. emittedWarnings.add(message);
  867. return emitWarning(message);
  868. }
  869. }
  870. exports.emitWarningOnce = emitWarningOnce;
  871. /**
  872. * Takes a JS object and joins the values into a string separated by ', '
  873. */
  874. function enumToString(en) {
  875. return Object.values(en).join(', ');
  876. }
  877. exports.enumToString = enumToString;
  878. /**
  879. * Determine if a server supports retryable writes.
  880. *
  881. * @internal
  882. */
  883. function supportsRetryableWrites(server) {
  884. if (!server) {
  885. return false;
  886. }
  887. if (server.loadBalanced) {
  888. // Loadbalanced topologies will always support retry writes
  889. return true;
  890. }
  891. if (server.description.logicalSessionTimeoutMinutes != null) {
  892. // that supports sessions
  893. if (server.description.type !== common_1.ServerType.Standalone) {
  894. // and that is not a standalone
  895. return true;
  896. }
  897. }
  898. return false;
  899. }
  900. exports.supportsRetryableWrites = supportsRetryableWrites;
  901. /**
  902. * Fisher–Yates Shuffle
  903. *
  904. * Reference: https://bost.ocks.org/mike/shuffle/
  905. * @param sequence - items to be shuffled
  906. * @param limit - Defaults to `0`. If nonzero shuffle will slice the randomized array e.g, `.slice(0, limit)` otherwise will return the entire randomized array.
  907. */
  908. function shuffle(sequence, limit = 0) {
  909. const items = Array.from(sequence); // shallow copy in order to never shuffle the input
  910. if (limit > items.length) {
  911. throw new error_1.MongoRuntimeError('Limit must be less than the number of items');
  912. }
  913. let remainingItemsToShuffle = items.length;
  914. const lowerBound = limit % items.length === 0 ? 1 : items.length - limit;
  915. while (remainingItemsToShuffle > lowerBound) {
  916. // Pick a remaining element
  917. const randomIndex = Math.floor(Math.random() * remainingItemsToShuffle);
  918. remainingItemsToShuffle -= 1;
  919. // And swap it with the current element
  920. const swapHold = items[remainingItemsToShuffle];
  921. items[remainingItemsToShuffle] = items[randomIndex];
  922. items[randomIndex] = swapHold;
  923. }
  924. return limit % items.length === 0 ? items : items.slice(lowerBound);
  925. }
  926. exports.shuffle = shuffle;
  927. // TODO(NODE-4936): read concern eligibility for commands should be codified in command construction
  928. // @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.rst#read-concern
  929. function commandSupportsReadConcern(command, options) {
  930. if (command.aggregate || command.count || command.distinct || command.find || command.geoNear) {
  931. return true;
  932. }
  933. if (command.mapReduce &&
  934. options &&
  935. options.out &&
  936. (options.out.inline === 1 || options.out === 'inline')) {
  937. return true;
  938. }
  939. return false;
  940. }
  941. exports.commandSupportsReadConcern = commandSupportsReadConcern;
  942. /** A utility function to get the instance of mongodb-client-encryption, if it exists. */
  943. function getMongoDBClientEncryption() {
  944. let mongodbClientEncryption = null;
  945. // NOTE(NODE-4254): This is to get around the circular dependency between
  946. // mongodb-client-encryption and the driver in the test scenarios.
  947. if (typeof process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE === 'string' &&
  948. process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE.length > 0) {
  949. try {
  950. // NOTE(NODE-3199): Ensure you always wrap an optional require literally in the try block
  951. // Cannot be moved to helper utility function, bundlers search and replace the actual require call
  952. // in a way that makes this line throw at bundle time, not runtime, catching here will make bundling succeed
  953. mongodbClientEncryption = require(process.env.MONGODB_CLIENT_ENCRYPTION_OVERRIDE);
  954. }
  955. catch {
  956. // ignore
  957. }
  958. }
  959. else {
  960. try {
  961. // NOTE(NODE-3199): Ensure you always wrap an optional require literally in the try block
  962. // Cannot be moved to helper utility function, bundlers search and replace the actual require call
  963. // in a way that makes this line throw at bundle time, not runtime, catching here will make bundling succeed
  964. mongodbClientEncryption = require('mongodb-client-encryption');
  965. }
  966. catch {
  967. // ignore
  968. }
  969. }
  970. return mongodbClientEncryption;
  971. }
  972. exports.getMongoDBClientEncryption = getMongoDBClientEncryption;
  973. /**
  974. * Compare objectIds. `null` is always less
  975. * - `+1 = oid1 is greater than oid2`
  976. * - `-1 = oid1 is less than oid2`
  977. * - `+0 = oid1 is equal oid2`
  978. */
  979. function compareObjectId(oid1, oid2) {
  980. if (oid1 == null && oid2 == null) {
  981. return 0;
  982. }
  983. if (oid1 == null) {
  984. return -1;
  985. }
  986. if (oid2 == null) {
  987. return 1;
  988. }
  989. return exports.ByteUtils.compare(oid1.id, oid2.id);
  990. }
  991. exports.compareObjectId = compareObjectId;
  992. function parseInteger(value) {
  993. if (typeof value === 'number')
  994. return Math.trunc(value);
  995. const parsedValue = Number.parseInt(String(value), 10);
  996. return Number.isNaN(parsedValue) ? null : parsedValue;
  997. }
  998. exports.parseInteger = parseInteger;
  999. function parseUnsignedInteger(value) {
  1000. const parsedInt = parseInteger(value);
  1001. return parsedInt != null && parsedInt >= 0 ? parsedInt : null;
  1002. }
  1003. exports.parseUnsignedInteger = parseUnsignedInteger;
  1004. /**
  1005. * Determines whether a provided address matches the provided parent domain.
  1006. *
  1007. * If a DNS server were to become compromised SRV records would still need to
  1008. * advertise addresses that are under the same domain as the srvHost.
  1009. *
  1010. * @param address - The address to check against a domain
  1011. * @param srvHost - The domain to check the provided address against
  1012. * @returns Whether the provided address matches the parent domain
  1013. */
  1014. function matchesParentDomain(address, srvHost) {
  1015. // Remove trailing dot if exists on either the resolved address or the srv hostname
  1016. const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address;
  1017. const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost;
  1018. const allCharacterBeforeFirstDot = /^.*?\./;
  1019. // Remove all characters before first dot
  1020. // Add leading dot back to string so
  1021. // an srvHostDomain = '.trusted.site'
  1022. // will not satisfy an addressDomain that endsWith '.fake-trusted.site'
  1023. const addressDomain = `.${normalizedAddress.replace(allCharacterBeforeFirstDot, '')}`;
  1024. const srvHostDomain = `.${normalizedSrvHost.replace(allCharacterBeforeFirstDot, '')}`;
  1025. return addressDomain.endsWith(srvHostDomain);
  1026. }
  1027. exports.matchesParentDomain = matchesParentDomain;
  1028. async function request(uri, options = {}) {
  1029. return new Promise((resolve, reject) => {
  1030. const requestOptions = {
  1031. method: 'GET',
  1032. timeout: 10000,
  1033. json: true,
  1034. ...url.parse(uri),
  1035. ...options
  1036. };
  1037. const req = http.request(requestOptions, res => {
  1038. res.setEncoding('utf8');
  1039. let data = '';
  1040. res.on('data', d => {
  1041. data += d;
  1042. });
  1043. res.once('end', () => {
  1044. if (options.json === false) {
  1045. resolve(data);
  1046. return;
  1047. }
  1048. try {
  1049. const parsed = JSON.parse(data);
  1050. resolve(parsed);
  1051. }
  1052. catch {
  1053. // TODO(NODE-3483)
  1054. reject(new error_1.MongoRuntimeError(`Invalid JSON response: "${data}"`));
  1055. }
  1056. });
  1057. });
  1058. req.once('timeout', () => req.destroy(new error_1.MongoNetworkTimeoutError(`Network request to ${uri} timed out after ${options.timeout} ms`)));
  1059. req.once('error', error => reject(error));
  1060. req.end();
  1061. });
  1062. }
  1063. exports.request = request;
  1064. //# sourceMappingURL=utils.js.map