utils.ts 37 KB

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