Spaces:
Runtime error
Runtime error
| import * as dns from 'dns'; | |
| import ConnectionString from 'mongodb-connection-string-url'; | |
| import { URLSearchParams } from 'url'; | |
| import type { Document } from './bson'; | |
| import { MongoCredentials } from './cmap/auth/mongo_credentials'; | |
| import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers'; | |
| import { addContainerMetadata, makeClientMetadata } from './cmap/handshake/client_metadata'; | |
| import { Compressor, type CompressorName } from './cmap/wire_protocol/compression'; | |
| import { Encrypter } from './encrypter'; | |
| import { | |
| MongoAPIError, | |
| MongoInvalidArgumentError, | |
| MongoMissingCredentialsError, | |
| MongoParseError | |
| } from './error'; | |
| import { | |
| MongoClient, | |
| type MongoClientOptions, | |
| type MongoOptions, | |
| type PkFactory, | |
| type ServerApi, | |
| ServerApiVersion | |
| } from './mongo_client'; | |
| import { | |
| MongoLoggableComponent, | |
| MongoLogger, | |
| type MongoLoggerEnvOptions, | |
| type MongoLoggerMongoClientOptions, | |
| SeverityLevel | |
| } from './mongo_logger'; | |
| import { ReadConcern, type ReadConcernLevel } from './read_concern'; | |
| import { ReadPreference, type ReadPreferenceMode } from './read_preference'; | |
| import { ServerMonitoringMode } from './sdam/monitor'; | |
| import type { TagSet } from './sdam/server_description'; | |
| import { | |
| DEFAULT_PK_FACTORY, | |
| emitWarning, | |
| HostAddress, | |
| isRecord, | |
| matchesParentDomain, | |
| parseInteger, | |
| setDifference, | |
| squashError | |
| } from './utils'; | |
| import { type W, WriteConcern } from './write_concern'; | |
| const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced']; | |
| const LB_SINGLE_HOST_ERROR = 'loadBalanced option only supported with a single host in the URI'; | |
| const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSet option'; | |
| const LB_DIRECT_CONNECTION_ERROR = | |
| 'loadBalanced option not supported when directConnection is provided'; | |
| /** | |
| * Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal | |
| * connection string. | |
| * | |
| * @param uri - The connection string to parse | |
| * @param options - Optional user provided connection string options | |
| */ | |
| export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddress[]> { | |
| if (typeof options.srvHost !== 'string') { | |
| throw new MongoAPIError('Option "srvHost" must not be empty'); | |
| } | |
| if (options.srvHost.split('.').length < 3) { | |
| // TODO(NODE-3484): Replace with MongoConnectionStringError | |
| throw new MongoAPIError('URI must include hostname, domain name, and tld'); | |
| } | |
| // Asynchronously start TXT resolution so that we do not have to wait until | |
| // the SRV record is resolved before starting a second DNS query. | |
| const lookupAddress = options.srvHost; | |
| const txtResolutionPromise = dns.promises.resolveTxt(lookupAddress); | |
| // eslint-disable-next-line github/no-then | |
| txtResolutionPromise.then(undefined, squashError); // rejections will be handled later | |
| // Resolve the SRV record and use the result as the list of hosts to connect to. | |
| const addresses = await dns.promises.resolveSrv( | |
| `_${options.srvServiceName}._tcp.${lookupAddress}` | |
| ); | |
| if (addresses.length === 0) { | |
| throw new MongoAPIError('No addresses found at host'); | |
| } | |
| for (const { name } of addresses) { | |
| if (!matchesParentDomain(name, lookupAddress)) { | |
| throw new MongoAPIError('Server record does not share hostname with parent URI'); | |
| } | |
| } | |
| const hostAddresses = addresses.map(r => HostAddress.fromString(`${r.name}:${r.port ?? 27017}`)); | |
| validateLoadBalancedOptions(hostAddresses, options, true); | |
| // Use the result of resolving the TXT record and add options from there if they exist. | |
| let record; | |
| try { | |
| record = await txtResolutionPromise; | |
| } catch (error) { | |
| if (error.code !== 'ENODATA' && error.code !== 'ENOTFOUND') { | |
| throw error; | |
| } | |
| return hostAddresses; | |
| } | |
| if (record.length > 1) { | |
| throw new MongoParseError('Multiple text records not allowed'); | |
| } | |
| const txtRecordOptions = new URLSearchParams(record[0].join('')); | |
| const txtRecordOptionKeys = [...txtRecordOptions.keys()]; | |
| if (txtRecordOptionKeys.some(key => !VALID_TXT_RECORDS.includes(key))) { | |
| throw new MongoParseError(`Text record may only set any of: ${VALID_TXT_RECORDS.join(', ')}`); | |
| } | |
| if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) { | |
| throw new MongoParseError('Cannot have empty URI params in DNS TXT Record'); | |
| } | |
| const source = txtRecordOptions.get('authSource') ?? undefined; | |
| const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined; | |
| const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined; | |
| if ( | |
| !options.userSpecifiedAuthSource && | |
| source && | |
| options.credentials && | |
| !AUTH_MECHS_AUTH_SRC_EXTERNAL.has(options.credentials.mechanism) | |
| ) { | |
| options.credentials = MongoCredentials.merge(options.credentials, { source }); | |
| } | |
| if (!options.userSpecifiedReplicaSet && replicaSet) { | |
| options.replicaSet = replicaSet; | |
| } | |
| if (loadBalanced === 'true') { | |
| options.loadBalanced = true; | |
| } | |
| if (options.replicaSet && options.srvMaxHosts > 0) { | |
| throw new MongoParseError('Cannot combine replicaSet option with srvMaxHosts'); | |
| } | |
| validateLoadBalancedOptions(hostAddresses, options, true); | |
| return hostAddresses; | |
| } | |
| /** | |
| * Checks if TLS options are valid | |
| * | |
| * @param allOptions - All options provided by user or included in default options map | |
| * @throws MongoAPIError if TLS options are invalid | |
| */ | |
| function checkTLSOptions(allOptions: CaseInsensitiveMap): void { | |
| if (!allOptions) return; | |
| const check = (a: string, b: string) => { | |
| if (allOptions.has(a) && allOptions.has(b)) { | |
| throw new MongoAPIError(`The '${a}' option cannot be used with the '${b}' option`); | |
| } | |
| }; | |
| check('tlsInsecure', 'tlsAllowInvalidCertificates'); | |
| check('tlsInsecure', 'tlsAllowInvalidHostnames'); | |
| check('tlsInsecure', 'tlsDisableCertificateRevocationCheck'); | |
| check('tlsInsecure', 'tlsDisableOCSPEndpointCheck'); | |
| check('tlsAllowInvalidCertificates', 'tlsDisableCertificateRevocationCheck'); | |
| check('tlsAllowInvalidCertificates', 'tlsDisableOCSPEndpointCheck'); | |
| check('tlsDisableCertificateRevocationCheck', 'tlsDisableOCSPEndpointCheck'); | |
| } | |
| function getBoolean(name: string, value: unknown): boolean { | |
| if (typeof value === 'boolean') return value; | |
| switch (value) { | |
| case 'true': | |
| return true; | |
| case 'false': | |
| return false; | |
| default: | |
| throw new MongoParseError(`${name} must be either "true" or "false"`); | |
| } | |
| } | |
| function getIntFromOptions(name: string, value: unknown): number { | |
| const parsedInt = parseInteger(value); | |
| if (parsedInt != null) { | |
| return parsedInt; | |
| } | |
| throw new MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`); | |
| } | |
| function getUIntFromOptions(name: string, value: unknown): number { | |
| const parsedValue = getIntFromOptions(name, value); | |
| if (parsedValue < 0) { | |
| throw new MongoParseError(`${name} can only be a positive int value, got: ${value}`); | |
| } | |
| return parsedValue; | |
| } | |
| function* entriesFromString(value: string): Generator<[string, string]> { | |
| if (value === '') { | |
| return; | |
| } | |
| const keyValuePairs = value.split(','); | |
| for (const keyValue of keyValuePairs) { | |
| const [key, value] = keyValue.split(/:(.*)/); | |
| if (value == null) { | |
| throw new MongoParseError('Cannot have undefined values in key value pairs'); | |
| } | |
| yield [key, value]; | |
| } | |
| } | |
| class CaseInsensitiveMap<Value = any> extends Map<string, Value> { | |
| constructor(entries: Array<[string, any]> = []) { | |
| super(entries.map(([k, v]) => [k.toLowerCase(), v])); | |
| } | |
| override has(k: string) { | |
| return super.has(k.toLowerCase()); | |
| } | |
| override get(k: string) { | |
| return super.get(k.toLowerCase()); | |
| } | |
| override set(k: string, v: any) { | |
| return super.set(k.toLowerCase(), v); | |
| } | |
| override delete(k: string): boolean { | |
| return super.delete(k.toLowerCase()); | |
| } | |
| } | |
| export function parseOptions( | |
| uri: string, | |
| mongoClient: MongoClient | MongoClientOptions | undefined = undefined, | |
| options: MongoClientOptions = {} | |
| ): MongoOptions { | |
| if (mongoClient != null && !(mongoClient instanceof MongoClient)) { | |
| options = mongoClient; | |
| mongoClient = undefined; | |
| } | |
| // validate BSONOptions | |
| if (options.useBigInt64 && typeof options.promoteLongs === 'boolean' && !options.promoteLongs) { | |
| throw new MongoAPIError('Must request either bigint or Long for int64 deserialization'); | |
| } | |
| if (options.useBigInt64 && typeof options.promoteValues === 'boolean' && !options.promoteValues) { | |
| throw new MongoAPIError('Must request either bigint or Long for int64 deserialization'); | |
| } | |
| const url = new ConnectionString(uri); | |
| const { hosts, isSRV } = url; | |
| const mongoOptions = Object.create(null); | |
| // Feature flags | |
| for (const flag of Object.getOwnPropertySymbols(options)) { | |
| if (FEATURE_FLAGS.has(flag)) { | |
| mongoOptions[flag] = options[flag]; | |
| } | |
| } | |
| mongoOptions.hosts = isSRV ? [] : hosts.map(HostAddress.fromString); | |
| const urlOptions = new CaseInsensitiveMap<unknown[]>(); | |
| if (url.pathname !== '/' && url.pathname !== '') { | |
| const dbName = decodeURIComponent( | |
| url.pathname[0] === '/' ? url.pathname.slice(1) : url.pathname | |
| ); | |
| if (dbName) { | |
| urlOptions.set('dbName', [dbName]); | |
| } | |
| } | |
| if (url.username !== '') { | |
| const auth: Document = { | |
| username: decodeURIComponent(url.username) | |
| }; | |
| if (typeof url.password === 'string') { | |
| auth.password = decodeURIComponent(url.password); | |
| } | |
| urlOptions.set('auth', [auth]); | |
| } | |
| for (const key of url.searchParams.keys()) { | |
| const values = url.searchParams.getAll(key); | |
| const isReadPreferenceTags = /readPreferenceTags/i.test(key); | |
| if (!isReadPreferenceTags && values.length > 1) { | |
| throw new MongoInvalidArgumentError( | |
| `URI option "${key}" cannot appear more than once in the connection string` | |
| ); | |
| } | |
| if (!isReadPreferenceTags && values.includes('')) { | |
| throw new MongoAPIError(`URI option "${key}" cannot be specified with no value`); | |
| } | |
| if (!urlOptions.has(key)) { | |
| urlOptions.set(key, values); | |
| } | |
| } | |
| const objectOptions = new CaseInsensitiveMap<unknown>( | |
| Object.entries(options).filter(([, v]) => v != null) | |
| ); | |
| // Validate options that can only be provided by one of uri or object | |
| if (urlOptions.has('serverApi')) { | |
| throw new MongoParseError( | |
| 'URI cannot contain `serverApi`, it can only be passed to the client' | |
| ); | |
| } | |
| const uriMechanismProperties = urlOptions.get('authMechanismProperties'); | |
| if (uriMechanismProperties) { | |
| for (const property of uriMechanismProperties) { | |
| if (/(^|,)ALLOWED_HOSTS:/.test(property as string)) { | |
| throw new MongoParseError( | |
| 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' | |
| ); | |
| } | |
| } | |
| } | |
| if (objectOptions.has('loadBalanced')) { | |
| throw new MongoParseError('loadBalanced is only a valid option in the URI'); | |
| } | |
| // All option collection | |
| const allProvidedOptions = new CaseInsensitiveMap<unknown[]>(); | |
| const allProvidedKeys = new Set<string>([...urlOptions.keys(), ...objectOptions.keys()]); | |
| for (const key of allProvidedKeys) { | |
| const values = []; | |
| const objectOptionValue = objectOptions.get(key); | |
| if (objectOptionValue != null) { | |
| values.push(objectOptionValue); | |
| } | |
| const urlValues = urlOptions.get(key) ?? []; | |
| values.push(...urlValues); | |
| allProvidedOptions.set(key, values); | |
| } | |
| if (allProvidedOptions.has('tls') || allProvidedOptions.has('ssl')) { | |
| const tlsAndSslOpts = (allProvidedOptions.get('tls') || []) | |
| .concat(allProvidedOptions.get('ssl') || []) | |
| .map(getBoolean.bind(null, 'tls/ssl')); | |
| if (new Set(tlsAndSslOpts).size !== 1) { | |
| throw new MongoParseError('All values of tls/ssl must be the same.'); | |
| } | |
| } | |
| checkTLSOptions(allProvidedOptions); | |
| const unsupportedOptions = setDifference( | |
| allProvidedKeys, | |
| Array.from(Object.keys(OPTIONS)).map(s => s.toLowerCase()) | |
| ); | |
| if (unsupportedOptions.size !== 0) { | |
| const optionWord = unsupportedOptions.size > 1 ? 'options' : 'option'; | |
| const isOrAre = unsupportedOptions.size > 1 ? 'are' : 'is'; | |
| throw new MongoParseError( | |
| `${optionWord} ${Array.from(unsupportedOptions).join(', ')} ${isOrAre} not supported` | |
| ); | |
| } | |
| // Option parsing and setting | |
| for (const [key, descriptor] of Object.entries(OPTIONS)) { | |
| const values = allProvidedOptions.get(key); | |
| if (!values || values.length === 0) { | |
| if (DEFAULT_OPTIONS.has(key)) { | |
| setOption(mongoOptions, key, descriptor, [DEFAULT_OPTIONS.get(key)]); | |
| } | |
| } else { | |
| const { deprecated } = descriptor; | |
| if (deprecated) { | |
| const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : ''; | |
| emitWarning(`${key} is a deprecated option${deprecatedMsg}`); | |
| } | |
| setOption(mongoOptions, key, descriptor, values); | |
| } | |
| } | |
| if (mongoOptions.credentials) { | |
| const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI; | |
| const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509; | |
| const isAws = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_AWS; | |
| const isOidc = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_OIDC; | |
| if ( | |
| (isGssapi || isX509) && | |
| allProvidedOptions.has('authSource') && | |
| mongoOptions.credentials.source !== '$external' | |
| ) { | |
| // If authSource was explicitly given and its incorrect, we error | |
| throw new MongoParseError( | |
| `authMechanism ${mongoOptions.credentials.mechanism} requires an authSource of '$external'` | |
| ); | |
| } | |
| if ( | |
| !(isGssapi || isX509 || isAws || isOidc) && | |
| mongoOptions.dbName && | |
| !allProvidedOptions.has('authSource') | |
| ) { | |
| // inherit the dbName unless GSSAPI or X509, then silently ignore dbName | |
| // and there was no specific authSource given | |
| mongoOptions.credentials = MongoCredentials.merge(mongoOptions.credentials, { | |
| source: mongoOptions.dbName | |
| }); | |
| } | |
| if (isAws && mongoOptions.credentials.username && !mongoOptions.credentials.password) { | |
| throw new MongoMissingCredentialsError( | |
| `When using ${mongoOptions.credentials.mechanism} password must be set when a username is specified` | |
| ); | |
| } | |
| mongoOptions.credentials.validate(); | |
| // Check if the only auth related option provided was authSource, if so we can remove credentials | |
| if ( | |
| mongoOptions.credentials.password === '' && | |
| mongoOptions.credentials.username === '' && | |
| mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_DEFAULT && | |
| Object.keys(mongoOptions.credentials.mechanismProperties).length === 0 | |
| ) { | |
| delete mongoOptions.credentials; | |
| } | |
| } | |
| if (!mongoOptions.dbName) { | |
| // dbName default is applied here because of the credential validation above | |
| mongoOptions.dbName = 'test'; | |
| } | |
| validateLoadBalancedOptions(hosts, mongoOptions, isSRV); | |
| if (mongoClient && mongoOptions.autoEncryption) { | |
| Encrypter.checkForMongoCrypt(); | |
| mongoOptions.encrypter = new Encrypter(mongoClient, uri, options); | |
| mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter; | |
| } | |
| // Potential SRV Overrides and SRV connection string validations | |
| mongoOptions.userSpecifiedAuthSource = | |
| objectOptions.has('authSource') || urlOptions.has('authSource'); | |
| mongoOptions.userSpecifiedReplicaSet = | |
| objectOptions.has('replicaSet') || urlOptions.has('replicaSet'); | |
| if (isSRV) { | |
| // SRV Record is resolved upon connecting | |
| mongoOptions.srvHost = hosts[0]; | |
| if (mongoOptions.directConnection) { | |
| throw new MongoAPIError('SRV URI does not support directConnection'); | |
| } | |
| if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') { | |
| throw new MongoParseError('Cannot use srvMaxHosts option with replicaSet'); | |
| } | |
| // SRV turns on TLS by default, but users can override and turn it off | |
| const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls'); | |
| const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl'); | |
| if (noUserSpecifiedTLS && noUserSpecifiedSSL) { | |
| mongoOptions.tls = true; | |
| } | |
| } else { | |
| const userSpecifiedSrvOptions = | |
| urlOptions.has('srvMaxHosts') || | |
| objectOptions.has('srvMaxHosts') || | |
| urlOptions.has('srvServiceName') || | |
| objectOptions.has('srvServiceName'); | |
| if (userSpecifiedSrvOptions) { | |
| throw new MongoParseError( | |
| 'Cannot use srvMaxHosts or srvServiceName with a non-srv connection string' | |
| ); | |
| } | |
| } | |
| if (mongoOptions.directConnection && mongoOptions.hosts.length !== 1) { | |
| throw new MongoParseError('directConnection option requires exactly one host'); | |
| } | |
| if ( | |
| !mongoOptions.proxyHost && | |
| (mongoOptions.proxyPort || mongoOptions.proxyUsername || mongoOptions.proxyPassword) | |
| ) { | |
| throw new MongoParseError('Must specify proxyHost if other proxy options are passed'); | |
| } | |
| if ( | |
| (mongoOptions.proxyUsername && !mongoOptions.proxyPassword) || | |
| (!mongoOptions.proxyUsername && mongoOptions.proxyPassword) | |
| ) { | |
| throw new MongoParseError('Can only specify both of proxy username/password or neither'); | |
| } | |
| const proxyOptions = ['proxyHost', 'proxyPort', 'proxyUsername', 'proxyPassword'].map( | |
| key => urlOptions.get(key) ?? [] | |
| ); | |
| if (proxyOptions.some(options => options.length > 1)) { | |
| throw new MongoParseError( | |
| 'Proxy options cannot be specified multiple times in the connection string' | |
| ); | |
| } | |
| const loggerFeatureFlag = Symbol.for('@@mdb.enableMongoLogger'); | |
| mongoOptions[loggerFeatureFlag] = mongoOptions[loggerFeatureFlag] ?? false; | |
| let loggerEnvOptions: MongoLoggerEnvOptions = {}; | |
| let loggerClientOptions: MongoLoggerMongoClientOptions = {}; | |
| if (mongoOptions[loggerFeatureFlag]) { | |
| loggerEnvOptions = { | |
| MONGODB_LOG_COMMAND: process.env.MONGODB_LOG_COMMAND, | |
| MONGODB_LOG_TOPOLOGY: process.env.MONGODB_LOG_TOPOLOGY, | |
| MONGODB_LOG_SERVER_SELECTION: process.env.MONGODB_LOG_SERVER_SELECTION, | |
| MONGODB_LOG_CONNECTION: process.env.MONGODB_LOG_CONNECTION, | |
| MONGODB_LOG_CLIENT: process.env.MONGODB_LOG_CLIENT, | |
| MONGODB_LOG_ALL: process.env.MONGODB_LOG_ALL, | |
| MONGODB_LOG_MAX_DOCUMENT_LENGTH: process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH, | |
| MONGODB_LOG_PATH: process.env.MONGODB_LOG_PATH, | |
| ...mongoOptions[Symbol.for('@@mdb.internalLoggerConfig')] | |
| }; | |
| loggerClientOptions = { | |
| mongodbLogPath: mongoOptions.mongodbLogPath, | |
| mongodbLogComponentSeverities: mongoOptions.mongodbLogComponentSeverities, | |
| mongodbLogMaxDocumentLength: mongoOptions.mongodbLogMaxDocumentLength | |
| }; | |
| } | |
| mongoOptions.mongoLoggerOptions = MongoLogger.resolveOptions( | |
| loggerEnvOptions, | |
| loggerClientOptions | |
| ); | |
| mongoOptions.metadata = makeClientMetadata(mongoOptions); | |
| // eslint-disable-next-line github/no-then | |
| mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).then( | |
| undefined, | |
| squashError | |
| ); // rejections will be handled later | |
| return mongoOptions; | |
| } | |
| /** | |
| * #### Throws if LB mode is true: | |
| * - hosts contains more than one host | |
| * - there is a replicaSet name set | |
| * - directConnection is set | |
| * - if srvMaxHosts is used when an srv connection string is passed in | |
| * | |
| * @throws MongoParseError | |
| */ | |
| function validateLoadBalancedOptions( | |
| hosts: HostAddress[] | string[], | |
| mongoOptions: MongoOptions, | |
| isSrv: boolean | |
| ): void { | |
| if (mongoOptions.loadBalanced) { | |
| if (hosts.length > 1) { | |
| throw new MongoParseError(LB_SINGLE_HOST_ERROR); | |
| } | |
| if (mongoOptions.replicaSet) { | |
| throw new MongoParseError(LB_REPLICA_SET_ERROR); | |
| } | |
| if (mongoOptions.directConnection) { | |
| throw new MongoParseError(LB_DIRECT_CONNECTION_ERROR); | |
| } | |
| if (isSrv && mongoOptions.srvMaxHosts > 0) { | |
| throw new MongoParseError('Cannot limit srv hosts with loadBalanced enabled'); | |
| } | |
| } | |
| return; | |
| } | |
| function setOption( | |
| mongoOptions: any, | |
| key: string, | |
| descriptor: OptionDescriptor, | |
| values: unknown[] | |
| ) { | |
| const { target, type, transform } = descriptor; | |
| const name = target ?? key; | |
| switch (type) { | |
| case 'boolean': | |
| mongoOptions[name] = getBoolean(name, values[0]); | |
| break; | |
| case 'int': | |
| mongoOptions[name] = getIntFromOptions(name, values[0]); | |
| break; | |
| case 'uint': | |
| mongoOptions[name] = getUIntFromOptions(name, values[0]); | |
| break; | |
| case 'string': | |
| if (values[0] == null) { | |
| break; | |
| } | |
| mongoOptions[name] = String(values[0]); | |
| break; | |
| case 'record': | |
| if (!isRecord(values[0])) { | |
| throw new MongoParseError(`${name} must be an object`); | |
| } | |
| mongoOptions[name] = values[0]; | |
| break; | |
| case 'any': | |
| mongoOptions[name] = values[0]; | |
| break; | |
| default: { | |
| if (!transform) { | |
| throw new MongoParseError('Descriptors missing a type must define a transform'); | |
| } | |
| const transformValue = transform({ name, options: mongoOptions, values }); | |
| mongoOptions[name] = transformValue; | |
| break; | |
| } | |
| } | |
| } | |
| interface OptionDescriptor { | |
| target?: string; | |
| type?: 'boolean' | 'int' | 'uint' | 'record' | 'string' | 'any'; | |
| default?: any; | |
| deprecated?: boolean | string; | |
| /** | |
| * @param name - the original option name | |
| * @param options - the options so far for resolution | |
| * @param values - the possible values in precedence order | |
| */ | |
| transform?: (args: { name: string; options: MongoOptions; values: unknown[] }) => unknown; | |
| } | |
| export const OPTIONS = { | |
| appName: { | |
| type: 'string' | |
| }, | |
| auth: { | |
| target: 'credentials', | |
| transform({ name, options, values: [value] }): MongoCredentials { | |
| if (!isRecord(value, ['username', 'password'] as const)) { | |
| throw new MongoParseError( | |
| `${name} must be an object with 'username' and 'password' properties` | |
| ); | |
| } | |
| return MongoCredentials.merge(options.credentials, { | |
| username: value.username, | |
| password: value.password | |
| }); | |
| } | |
| }, | |
| authMechanism: { | |
| target: 'credentials', | |
| transform({ options, values: [value] }): MongoCredentials { | |
| const mechanisms = Object.values(AuthMechanism); | |
| const [mechanism] = mechanisms.filter(m => m.match(RegExp(String.raw`\b${value}\b`, 'i'))); | |
| if (!mechanism) { | |
| throw new MongoParseError(`authMechanism one of ${mechanisms}, got ${value}`); | |
| } | |
| let source = options.credentials?.source; | |
| if ( | |
| mechanism === AuthMechanism.MONGODB_PLAIN || | |
| AUTH_MECHS_AUTH_SRC_EXTERNAL.has(mechanism) | |
| ) { | |
| // some mechanisms have '$external' as the Auth Source | |
| source = '$external'; | |
| } | |
| let password = options.credentials?.password; | |
| if (mechanism === AuthMechanism.MONGODB_X509 && password === '') { | |
| password = undefined; | |
| } | |
| return MongoCredentials.merge(options.credentials, { | |
| mechanism, | |
| source, | |
| password | |
| }); | |
| } | |
| }, | |
| // Note that if the authMechanismProperties contain a TOKEN_RESOURCE that has a | |
| // comma in it, it MUST be supplied as a MongoClient option instead of in the | |
| // connection string. | |
| authMechanismProperties: { | |
| target: 'credentials', | |
| transform({ options, values }): MongoCredentials { | |
| // We can have a combination of options passed in the URI and options passed | |
| // as an object to the MongoClient. So we must transform the string options | |
| // as well as merge them together with a potentially provided object. | |
| let mechanismProperties = Object.create(null); | |
| for (const optionValue of values) { | |
| if (typeof optionValue === 'string') { | |
| for (const [key, value] of entriesFromString(optionValue)) { | |
| try { | |
| mechanismProperties[key] = getBoolean(key, value); | |
| } catch { | |
| mechanismProperties[key] = value; | |
| } | |
| } | |
| } else { | |
| if (!isRecord(optionValue)) { | |
| throw new MongoParseError('AuthMechanismProperties must be an object'); | |
| } | |
| mechanismProperties = { ...optionValue }; | |
| } | |
| } | |
| return MongoCredentials.merge(options.credentials, { | |
| mechanismProperties | |
| }); | |
| } | |
| }, | |
| authSource: { | |
| target: 'credentials', | |
| transform({ options, values: [value] }): MongoCredentials { | |
| const source = String(value); | |
| return MongoCredentials.merge(options.credentials, { source }); | |
| } | |
| }, | |
| autoEncryption: { | |
| type: 'record' | |
| }, | |
| bsonRegExp: { | |
| type: 'boolean' | |
| }, | |
| serverApi: { | |
| target: 'serverApi', | |
| transform({ values: [version] }): ServerApi { | |
| const serverApiToValidate = | |
| typeof version === 'string' ? ({ version } as ServerApi) : (version as ServerApi); | |
| const versionToValidate = serverApiToValidate && serverApiToValidate.version; | |
| if (!versionToValidate) { | |
| throw new MongoParseError( | |
| `Invalid \`serverApi\` property; must specify a version from the following enum: ["${Object.values( | |
| ServerApiVersion | |
| ).join('", "')}"]` | |
| ); | |
| } | |
| if (!Object.values(ServerApiVersion).some(v => v === versionToValidate)) { | |
| throw new MongoParseError( | |
| `Invalid server API version=${versionToValidate}; must be in the following enum: ["${Object.values( | |
| ServerApiVersion | |
| ).join('", "')}"]` | |
| ); | |
| } | |
| return serverApiToValidate; | |
| } | |
| }, | |
| checkKeys: { | |
| type: 'boolean' | |
| }, | |
| compressors: { | |
| default: 'none', | |
| target: 'compressors', | |
| transform({ values }) { | |
| const compressionList = new Set(); | |
| for (const compVal of values as (CompressorName[] | string)[]) { | |
| const compValArray = typeof compVal === 'string' ? compVal.split(',') : compVal; | |
| if (!Array.isArray(compValArray)) { | |
| throw new MongoInvalidArgumentError( | |
| 'compressors must be an array or a comma-delimited list of strings' | |
| ); | |
| } | |
| for (const c of compValArray) { | |
| if (Object.keys(Compressor).includes(String(c))) { | |
| compressionList.add(String(c)); | |
| } else { | |
| throw new MongoInvalidArgumentError( | |
| `${c} is not a valid compression mechanism. Must be one of: ${Object.keys( | |
| Compressor | |
| )}.` | |
| ); | |
| } | |
| } | |
| } | |
| return [...compressionList]; | |
| } | |
| }, | |
| connectTimeoutMS: { | |
| default: 30000, | |
| type: 'uint' | |
| }, | |
| dbName: { | |
| type: 'string' | |
| }, | |
| directConnection: { | |
| default: false, | |
| type: 'boolean' | |
| }, | |
| driverInfo: { | |
| default: {}, | |
| type: 'record' | |
| }, | |
| enableUtf8Validation: { type: 'boolean', default: true }, | |
| family: { | |
| transform({ name, values: [value] }): 4 | 6 { | |
| const transformValue = getIntFromOptions(name, value); | |
| if (transformValue === 4 || transformValue === 6) { | |
| return transformValue; | |
| } | |
| throw new MongoParseError(`Option 'family' must be 4 or 6 got ${transformValue}.`); | |
| } | |
| }, | |
| fieldsAsRaw: { | |
| type: 'record' | |
| }, | |
| forceServerObjectId: { | |
| default: false, | |
| type: 'boolean' | |
| }, | |
| fsync: { | |
| deprecated: 'Please use journal instead', | |
| target: 'writeConcern', | |
| transform({ name, options, values: [value] }): WriteConcern { | |
| const wc = WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| fsync: getBoolean(name, value) | |
| } | |
| }); | |
| if (!wc) throw new MongoParseError(`Unable to make a writeConcern from fsync=${value}`); | |
| return wc; | |
| } | |
| } as OptionDescriptor, | |
| heartbeatFrequencyMS: { | |
| default: 10000, | |
| type: 'uint' | |
| }, | |
| ignoreUndefined: { | |
| type: 'boolean' | |
| }, | |
| j: { | |
| deprecated: 'Please use journal instead', | |
| target: 'writeConcern', | |
| transform({ name, options, values: [value] }): WriteConcern { | |
| const wc = WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| journal: getBoolean(name, value) | |
| } | |
| }); | |
| if (!wc) throw new MongoParseError(`Unable to make a writeConcern from journal=${value}`); | |
| return wc; | |
| } | |
| } as OptionDescriptor, | |
| journal: { | |
| target: 'writeConcern', | |
| transform({ name, options, values: [value] }): WriteConcern { | |
| const wc = WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| journal: getBoolean(name, value) | |
| } | |
| }); | |
| if (!wc) throw new MongoParseError(`Unable to make a writeConcern from journal=${value}`); | |
| return wc; | |
| } | |
| }, | |
| loadBalanced: { | |
| default: false, | |
| type: 'boolean' | |
| }, | |
| localThresholdMS: { | |
| default: 15, | |
| type: 'uint' | |
| }, | |
| maxConnecting: { | |
| default: 2, | |
| transform({ name, values: [value] }): number { | |
| const maxConnecting = getUIntFromOptions(name, value); | |
| if (maxConnecting === 0) { | |
| throw new MongoInvalidArgumentError('maxConnecting must be > 0 if specified'); | |
| } | |
| return maxConnecting; | |
| } | |
| }, | |
| maxIdleTimeMS: { | |
| default: 0, | |
| type: 'uint' | |
| }, | |
| maxPoolSize: { | |
| default: 100, | |
| type: 'uint' | |
| }, | |
| maxStalenessSeconds: { | |
| target: 'readPreference', | |
| transform({ name, options, values: [value] }) { | |
| const maxStalenessSeconds = getUIntFromOptions(name, value); | |
| if (options.readPreference) { | |
| return ReadPreference.fromOptions({ | |
| readPreference: { ...options.readPreference, maxStalenessSeconds } | |
| }); | |
| } else { | |
| return new ReadPreference('secondary', undefined, { maxStalenessSeconds }); | |
| } | |
| } | |
| }, | |
| minInternalBufferSize: { | |
| type: 'uint' | |
| }, | |
| minPoolSize: { | |
| default: 0, | |
| type: 'uint' | |
| }, | |
| minHeartbeatFrequencyMS: { | |
| default: 500, | |
| type: 'uint' | |
| }, | |
| monitorCommands: { | |
| default: false, | |
| type: 'boolean' | |
| }, | |
| name: { | |
| target: 'driverInfo', | |
| transform({ values: [value], options }) { | |
| return { ...options.driverInfo, name: String(value) }; | |
| } | |
| } as OptionDescriptor, | |
| noDelay: { | |
| default: true, | |
| type: 'boolean' | |
| }, | |
| pkFactory: { | |
| default: DEFAULT_PK_FACTORY, | |
| transform({ values: [value] }): PkFactory { | |
| if (isRecord(value, ['createPk'] as const) && typeof value.createPk === 'function') { | |
| return value as PkFactory; | |
| } | |
| throw new MongoParseError( | |
| `Option pkFactory must be an object with a createPk function, got ${value}` | |
| ); | |
| } | |
| }, | |
| promoteBuffers: { | |
| type: 'boolean' | |
| }, | |
| promoteLongs: { | |
| type: 'boolean' | |
| }, | |
| promoteValues: { | |
| type: 'boolean' | |
| }, | |
| useBigInt64: { | |
| type: 'boolean' | |
| }, | |
| proxyHost: { | |
| type: 'string' | |
| }, | |
| proxyPassword: { | |
| type: 'string' | |
| }, | |
| proxyPort: { | |
| type: 'uint' | |
| }, | |
| proxyUsername: { | |
| type: 'string' | |
| }, | |
| raw: { | |
| default: false, | |
| type: 'boolean' | |
| }, | |
| readConcern: { | |
| transform({ values: [value], options }) { | |
| if (value instanceof ReadConcern || isRecord(value, ['level'] as const)) { | |
| return ReadConcern.fromOptions({ ...options.readConcern, ...value } as any); | |
| } | |
| throw new MongoParseError(`ReadConcern must be an object, got ${JSON.stringify(value)}`); | |
| } | |
| }, | |
| readConcernLevel: { | |
| target: 'readConcern', | |
| transform({ values: [level], options }) { | |
| return ReadConcern.fromOptions({ | |
| ...options.readConcern, | |
| level: level as ReadConcernLevel | |
| }); | |
| } | |
| }, | |
| readPreference: { | |
| default: ReadPreference.primary, | |
| transform({ values: [value], options }) { | |
| if (value instanceof ReadPreference) { | |
| return ReadPreference.fromOptions({ | |
| readPreference: { ...options.readPreference, ...value }, | |
| ...value | |
| } as any); | |
| } | |
| if (isRecord(value, ['mode'] as const)) { | |
| const rp = ReadPreference.fromOptions({ | |
| readPreference: { ...options.readPreference, ...value }, | |
| ...value | |
| } as any); | |
| if (rp) return rp; | |
| else throw new MongoParseError(`Cannot make read preference from ${JSON.stringify(value)}`); | |
| } | |
| if (typeof value === 'string') { | |
| const rpOpts = { | |
| hedge: options.readPreference?.hedge, | |
| maxStalenessSeconds: options.readPreference?.maxStalenessSeconds | |
| }; | |
| return new ReadPreference( | |
| value as ReadPreferenceMode, | |
| options.readPreference?.tags, | |
| rpOpts | |
| ); | |
| } | |
| throw new MongoParseError(`Unknown ReadPreference value: ${value}`); | |
| } | |
| }, | |
| readPreferenceTags: { | |
| target: 'readPreference', | |
| transform({ | |
| values, | |
| options | |
| }: { | |
| values: Array<string | Record<string, string>[]>; | |
| options: MongoClientOptions; | |
| }) { | |
| const tags: Array<string | Record<string, string>> = Array.isArray(values[0]) | |
| ? values[0] | |
| : (values as Array<string>); | |
| const readPreferenceTags = []; | |
| for (const tag of tags) { | |
| const readPreferenceTag: TagSet = Object.create(null); | |
| if (typeof tag === 'string') { | |
| for (const [k, v] of entriesFromString(tag)) { | |
| readPreferenceTag[k] = v; | |
| } | |
| } | |
| if (isRecord(tag)) { | |
| for (const [k, v] of Object.entries(tag)) { | |
| readPreferenceTag[k] = v; | |
| } | |
| } | |
| readPreferenceTags.push(readPreferenceTag); | |
| } | |
| return ReadPreference.fromOptions({ | |
| readPreference: options.readPreference, | |
| readPreferenceTags | |
| }); | |
| } | |
| }, | |
| replicaSet: { | |
| type: 'string' | |
| }, | |
| retryReads: { | |
| default: true, | |
| type: 'boolean' | |
| }, | |
| retryWrites: { | |
| default: true, | |
| type: 'boolean' | |
| }, | |
| serializeFunctions: { | |
| type: 'boolean' | |
| }, | |
| serverMonitoringMode: { | |
| default: 'auto', | |
| transform({ values: [value] }) { | |
| if (!Object.values(ServerMonitoringMode).includes(value as any)) { | |
| throw new MongoParseError( | |
| 'serverMonitoringMode must be one of `auto`, `poll`, or `stream`' | |
| ); | |
| } | |
| return value; | |
| } | |
| }, | |
| serverSelectionTimeoutMS: { | |
| default: 30000, | |
| type: 'uint' | |
| }, | |
| servername: { | |
| type: 'string' | |
| }, | |
| socketTimeoutMS: { | |
| default: 0, | |
| type: 'uint' | |
| }, | |
| srvMaxHosts: { | |
| type: 'uint', | |
| default: 0 | |
| }, | |
| srvServiceName: { | |
| type: 'string', | |
| default: 'mongodb' | |
| }, | |
| ssl: { | |
| target: 'tls', | |
| type: 'boolean' | |
| }, | |
| timeoutMS: { | |
| type: 'uint' | |
| }, | |
| tls: { | |
| type: 'boolean' | |
| }, | |
| tlsAllowInvalidCertificates: { | |
| target: 'rejectUnauthorized', | |
| transform({ name, values: [value] }) { | |
| // allowInvalidCertificates is the inverse of rejectUnauthorized | |
| return !getBoolean(name, value); | |
| } | |
| }, | |
| tlsAllowInvalidHostnames: { | |
| target: 'checkServerIdentity', | |
| transform({ name, values: [value] }) { | |
| // tlsAllowInvalidHostnames means setting the checkServerIdentity function to a noop | |
| return getBoolean(name, value) ? () => undefined : undefined; | |
| } | |
| }, | |
| tlsCAFile: { | |
| type: 'string' | |
| }, | |
| tlsCRLFile: { | |
| type: 'string' | |
| }, | |
| tlsCertificateKeyFile: { | |
| type: 'string' | |
| }, | |
| tlsCertificateKeyFilePassword: { | |
| target: 'passphrase', | |
| type: 'any' | |
| }, | |
| tlsInsecure: { | |
| transform({ name, options, values: [value] }) { | |
| const tlsInsecure = getBoolean(name, value); | |
| if (tlsInsecure) { | |
| options.checkServerIdentity = () => undefined; | |
| options.rejectUnauthorized = false; | |
| } else { | |
| options.checkServerIdentity = options.tlsAllowInvalidHostnames | |
| ? () => undefined | |
| : undefined; | |
| options.rejectUnauthorized = options.tlsAllowInvalidCertificates ? false : true; | |
| } | |
| return tlsInsecure; | |
| } | |
| }, | |
| w: { | |
| target: 'writeConcern', | |
| transform({ values: [value], options }) { | |
| return WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, w: value as W } }); | |
| } | |
| }, | |
| waitQueueTimeoutMS: { | |
| default: 0, | |
| type: 'uint' | |
| }, | |
| writeConcern: { | |
| target: 'writeConcern', | |
| transform({ values: [value], options }) { | |
| if (isRecord(value) || value instanceof WriteConcern) { | |
| return WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| ...value | |
| } | |
| }); | |
| } else if (value === 'majority' || typeof value === 'number') { | |
| return WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| w: value | |
| } | |
| }); | |
| } | |
| throw new MongoParseError(`Invalid WriteConcern cannot parse: ${JSON.stringify(value)}`); | |
| } | |
| }, | |
| wtimeout: { | |
| deprecated: 'Please use wtimeoutMS instead', | |
| target: 'writeConcern', | |
| transform({ values: [value], options }) { | |
| const wc = WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| wtimeout: getUIntFromOptions('wtimeout', value) | |
| } | |
| }); | |
| if (wc) return wc; | |
| throw new MongoParseError(`Cannot make WriteConcern from wtimeout`); | |
| } | |
| } as OptionDescriptor, | |
| wtimeoutMS: { | |
| target: 'writeConcern', | |
| transform({ values: [value], options }) { | |
| const wc = WriteConcern.fromOptions({ | |
| writeConcern: { | |
| ...options.writeConcern, | |
| wtimeoutMS: getUIntFromOptions('wtimeoutMS', value) | |
| } | |
| }); | |
| if (wc) return wc; | |
| throw new MongoParseError(`Cannot make WriteConcern from wtimeout`); | |
| } | |
| }, | |
| zlibCompressionLevel: { | |
| default: 0, | |
| type: 'int' | |
| }, | |
| // Custom types for modifying core behavior | |
| connectionType: { type: 'any' }, | |
| srvPoller: { type: 'any' }, | |
| // Accepted NodeJS Options | |
| minDHSize: { type: 'any' }, | |
| pskCallback: { type: 'any' }, | |
| secureContext: { type: 'any' }, | |
| enableTrace: { type: 'any' }, | |
| requestCert: { type: 'any' }, | |
| rejectUnauthorized: { type: 'any' }, | |
| checkServerIdentity: { type: 'any' }, | |
| ALPNProtocols: { type: 'any' }, | |
| SNICallback: { type: 'any' }, | |
| session: { type: 'any' }, | |
| requestOCSP: { type: 'any' }, | |
| localAddress: { type: 'any' }, | |
| localPort: { type: 'any' }, | |
| hints: { type: 'any' }, | |
| lookup: { type: 'any' }, | |
| ca: { type: 'any' }, | |
| cert: { type: 'any' }, | |
| ciphers: { type: 'any' }, | |
| crl: { type: 'any' }, | |
| ecdhCurve: { type: 'any' }, | |
| key: { type: 'any' }, | |
| passphrase: { type: 'any' }, | |
| pfx: { type: 'any' }, | |
| secureProtocol: { type: 'any' }, | |
| index: { type: 'any' }, | |
| // Legacy options from v3 era | |
| useNewUrlParser: { | |
| type: 'boolean', | |
| deprecated: | |
| 'useNewUrlParser has no effect since Node.js Driver version 4.0.0 and will be removed in the next major version' | |
| } as OptionDescriptor, | |
| useUnifiedTopology: { | |
| type: 'boolean', | |
| deprecated: | |
| 'useUnifiedTopology has no effect since Node.js Driver version 4.0.0 and will be removed in the next major version' | |
| } as OptionDescriptor, | |
| // MongoLogger | |
| /** | |
| * @internal | |
| * TODO: NODE-5671 - remove internal flag | |
| */ | |
| mongodbLogPath: { | |
| transform({ values: [value] }) { | |
| if ( | |
| !( | |
| (typeof value === 'string' && ['stderr', 'stdout'].includes(value)) || | |
| (value && | |
| typeof value === 'object' && | |
| 'write' in value && | |
| typeof value.write === 'function') | |
| ) | |
| ) { | |
| throw new MongoAPIError( | |
| `Option 'mongodbLogPath' must be of type 'stderr' | 'stdout' | MongoDBLogWritable` | |
| ); | |
| } | |
| return value; | |
| } | |
| }, | |
| /** | |
| * @internal | |
| * TODO: NODE-5671 - remove internal flag | |
| */ | |
| mongodbLogComponentSeverities: { | |
| transform({ values: [value] }) { | |
| if (typeof value !== 'object' || !value) { | |
| throw new MongoAPIError(`Option 'mongodbLogComponentSeverities' must be a non-null object`); | |
| } | |
| for (const [k, v] of Object.entries(value)) { | |
| if (typeof v !== 'string' || typeof k !== 'string') { | |
| throw new MongoAPIError( | |
| `User input for option 'mongodbLogComponentSeverities' object cannot include a non-string key or value` | |
| ); | |
| } | |
| if (!Object.values(MongoLoggableComponent).some(val => val === k) && k !== 'default') { | |
| throw new MongoAPIError( | |
| `User input for option 'mongodbLogComponentSeverities' contains invalid key: ${k}` | |
| ); | |
| } | |
| if (!Object.values(SeverityLevel).some(val => val === v)) { | |
| throw new MongoAPIError( | |
| `Option 'mongodbLogComponentSeverities' does not support ${v} as a value for ${k}` | |
| ); | |
| } | |
| } | |
| return value; | |
| } | |
| }, | |
| /** | |
| * @internal | |
| * TODO: NODE-5671 - remove internal flag | |
| */ | |
| mongodbLogMaxDocumentLength: { type: 'uint' } | |
| } as Record<keyof MongoClientOptions, OptionDescriptor>; | |
| export const DEFAULT_OPTIONS = new CaseInsensitiveMap( | |
| Object.entries(OPTIONS) | |
| .filter(([, descriptor]) => descriptor.default != null) | |
| .map(([k, d]) => [k, d.default]) | |
| ); | |
| /** | |
| * Set of permitted feature flags | |
| * @internal | |
| */ | |
| export const FEATURE_FLAGS = new Set([ | |
| Symbol.for('@@mdb.skipPingOnConnect'), | |
| Symbol.for('@@mdb.enableMongoLogger'), | |
| Symbol.for('@@mdb.internalLoggerConfig') | |
| ]); | |