You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1196 lines
41 KiB
1196 lines
41 KiB
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.kDispose = exports.randomBytes = exports.COSMOS_DB_MSG = exports.DOCUMENT_DB_MSG = exports.COSMOS_DB_CHECK = exports.DOCUMENT_DB_CHECK = exports.MONGODB_WARNING_CODE = exports.DEFAULT_PK_FACTORY = exports.HostAddress = exports.BufferPool = exports.List = exports.MongoDBCollectionNamespace = exports.MongoDBNamespace = exports.ByteUtils = void 0; |
|
exports.isUint8Array = isUint8Array; |
|
exports.hostMatchesWildcards = hostMatchesWildcards; |
|
exports.normalizeHintField = normalizeHintField; |
|
exports.isObject = isObject; |
|
exports.mergeOptions = mergeOptions; |
|
exports.filterOptions = filterOptions; |
|
exports.applyRetryableWrites = applyRetryableWrites; |
|
exports.isPromiseLike = isPromiseLike; |
|
exports.decorateWithCollation = decorateWithCollation; |
|
exports.decorateWithReadConcern = decorateWithReadConcern; |
|
exports.getTopology = getTopology; |
|
exports.ns = ns; |
|
exports.makeCounter = makeCounter; |
|
exports.uuidV4 = uuidV4; |
|
exports.maxWireVersion = maxWireVersion; |
|
exports.arrayStrictEqual = arrayStrictEqual; |
|
exports.errorStrictEqual = errorStrictEqual; |
|
exports.makeStateMachine = makeStateMachine; |
|
exports.now = now; |
|
exports.calculateDurationInMs = calculateDurationInMs; |
|
exports.hasAtomicOperators = hasAtomicOperators; |
|
exports.resolveTimeoutOptions = resolveTimeoutOptions; |
|
exports.resolveOptions = resolveOptions; |
|
exports.isSuperset = isSuperset; |
|
exports.isHello = isHello; |
|
exports.setDifference = setDifference; |
|
exports.isRecord = isRecord; |
|
exports.emitWarning = emitWarning; |
|
exports.emitWarningOnce = emitWarningOnce; |
|
exports.enumToString = enumToString; |
|
exports.supportsRetryableWrites = supportsRetryableWrites; |
|
exports.shuffle = shuffle; |
|
exports.commandSupportsReadConcern = commandSupportsReadConcern; |
|
exports.compareObjectId = compareObjectId; |
|
exports.parseInteger = parseInteger; |
|
exports.parseUnsignedInteger = parseUnsignedInteger; |
|
exports.checkParentDomainMatch = checkParentDomainMatch; |
|
exports.get = get; |
|
exports.request = request; |
|
exports.isHostMatch = isHostMatch; |
|
exports.promiseWithResolvers = promiseWithResolvers; |
|
exports.squashError = squashError; |
|
exports.once = once; |
|
exports.maybeAddIdToDocuments = maybeAddIdToDocuments; |
|
exports.fileIsAccessible = fileIsAccessible; |
|
exports.csotMin = csotMin; |
|
exports.noop = noop; |
|
exports.decorateDecryptionResult = decorateDecryptionResult; |
|
exports.addAbortListener = addAbortListener; |
|
exports.abortable = abortable; |
|
const crypto = require("crypto"); |
|
const fs_1 = require("fs"); |
|
const http = require("http"); |
|
const timers_1 = require("timers"); |
|
const url = require("url"); |
|
const url_1 = require("url"); |
|
const util_1 = require("util"); |
|
const bson_1 = require("./bson"); |
|
const constants_1 = require("./cmap/wire_protocol/constants"); |
|
const constants_2 = require("./constants"); |
|
const error_1 = require("./error"); |
|
const read_concern_1 = require("./read_concern"); |
|
const read_preference_1 = require("./read_preference"); |
|
const common_1 = require("./sdam/common"); |
|
const write_concern_1 = require("./write_concern"); |
|
exports.ByteUtils = { |
|
toLocalBufferType(buffer) { |
|
return Buffer.isBuffer(buffer) |
|
? buffer |
|
: Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength); |
|
}, |
|
equals(seqA, seqB) { |
|
return exports.ByteUtils.toLocalBufferType(seqA).equals(seqB); |
|
}, |
|
compare(seqA, seqB) { |
|
return exports.ByteUtils.toLocalBufferType(seqA).compare(seqB); |
|
}, |
|
toBase64(uint8array) { |
|
return exports.ByteUtils.toLocalBufferType(uint8array).toString('base64'); |
|
} |
|
}; |
|
/** |
|
* Returns true if value is a Uint8Array or a Buffer |
|
* @param value - any value that may be a Uint8Array |
|
*/ |
|
function isUint8Array(value) { |
|
return (value != null && |
|
typeof value === 'object' && |
|
Symbol.toStringTag in value && |
|
value[Symbol.toStringTag] === 'Uint8Array'); |
|
} |
|
/** |
|
* Determines if a connection's address matches a user provided list |
|
* of domain wildcards. |
|
*/ |
|
function hostMatchesWildcards(host, wildcards) { |
|
for (const wildcard of wildcards) { |
|
if (host === wildcard || |
|
(wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) || |
|
(wildcard.startsWith('*/') && host?.endsWith(wildcard.substring(2, wildcard.length)))) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
/** |
|
* Ensure Hint field is in a shape we expect: |
|
* - object of index names mapping to 1 or -1 |
|
* - just an index name |
|
* @internal |
|
*/ |
|
function normalizeHintField(hint) { |
|
let finalHint = undefined; |
|
if (typeof hint === 'string') { |
|
finalHint = hint; |
|
} |
|
else if (Array.isArray(hint)) { |
|
finalHint = {}; |
|
hint.forEach(param => { |
|
finalHint[param] = 1; |
|
}); |
|
} |
|
else if (hint != null && typeof hint === 'object') { |
|
finalHint = {}; |
|
for (const name in hint) { |
|
finalHint[name] = hint[name]; |
|
} |
|
} |
|
return finalHint; |
|
} |
|
const TO_STRING = (object) => Object.prototype.toString.call(object); |
|
/** |
|
* Checks if arg is an Object: |
|
* - **NOTE**: the check is based on the `[Symbol.toStringTag]() === 'Object'` |
|
* @internal |
|
*/ |
|
function isObject(arg) { |
|
return '[object Object]' === TO_STRING(arg); |
|
} |
|
/** @internal */ |
|
function mergeOptions(target, source) { |
|
return { ...target, ...source }; |
|
} |
|
/** @internal */ |
|
function filterOptions(options, names) { |
|
const filterOptions = {}; |
|
for (const name in options) { |
|
if (names.includes(name)) { |
|
filterOptions[name] = options[name]; |
|
} |
|
} |
|
// Filtered options |
|
return filterOptions; |
|
} |
|
/** |
|
* Applies retryWrites: true to a command if retryWrites is set on the command's database. |
|
* @internal |
|
* |
|
* @param target - The target command to which we will apply retryWrites. |
|
* @param db - The database from which we can inherit a retryWrites value. |
|
*/ |
|
function applyRetryableWrites(target, db) { |
|
if (db && db.s.options?.retryWrites) { |
|
target.retryWrites = true; |
|
} |
|
return target; |
|
} |
|
/** |
|
* Applies a write concern to a command based on well defined inheritance rules, optionally |
|
* detecting support for the write concern in the first place. |
|
* @internal |
|
* |
|
* @param target - the target command we will be applying the write concern to |
|
* @param sources - sources where we can inherit default write concerns from |
|
* @param options - optional settings passed into a command for write concern overrides |
|
*/ |
|
/** |
|
* Checks if a given value is a Promise |
|
* |
|
* @typeParam T - The resolution type of the possible promise |
|
* @param value - An object that could be a promise |
|
* @returns true if the provided value is a Promise |
|
*/ |
|
function isPromiseLike(value) { |
|
return (value != null && |
|
typeof value === 'object' && |
|
'then' in value && |
|
typeof value.then === 'function'); |
|
} |
|
/** |
|
* Applies collation to a given command. |
|
* @internal |
|
* |
|
* @param command - the command on which to apply collation |
|
* @param target - target of command |
|
* @param options - options containing collation settings |
|
*/ |
|
function decorateWithCollation(command, target, options) { |
|
const capabilities = getTopology(target).capabilities; |
|
if (options.collation && typeof options.collation === 'object') { |
|
if (capabilities && capabilities.commandsTakeCollation) { |
|
command.collation = options.collation; |
|
} |
|
else { |
|
throw new error_1.MongoCompatibilityError(`Current topology does not support collation`); |
|
} |
|
} |
|
} |
|
/** |
|
* Applies a read concern to a given command. |
|
* @internal |
|
* |
|
* @param command - the command on which to apply the read concern |
|
* @param coll - the parent collection of the operation calling this method |
|
*/ |
|
function decorateWithReadConcern(command, coll, options) { |
|
if (options && options.session && options.session.inTransaction()) { |
|
return; |
|
} |
|
const readConcern = Object.assign({}, command.readConcern || {}); |
|
if (coll.s.readConcern) { |
|
Object.assign(readConcern, coll.s.readConcern); |
|
} |
|
if (Object.keys(readConcern).length > 0) { |
|
Object.assign(command, { readConcern: readConcern }); |
|
} |
|
} |
|
/** |
|
* A helper function to get the topology from a given provider. Throws |
|
* if the topology cannot be found. |
|
* @throws MongoNotConnectedError |
|
* @internal |
|
*/ |
|
function getTopology(provider) { |
|
// MongoClient or ClientSession or AbstractCursor |
|
if ('topology' in provider && provider.topology) { |
|
return provider.topology; |
|
} |
|
else if ('client' in provider && provider.client.topology) { |
|
return provider.client.topology; |
|
} |
|
throw new error_1.MongoNotConnectedError('MongoClient must be connected to perform this operation'); |
|
} |
|
/** @internal */ |
|
function ns(ns) { |
|
return MongoDBNamespace.fromString(ns); |
|
} |
|
/** @public */ |
|
class MongoDBNamespace { |
|
/** |
|
* Create a namespace object |
|
* |
|
* @param db - database name |
|
* @param collection - collection name |
|
*/ |
|
constructor(db, collection) { |
|
this.db = db; |
|
this.collection = collection; |
|
this.collection = collection === '' ? undefined : collection; |
|
} |
|
toString() { |
|
return this.collection ? `${this.db}.${this.collection}` : this.db; |
|
} |
|
withCollection(collection) { |
|
return new MongoDBCollectionNamespace(this.db, collection); |
|
} |
|
static fromString(namespace) { |
|
if (typeof namespace !== 'string' || namespace === '') { |
|
// TODO(NODE-3483): Replace with MongoNamespaceError |
|
throw new error_1.MongoRuntimeError(`Cannot parse namespace from "${namespace}"`); |
|
} |
|
const [db, ...collectionParts] = namespace.split('.'); |
|
const collection = collectionParts.join('.'); |
|
return new MongoDBNamespace(db, collection === '' ? undefined : collection); |
|
} |
|
} |
|
exports.MongoDBNamespace = MongoDBNamespace; |
|
/** |
|
* @public |
|
* |
|
* A class representing a collection's namespace. This class enforces (through Typescript) that |
|
* the `collection` portion of the namespace is defined and should only be |
|
* used in scenarios where this can be guaranteed. |
|
*/ |
|
class MongoDBCollectionNamespace extends MongoDBNamespace { |
|
constructor(db, collection) { |
|
super(db, collection); |
|
this.collection = collection; |
|
} |
|
static fromString(namespace) { |
|
return super.fromString(namespace); |
|
} |
|
} |
|
exports.MongoDBCollectionNamespace = MongoDBCollectionNamespace; |
|
/** @internal */ |
|
function* makeCounter(seed = 0) { |
|
let count = seed; |
|
while (true) { |
|
const newCount = count; |
|
count += 1; |
|
yield newCount; |
|
} |
|
} |
|
/** |
|
* Synchronously Generate a UUIDv4 |
|
* @internal |
|
*/ |
|
function uuidV4() { |
|
const result = crypto.randomBytes(16); |
|
result[6] = (result[6] & 0x0f) | 0x40; |
|
result[8] = (result[8] & 0x3f) | 0x80; |
|
return result; |
|
} |
|
/** |
|
* A helper function for determining `maxWireVersion` between legacy and new topology instances |
|
* @internal |
|
*/ |
|
function maxWireVersion(topologyOrServer) { |
|
if (topologyOrServer) { |
|
if (topologyOrServer.loadBalanced || topologyOrServer.serverApi?.version) { |
|
// Since we do not have a monitor in the load balanced mode, |
|
// we assume the load-balanced server is always pointed at the latest mongodb version. |
|
// There is a risk that for on-prem deployments |
|
// that don't upgrade immediately that this could alert to the |
|
// application that a feature is available that is actually not. |
|
// We also return the max supported wire version for serverAPI. |
|
return constants_1.MAX_SUPPORTED_WIRE_VERSION; |
|
} |
|
if (topologyOrServer.hello) { |
|
return topologyOrServer.hello.maxWireVersion; |
|
} |
|
if ('lastHello' in topologyOrServer && typeof topologyOrServer.lastHello === 'function') { |
|
const lastHello = topologyOrServer.lastHello(); |
|
if (lastHello) { |
|
return lastHello.maxWireVersion; |
|
} |
|
} |
|
if (topologyOrServer.description && |
|
'maxWireVersion' in topologyOrServer.description && |
|
topologyOrServer.description.maxWireVersion != null) { |
|
return topologyOrServer.description.maxWireVersion; |
|
} |
|
} |
|
return 0; |
|
} |
|
/** @internal */ |
|
function arrayStrictEqual(arr, arr2) { |
|
if (!Array.isArray(arr) || !Array.isArray(arr2)) { |
|
return false; |
|
} |
|
return arr.length === arr2.length && arr.every((elt, idx) => elt === arr2[idx]); |
|
} |
|
/** @internal */ |
|
function errorStrictEqual(lhs, rhs) { |
|
if (lhs === rhs) { |
|
return true; |
|
} |
|
if (!lhs || !rhs) { |
|
return lhs === rhs; |
|
} |
|
if ((lhs == null && rhs != null) || (lhs != null && rhs == null)) { |
|
return false; |
|
} |
|
if (lhs.constructor.name !== rhs.constructor.name) { |
|
return false; |
|
} |
|
if (lhs.message !== rhs.message) { |
|
return false; |
|
} |
|
return true; |
|
} |
|
/** @internal */ |
|
function makeStateMachine(stateTable) { |
|
return function stateTransition(target, newState) { |
|
const legalStates = stateTable[target.s.state]; |
|
if (legalStates && legalStates.indexOf(newState) < 0) { |
|
throw new error_1.MongoRuntimeError(`illegal state transition from [${target.s.state}] => [${newState}], allowed: [${legalStates}]`); |
|
} |
|
target.emit('stateChanged', target.s.state, newState); |
|
target.s.state = newState; |
|
}; |
|
} |
|
/** @internal */ |
|
function now() { |
|
const hrtime = process.hrtime(); |
|
return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000); |
|
} |
|
/** @internal */ |
|
function calculateDurationInMs(started) { |
|
if (typeof started !== 'number') { |
|
return -1; |
|
} |
|
const elapsed = now() - started; |
|
return elapsed < 0 ? 0 : elapsed; |
|
} |
|
/** @internal */ |
|
function hasAtomicOperators(doc) { |
|
if (Array.isArray(doc)) { |
|
for (const document of doc) { |
|
if (hasAtomicOperators(document)) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
const keys = Object.keys(doc); |
|
return keys.length > 0 && keys[0][0] === '$'; |
|
} |
|
function resolveTimeoutOptions(client, options) { |
|
const { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS } = client.s.options; |
|
return { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS, ...options }; |
|
} |
|
/** |
|
* Merge inherited properties from parent into options, prioritizing values from options, |
|
* then values from parent. |
|
* |
|
* @param parent - An optional owning class of the operation being run. ex. Db/Collection/MongoClient. |
|
* @param options - The options passed to the operation method. |
|
* |
|
* @internal |
|
*/ |
|
function resolveOptions(parent, options) { |
|
const result = Object.assign({}, options, (0, bson_1.resolveBSONOptions)(options, parent)); |
|
const timeoutMS = options?.timeoutMS ?? parent?.timeoutMS; |
|
// Users cannot pass a readConcern/writeConcern to operations in a transaction |
|
const session = options?.session; |
|
if (!session?.inTransaction()) { |
|
const readConcern = read_concern_1.ReadConcern.fromOptions(options) ?? parent?.readConcern; |
|
if (readConcern) { |
|
result.readConcern = readConcern; |
|
} |
|
let writeConcern = write_concern_1.WriteConcern.fromOptions(options) ?? parent?.writeConcern; |
|
if (writeConcern) { |
|
if (timeoutMS != null) { |
|
writeConcern = write_concern_1.WriteConcern.fromOptions({ |
|
writeConcern: { |
|
...writeConcern, |
|
wtimeout: undefined, |
|
wtimeoutMS: undefined |
|
} |
|
}); |
|
} |
|
result.writeConcern = writeConcern; |
|
} |
|
} |
|
result.timeoutMS = timeoutMS; |
|
const readPreference = read_preference_1.ReadPreference.fromOptions(options) ?? parent?.readPreference; |
|
if (readPreference) { |
|
result.readPreference = readPreference; |
|
} |
|
const isConvenientTransaction = session?.explicit && session?.timeoutContext != null; |
|
if (isConvenientTransaction && options?.timeoutMS != null) { |
|
throw new error_1.MongoInvalidArgumentError('An operation cannot be given a timeoutMS setting when inside a withTransaction call that has a timeoutMS setting'); |
|
} |
|
return result; |
|
} |
|
function isSuperset(set, subset) { |
|
set = Array.isArray(set) ? new Set(set) : set; |
|
subset = Array.isArray(subset) ? new Set(subset) : subset; |
|
for (const elem of subset) { |
|
if (!set.has(elem)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
/** |
|
* Checks if the document is a Hello request |
|
* @internal |
|
*/ |
|
function isHello(doc) { |
|
return doc[constants_2.LEGACY_HELLO_COMMAND] || doc.hello ? true : false; |
|
} |
|
/** Returns the items that are uniquely in setA */ |
|
function setDifference(setA, setB) { |
|
const difference = new Set(setA); |
|
for (const elem of setB) { |
|
difference.delete(elem); |
|
} |
|
return difference; |
|
} |
|
const HAS_OWN = (object, prop) => Object.prototype.hasOwnProperty.call(object, prop); |
|
function isRecord(value, requiredKeys = undefined) { |
|
if (!isObject(value)) { |
|
return false; |
|
} |
|
const ctor = value.constructor; |
|
if (ctor && ctor.prototype) { |
|
if (!isObject(ctor.prototype)) { |
|
return false; |
|
} |
|
// Check to see if some method exists from the Object exists |
|
if (!HAS_OWN(ctor.prototype, 'isPrototypeOf')) { |
|
return false; |
|
} |
|
} |
|
if (requiredKeys) { |
|
const keys = Object.keys(value); |
|
return isSuperset(keys, requiredKeys); |
|
} |
|
return true; |
|
} |
|
/** |
|
* A sequential list of items in a circularly linked list |
|
* @remarks |
|
* The head node is special, it is always defined and has a value of null. |
|
* It is never "included" in the list, in that, it is not returned by pop/shift or yielded by the iterator. |
|
* The circular linkage and always defined head node are to reduce checks for null next/prev references to zero. |
|
* New nodes are declared as object literals with keys always in the same order: next, prev, value. |
|
* @internal |
|
*/ |
|
class List { |
|
get length() { |
|
return this.count; |
|
} |
|
get [Symbol.toStringTag]() { |
|
return 'List'; |
|
} |
|
constructor() { |
|
this.count = 0; |
|
// this is carefully crafted: |
|
// declaring a complete and consistently key ordered |
|
// object is beneficial to the runtime optimizations |
|
this.head = { |
|
next: null, |
|
prev: null, |
|
value: null |
|
}; |
|
this.head.next = this.head; |
|
this.head.prev = this.head; |
|
} |
|
toArray() { |
|
return Array.from(this); |
|
} |
|
toString() { |
|
return `head <=> ${this.toArray().join(' <=> ')} <=> head`; |
|
} |
|
*[Symbol.iterator]() { |
|
for (const node of this.nodes()) { |
|
yield node.value; |
|
} |
|
} |
|
*nodes() { |
|
let ptr = this.head.next; |
|
while (ptr !== this.head) { |
|
// Save next before yielding so that we make removing within iteration safe |
|
const { next } = ptr; |
|
yield ptr; |
|
ptr = next; |
|
} |
|
} |
|
/** Insert at end of list */ |
|
push(value) { |
|
this.count += 1; |
|
const newNode = { |
|
next: this.head, |
|
prev: this.head.prev, |
|
value |
|
}; |
|
this.head.prev.next = newNode; |
|
this.head.prev = newNode; |
|
} |
|
/** Inserts every item inside an iterable instead of the iterable itself */ |
|
pushMany(iterable) { |
|
for (const value of iterable) { |
|
this.push(value); |
|
} |
|
} |
|
/** Insert at front of list */ |
|
unshift(value) { |
|
this.count += 1; |
|
const newNode = { |
|
next: this.head.next, |
|
prev: this.head, |
|
value |
|
}; |
|
this.head.next.prev = newNode; |
|
this.head.next = newNode; |
|
} |
|
remove(node) { |
|
if (node === this.head || this.length === 0) { |
|
return null; |
|
} |
|
this.count -= 1; |
|
const prevNode = node.prev; |
|
const nextNode = node.next; |
|
prevNode.next = nextNode; |
|
nextNode.prev = prevNode; |
|
return node.value; |
|
} |
|
/** Removes the first node at the front of the list */ |
|
shift() { |
|
return this.remove(this.head.next); |
|
} |
|
/** Removes the last node at the end of the list */ |
|
pop() { |
|
return this.remove(this.head.prev); |
|
} |
|
/** Iterates through the list and removes nodes where filter returns true */ |
|
prune(filter) { |
|
for (const node of this.nodes()) { |
|
if (filter(node.value)) { |
|
this.remove(node); |
|
} |
|
} |
|
} |
|
clear() { |
|
this.count = 0; |
|
this.head.next = this.head; |
|
this.head.prev = this.head; |
|
} |
|
/** Returns the first item in the list, does not remove */ |
|
first() { |
|
// If the list is empty, value will be the head's null |
|
return this.head.next.value; |
|
} |
|
/** Returns the last item in the list, does not remove */ |
|
last() { |
|
// If the list is empty, value will be the head's null |
|
return this.head.prev.value; |
|
} |
|
} |
|
exports.List = List; |
|
/** |
|
* A pool of Buffers which allow you to read them as if they were one |
|
* @internal |
|
*/ |
|
class BufferPool { |
|
constructor() { |
|
this.buffers = new List(); |
|
this.totalByteLength = 0; |
|
} |
|
get length() { |
|
return this.totalByteLength; |
|
} |
|
/** Adds a buffer to the internal buffer pool list */ |
|
append(buffer) { |
|
this.buffers.push(buffer); |
|
this.totalByteLength += buffer.length; |
|
} |
|
/** |
|
* If BufferPool contains 4 bytes or more construct an int32 from the leading bytes, |
|
* otherwise return null. Size can be negative, caller should error check. |
|
*/ |
|
getInt32() { |
|
if (this.totalByteLength < 4) { |
|
return null; |
|
} |
|
const firstBuffer = this.buffers.first(); |
|
if (firstBuffer != null && firstBuffer.byteLength >= 4) { |
|
return firstBuffer.readInt32LE(0); |
|
} |
|
// Unlikely case: an int32 is split across buffers. |
|
// Use read and put the returned buffer back on top |
|
const top4Bytes = this.read(4); |
|
const value = top4Bytes.readInt32LE(0); |
|
// Put it back. |
|
this.totalByteLength += 4; |
|
this.buffers.unshift(top4Bytes); |
|
return value; |
|
} |
|
/** Reads the requested number of bytes, optionally consuming them */ |
|
read(size) { |
|
if (typeof size !== 'number' || size < 0) { |
|
throw new error_1.MongoInvalidArgumentError('Argument "size" must be a non-negative number'); |
|
} |
|
// oversized request returns empty buffer |
|
if (size > this.totalByteLength) { |
|
return Buffer.alloc(0); |
|
} |
|
// We know we have enough, we just don't know how it is spread across chunks |
|
// TODO(NODE-4732): alloc API should change based on raw option |
|
const result = Buffer.allocUnsafe(size); |
|
for (let bytesRead = 0; bytesRead < size;) { |
|
const buffer = this.buffers.shift(); |
|
if (buffer == null) { |
|
break; |
|
} |
|
const bytesRemaining = size - bytesRead; |
|
const bytesReadable = Math.min(bytesRemaining, buffer.byteLength); |
|
const bytes = buffer.subarray(0, bytesReadable); |
|
result.set(bytes, bytesRead); |
|
bytesRead += bytesReadable; |
|
this.totalByteLength -= bytesReadable; |
|
if (bytesReadable < buffer.byteLength) { |
|
this.buffers.unshift(buffer.subarray(bytesReadable)); |
|
} |
|
} |
|
return result; |
|
} |
|
} |
|
exports.BufferPool = BufferPool; |
|
/** @public */ |
|
class HostAddress { |
|
constructor(hostString) { |
|
this.host = undefined; |
|
this.port = undefined; |
|
this.socketPath = undefined; |
|
this.isIPv6 = false; |
|
const escapedHost = hostString.split(' ').join('%20'); // escape spaces, for socket path hosts |
|
if (escapedHost.endsWith('.sock')) { |
|
// heuristically determine if we're working with a domain socket |
|
this.socketPath = decodeURIComponent(escapedHost); |
|
return; |
|
} |
|
const urlString = `iLoveJS://${escapedHost}`; |
|
let url; |
|
try { |
|
url = new url_1.URL(urlString); |
|
} |
|
catch (urlError) { |
|
const runtimeError = new error_1.MongoRuntimeError(`Unable to parse ${escapedHost} with URL`); |
|
runtimeError.cause = urlError; |
|
throw runtimeError; |
|
} |
|
const hostname = url.hostname; |
|
const port = url.port; |
|
let normalized = decodeURIComponent(hostname).toLowerCase(); |
|
if (normalized.startsWith('[') && normalized.endsWith(']')) { |
|
this.isIPv6 = true; |
|
normalized = normalized.substring(1, hostname.length - 1); |
|
} |
|
this.host = normalized.toLowerCase(); |
|
if (typeof port === 'number') { |
|
this.port = port; |
|
} |
|
else if (typeof port === 'string' && port !== '') { |
|
this.port = Number.parseInt(port, 10); |
|
} |
|
else { |
|
this.port = 27017; |
|
} |
|
if (this.port === 0) { |
|
throw new error_1.MongoParseError('Invalid port (zero) with hostname'); |
|
} |
|
Object.freeze(this); |
|
} |
|
[Symbol.for('nodejs.util.inspect.custom')]() { |
|
return this.inspect(); |
|
} |
|
inspect() { |
|
return `new HostAddress('${this.toString()}')`; |
|
} |
|
toString() { |
|
if (typeof this.host === 'string') { |
|
if (this.isIPv6) { |
|
return `[${this.host}]:${this.port}`; |
|
} |
|
return `${this.host}:${this.port}`; |
|
} |
|
return `${this.socketPath}`; |
|
} |
|
static fromString(s) { |
|
return new HostAddress(s); |
|
} |
|
static fromHostPort(host, port) { |
|
if (host.includes(':')) { |
|
host = `[${host}]`; // IPv6 address |
|
} |
|
return HostAddress.fromString(`${host}:${port}`); |
|
} |
|
static fromSrvRecord({ name, port }) { |
|
return HostAddress.fromHostPort(name, port); |
|
} |
|
toHostPort() { |
|
if (this.socketPath) { |
|
return { host: this.socketPath, port: 0 }; |
|
} |
|
const host = this.host ?? ''; |
|
const port = this.port ?? 0; |
|
return { host, port }; |
|
} |
|
} |
|
exports.HostAddress = HostAddress; |
|
exports.DEFAULT_PK_FACTORY = { |
|
// We prefer not to rely on ObjectId having a createPk method |
|
createPk() { |
|
return new bson_1.ObjectId(); |
|
} |
|
}; |
|
/** |
|
* When the driver used emitWarning the code will be equal to this. |
|
* @public |
|
* |
|
* @example |
|
* ```ts |
|
* process.on('warning', (warning) => { |
|
* if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)') |
|
* }) |
|
* ``` |
|
*/ |
|
exports.MONGODB_WARNING_CODE = 'MONGODB DRIVER'; |
|
/** @internal */ |
|
function emitWarning(message) { |
|
return process.emitWarning(message, { code: exports.MONGODB_WARNING_CODE }); |
|
} |
|
const emittedWarnings = new Set(); |
|
/** |
|
* Will emit a warning once for the duration of the application. |
|
* Uses the message to identify if it has already been emitted |
|
* so using string interpolation can cause multiple emits |
|
* @internal |
|
*/ |
|
function emitWarningOnce(message) { |
|
if (!emittedWarnings.has(message)) { |
|
emittedWarnings.add(message); |
|
return emitWarning(message); |
|
} |
|
} |
|
/** |
|
* Takes a JS object and joins the values into a string separated by ', ' |
|
*/ |
|
function enumToString(en) { |
|
return Object.values(en).join(', '); |
|
} |
|
/** |
|
* Determine if a server supports retryable writes. |
|
* |
|
* @internal |
|
*/ |
|
function supportsRetryableWrites(server) { |
|
if (!server) { |
|
return false; |
|
} |
|
if (server.loadBalanced) { |
|
// Loadbalanced topologies will always support retry writes |
|
return true; |
|
} |
|
if (server.description.logicalSessionTimeoutMinutes != null) { |
|
// that supports sessions |
|
if (server.description.type !== common_1.ServerType.Standalone) { |
|
// and that is not a standalone |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
/** |
|
* Fisher–Yates Shuffle |
|
* |
|
* Reference: https://bost.ocks.org/mike/shuffle/ |
|
* @param sequence - items to be shuffled |
|
* @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. |
|
*/ |
|
function shuffle(sequence, limit = 0) { |
|
const items = Array.from(sequence); // shallow copy in order to never shuffle the input |
|
if (limit > items.length) { |
|
throw new error_1.MongoRuntimeError('Limit must be less than the number of items'); |
|
} |
|
let remainingItemsToShuffle = items.length; |
|
const lowerBound = limit % items.length === 0 ? 1 : items.length - limit; |
|
while (remainingItemsToShuffle > lowerBound) { |
|
// Pick a remaining element |
|
const randomIndex = Math.floor(Math.random() * remainingItemsToShuffle); |
|
remainingItemsToShuffle -= 1; |
|
// And swap it with the current element |
|
const swapHold = items[remainingItemsToShuffle]; |
|
items[remainingItemsToShuffle] = items[randomIndex]; |
|
items[randomIndex] = swapHold; |
|
} |
|
return limit % items.length === 0 ? items : items.slice(lowerBound); |
|
} |
|
/** |
|
* TODO(NODE-4936): read concern eligibility for commands should be codified in command construction |
|
* @internal |
|
* @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.md#read-concern |
|
*/ |
|
function commandSupportsReadConcern(command) { |
|
if (command.aggregate || command.count || command.distinct || command.find || command.geoNear) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
/** |
|
* Compare objectIds. `null` is always less |
|
* - `+1 = oid1 is greater than oid2` |
|
* - `-1 = oid1 is less than oid2` |
|
* - `+0 = oid1 is equal oid2` |
|
*/ |
|
function compareObjectId(oid1, oid2) { |
|
if (oid1 == null && oid2 == null) { |
|
return 0; |
|
} |
|
if (oid1 == null) { |
|
return -1; |
|
} |
|
if (oid2 == null) { |
|
return 1; |
|
} |
|
return exports.ByteUtils.compare(oid1.id, oid2.id); |
|
} |
|
function parseInteger(value) { |
|
if (typeof value === 'number') |
|
return Math.trunc(value); |
|
const parsedValue = Number.parseInt(String(value), 10); |
|
return Number.isNaN(parsedValue) ? null : parsedValue; |
|
} |
|
function parseUnsignedInteger(value) { |
|
const parsedInt = parseInteger(value); |
|
return parsedInt != null && parsedInt >= 0 ? parsedInt : null; |
|
} |
|
/** |
|
* This function throws a MongoAPIError in the event that either of the following is true: |
|
* * If the provided address domain does not match the provided parent domain |
|
* * If the parent domain contains less than three `.` separated parts and the provided address does not contain at least one more domain level than its parent |
|
* |
|
* If a DNS server were to become compromised SRV records would still need to |
|
* advertise addresses that are under the same domain as the srvHost. |
|
* |
|
* @param address - The address to check against a domain |
|
* @param srvHost - The domain to check the provided address against |
|
* @returns void |
|
*/ |
|
function checkParentDomainMatch(address, srvHost) { |
|
// Remove trailing dot if exists on either the resolved address or the srv hostname |
|
const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address; |
|
const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost; |
|
const allCharacterBeforeFirstDot = /^.*?\./; |
|
const srvIsLessThanThreeParts = normalizedSrvHost.split('.').length < 3; |
|
// Remove all characters before first dot |
|
// Add leading dot back to string so |
|
// an srvHostDomain = '.trusted.site' |
|
// will not satisfy an addressDomain that endsWith '.fake-trusted.site' |
|
const addressDomain = `.${normalizedAddress.replace(allCharacterBeforeFirstDot, '')}`; |
|
let srvHostDomain = srvIsLessThanThreeParts |
|
? normalizedSrvHost |
|
: `.${normalizedSrvHost.replace(allCharacterBeforeFirstDot, '')}`; |
|
if (!srvHostDomain.startsWith('.')) { |
|
srvHostDomain = '.' + srvHostDomain; |
|
} |
|
if (srvIsLessThanThreeParts && |
|
normalizedAddress.split('.').length <= normalizedSrvHost.split('.').length) { |
|
throw new error_1.MongoAPIError('Server record does not have at least one more domain level than parent URI'); |
|
} |
|
if (!addressDomain.endsWith(srvHostDomain)) { |
|
throw new error_1.MongoAPIError('Server record does not share hostname with parent URI'); |
|
} |
|
} |
|
/** |
|
* Perform a get request that returns status and body. |
|
* @internal |
|
*/ |
|
function get(url, options = {}) { |
|
return new Promise((resolve, reject) => { |
|
/* eslint-disable prefer-const */ |
|
let timeoutId; |
|
const request = http |
|
.get(url, options, response => { |
|
response.setEncoding('utf8'); |
|
let body = ''; |
|
response.on('data', chunk => (body += chunk)); |
|
response.on('end', () => { |
|
(0, timers_1.clearTimeout)(timeoutId); |
|
resolve({ status: response.statusCode, body }); |
|
}); |
|
}) |
|
.on('error', error => { |
|
(0, timers_1.clearTimeout)(timeoutId); |
|
reject(error); |
|
}) |
|
.end(); |
|
timeoutId = (0, timers_1.setTimeout)(() => { |
|
request.destroy(new error_1.MongoNetworkTimeoutError(`request timed out after 10 seconds`)); |
|
}, 10000); |
|
}); |
|
} |
|
async function request(uri, options = {}) { |
|
return await new Promise((resolve, reject) => { |
|
const requestOptions = { |
|
method: 'GET', |
|
timeout: 10000, |
|
json: true, |
|
...url.parse(uri), |
|
...options |
|
}; |
|
const req = http.request(requestOptions, res => { |
|
res.setEncoding('utf8'); |
|
let data = ''; |
|
res.on('data', d => { |
|
data += d; |
|
}); |
|
res.once('end', () => { |
|
if (options.json === false) { |
|
resolve(data); |
|
return; |
|
} |
|
try { |
|
const parsed = JSON.parse(data); |
|
resolve(parsed); |
|
} |
|
catch { |
|
// TODO(NODE-3483) |
|
reject(new error_1.MongoRuntimeError(`Invalid JSON response: "${data}"`)); |
|
} |
|
}); |
|
}); |
|
req.once('timeout', () => req.destroy(new error_1.MongoNetworkTimeoutError(`Network request to ${uri} timed out after ${options.timeout} ms`))); |
|
req.once('error', error => reject(error)); |
|
req.end(); |
|
}); |
|
} |
|
/** @internal */ |
|
exports.DOCUMENT_DB_CHECK = /(\.docdb\.amazonaws\.com$)|(\.docdb-elastic\.amazonaws\.com$)/; |
|
/** @internal */ |
|
exports.COSMOS_DB_CHECK = /\.cosmos\.azure\.com$/; |
|
/** @internal */ |
|
exports.DOCUMENT_DB_MSG = 'You appear to be connected to a DocumentDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/documentdb'; |
|
/** @internal */ |
|
exports.COSMOS_DB_MSG = 'You appear to be connected to a CosmosDB cluster. For more information regarding feature compatibility and support please visit https://www.mongodb.com/supportability/cosmosdb'; |
|
/** @internal */ |
|
function isHostMatch(match, host) { |
|
return host && match.test(host.toLowerCase()) ? true : false; |
|
} |
|
function promiseWithResolvers() { |
|
let resolve; |
|
let reject; |
|
const promise = new Promise(function withResolversExecutor(promiseResolve, promiseReject) { |
|
resolve = promiseResolve; |
|
reject = promiseReject; |
|
}); |
|
return { promise, resolve, reject }; |
|
} |
|
/** |
|
* A noop function intended for use in preventing unhandled rejections. |
|
* |
|
* @example |
|
* ```js |
|
* const promise = myAsyncTask(); |
|
* // eslint-disable-next-line github/no-then |
|
* promise.then(undefined, squashError); |
|
* ``` |
|
*/ |
|
function squashError(_error) { |
|
return; |
|
} |
|
exports.randomBytes = (0, util_1.promisify)(crypto.randomBytes); |
|
/** |
|
* Replicates the events.once helper. |
|
* |
|
* Removes unused signal logic and It **only** supports 0 or 1 argument events. |
|
* |
|
* @param ee - An event emitter that may emit `ev` |
|
* @param name - An event name to wait for |
|
*/ |
|
async function once(ee, name, options) { |
|
options?.signal?.throwIfAborted(); |
|
const { promise, resolve, reject } = promiseWithResolvers(); |
|
const onEvent = (data) => resolve(data); |
|
const onError = (error) => reject(error); |
|
const abortListener = addAbortListener(options?.signal, function () { |
|
reject(this.reason); |
|
}); |
|
ee.once(name, onEvent).once('error', onError); |
|
try { |
|
return await promise; |
|
} |
|
finally { |
|
ee.off(name, onEvent); |
|
ee.off('error', onError); |
|
abortListener?.[exports.kDispose](); |
|
} |
|
} |
|
function maybeAddIdToDocuments(coll, docOrDocs, options) { |
|
const forceServerObjectId = typeof options.forceServerObjectId === 'boolean' |
|
? options.forceServerObjectId |
|
: coll.s.db.options?.forceServerObjectId; |
|
// no need to modify the docs if server sets the ObjectId |
|
if (forceServerObjectId === true) { |
|
return docOrDocs; |
|
} |
|
const transform = (doc) => { |
|
if (doc._id == null) { |
|
doc._id = coll.s.pkFactory.createPk(); |
|
} |
|
return doc; |
|
}; |
|
return Array.isArray(docOrDocs) ? docOrDocs.map(transform) : transform(docOrDocs); |
|
} |
|
async function fileIsAccessible(fileName, mode) { |
|
try { |
|
await fs_1.promises.access(fileName, mode); |
|
return true; |
|
} |
|
catch { |
|
return false; |
|
} |
|
} |
|
function csotMin(duration1, duration2) { |
|
if (duration1 === 0) |
|
return duration2; |
|
if (duration2 === 0) |
|
return duration1; |
|
return Math.min(duration1, duration2); |
|
} |
|
function noop() { |
|
return; |
|
} |
|
/** |
|
* Recurse through the (identically-shaped) `decrypted` and `original` |
|
* objects and attach a `decryptedKeys` property on each sub-object that |
|
* contained encrypted fields. Because we only call this on BSON responses, |
|
* we do not need to worry about circular references. |
|
* |
|
* @internal |
|
*/ |
|
function decorateDecryptionResult(decrypted, original, isTopLevelDecorateCall = true) { |
|
if (isTopLevelDecorateCall) { |
|
// The original value could have been either a JS object or a BSON buffer |
|
if (Buffer.isBuffer(original)) { |
|
original = (0, bson_1.deserialize)(original); |
|
} |
|
if (Buffer.isBuffer(decrypted)) { |
|
throw new error_1.MongoRuntimeError('Expected result of decryption to be deserialized BSON object'); |
|
} |
|
} |
|
if (!decrypted || typeof decrypted !== 'object') |
|
return; |
|
for (const k of Object.keys(decrypted)) { |
|
const originalValue = original[k]; |
|
// An object was decrypted by libmongocrypt if and only if it was |
|
// a BSON Binary object with subtype 6. |
|
if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) { |
|
if (!decrypted[constants_2.kDecoratedKeys]) { |
|
Object.defineProperty(decrypted, constants_2.kDecoratedKeys, { |
|
value: [], |
|
configurable: true, |
|
enumerable: false, |
|
writable: false |
|
}); |
|
} |
|
// this is defined in the preceding if-statement |
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
|
decrypted[constants_2.kDecoratedKeys].push(k); |
|
// Do not recurse into this decrypted value. It could be a sub-document/array, |
|
// in which case there is no original value associated with its subfields. |
|
continue; |
|
} |
|
decorateDecryptionResult(decrypted[k], originalValue, false); |
|
} |
|
} |
|
/** @internal */ |
|
exports.kDispose = Symbol.dispose ?? Symbol('dispose'); |
|
/** |
|
* A utility that helps with writing listener code idiomatically |
|
* |
|
* @example |
|
* ```js |
|
* using listener = addAbortListener(signal, function () { |
|
* console.log('aborted', this.reason); |
|
* }); |
|
* ``` |
|
* |
|
* @param signal - if exists adds an abort listener |
|
* @param listener - the listener to be added to signal |
|
* @returns A disposable that will remove the abort listener |
|
*/ |
|
function addAbortListener(signal, listener) { |
|
if (signal == null) |
|
return; |
|
signal.addEventListener('abort', listener, { once: true }); |
|
return { [exports.kDispose]: () => signal.removeEventListener('abort', listener) }; |
|
} |
|
/** |
|
* Takes a promise and races it with a promise wrapping the abort event of the optionally provided signal. |
|
* The given promise is _always_ ordered before the signal's abort promise. |
|
* When given an already rejected promise and an already aborted signal, the promise's rejection takes precedence. |
|
* |
|
* Any asynchronous processing in `promise` will continue even after the abort signal has fired, |
|
* but control will be returned to the caller |
|
* |
|
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race |
|
* |
|
* @param promise - A promise to discard if the signal aborts |
|
* @param options - An options object carrying an optional signal |
|
*/ |
|
async function abortable(promise, { signal }) { |
|
if (signal == null) { |
|
return await promise; |
|
} |
|
const { promise: aborted, reject } = promiseWithResolvers(); |
|
const abortListener = signal.aborted |
|
? reject(signal.reason) |
|
: addAbortListener(signal, function () { |
|
reject(this.reason); |
|
}); |
|
try { |
|
return await Promise.race([promise, aborted]); |
|
} |
|
finally { |
|
abortListener?.[exports.kDispose](); |
|
} |
|
} |
|
//# sourceMappingURL=utils.js.map
|