You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1095 lines
42 KiB
JavaScript
1095 lines
42 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.FEATURE_FLAGS = exports.DEFAULT_OPTIONS = exports.OPTIONS = exports.parseOptions = exports.resolveSRVRecord = void 0;
|
|
const dns = require("dns");
|
|
const fs = require("fs");
|
|
const mongodb_connection_string_url_1 = require("mongodb-connection-string-url");
|
|
const url_1 = require("url");
|
|
const mongo_credentials_1 = require("./cmap/auth/mongo_credentials");
|
|
const providers_1 = require("./cmap/auth/providers");
|
|
const client_metadata_1 = require("./cmap/handshake/client_metadata");
|
|
const compression_1 = require("./cmap/wire_protocol/compression");
|
|
const encrypter_1 = require("./encrypter");
|
|
const error_1 = require("./error");
|
|
const mongo_client_1 = require("./mongo_client");
|
|
const mongo_logger_1 = require("./mongo_logger");
|
|
const read_concern_1 = require("./read_concern");
|
|
const read_preference_1 = require("./read_preference");
|
|
const utils_1 = require("./utils");
|
|
const write_concern_1 = require("./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
|
|
*/
|
|
async function resolveSRVRecord(options) {
|
|
if (typeof options.srvHost !== 'string') {
|
|
throw new error_1.MongoAPIError('Option "srvHost" must not be empty');
|
|
}
|
|
if (options.srvHost.split('.').length < 3) {
|
|
// TODO(NODE-3484): Replace with MongoConnectionStringError
|
|
throw new error_1.MongoAPIError('URI must include hostname, domain name, and tld');
|
|
}
|
|
// Resolve the SRV record and use the result as the list of hosts to connect to.
|
|
const lookupAddress = options.srvHost;
|
|
const addresses = await dns.promises.resolveSrv(`_${options.srvServiceName}._tcp.${lookupAddress}`);
|
|
if (addresses.length === 0) {
|
|
throw new error_1.MongoAPIError('No addresses found at host');
|
|
}
|
|
for (const { name } of addresses) {
|
|
if (!(0, utils_1.matchesParentDomain)(name, lookupAddress)) {
|
|
throw new error_1.MongoAPIError('Server record does not share hostname with parent URI');
|
|
}
|
|
}
|
|
const hostAddresses = addresses.map(r => utils_1.HostAddress.fromString(`${r.name}:${r.port ?? 27017}`));
|
|
validateLoadBalancedOptions(hostAddresses, options, true);
|
|
// Resolve TXT record and add options from there if they exist.
|
|
let record;
|
|
try {
|
|
record = await dns.promises.resolveTxt(lookupAddress);
|
|
}
|
|
catch (error) {
|
|
if (error.code !== 'ENODATA' && error.code !== 'ENOTFOUND') {
|
|
throw error;
|
|
}
|
|
return hostAddresses;
|
|
}
|
|
if (record.length > 1) {
|
|
throw new error_1.MongoParseError('Multiple text records not allowed');
|
|
}
|
|
const txtRecordOptions = new url_1.URLSearchParams(record[0].join(''));
|
|
const txtRecordOptionKeys = [...txtRecordOptions.keys()];
|
|
if (txtRecordOptionKeys.some(key => !VALID_TXT_RECORDS.includes(key))) {
|
|
throw new error_1.MongoParseError(`Text record may only set any of: ${VALID_TXT_RECORDS.join(', ')}`);
|
|
}
|
|
if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {
|
|
throw new error_1.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 &&
|
|
!providers_1.AUTH_MECHS_AUTH_SRC_EXTERNAL.has(options.credentials.mechanism)) {
|
|
options.credentials = mongo_credentials_1.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 error_1.MongoParseError('Cannot combine replicaSet option with srvMaxHosts');
|
|
}
|
|
validateLoadBalancedOptions(hostAddresses, options, true);
|
|
return hostAddresses;
|
|
}
|
|
exports.resolveSRVRecord = resolveSRVRecord;
|
|
/**
|
|
* 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) {
|
|
if (!allOptions)
|
|
return;
|
|
const check = (a, b) => {
|
|
if (allOptions.has(a) && allOptions.has(b)) {
|
|
throw new error_1.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');
|
|
}
|
|
const TRUTHS = new Set(['true', 't', '1', 'y', 'yes']);
|
|
const FALSEHOODS = new Set(['false', 'f', '0', 'n', 'no', '-1']);
|
|
function getBoolean(name, value) {
|
|
if (typeof value === 'boolean')
|
|
return value;
|
|
const valueString = String(value).toLowerCase();
|
|
if (TRUTHS.has(valueString)) {
|
|
if (valueString !== 'true') {
|
|
(0, utils_1.emitWarningOnce)(`deprecated value for ${name} : ${valueString} - please update to ${name} : true instead`);
|
|
}
|
|
return true;
|
|
}
|
|
if (FALSEHOODS.has(valueString)) {
|
|
if (valueString !== 'false') {
|
|
(0, utils_1.emitWarningOnce)(`deprecated value for ${name} : ${valueString} - please update to ${name} : false instead`);
|
|
}
|
|
return false;
|
|
}
|
|
throw new error_1.MongoParseError(`Expected ${name} to be stringified boolean value, got: ${value}`);
|
|
}
|
|
function getIntFromOptions(name, value) {
|
|
const parsedInt = (0, utils_1.parseInteger)(value);
|
|
if (parsedInt != null) {
|
|
return parsedInt;
|
|
}
|
|
throw new error_1.MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`);
|
|
}
|
|
function getUIntFromOptions(name, value) {
|
|
const parsedValue = getIntFromOptions(name, value);
|
|
if (parsedValue < 0) {
|
|
throw new error_1.MongoParseError(`${name} can only be a positive int value, got: ${value}`);
|
|
}
|
|
return parsedValue;
|
|
}
|
|
function* entriesFromString(value) {
|
|
const keyValuePairs = value.split(',');
|
|
for (const keyValue of keyValuePairs) {
|
|
const [key, value] = keyValue.split(':');
|
|
if (value == null) {
|
|
throw new error_1.MongoParseError('Cannot have undefined values in key value pairs');
|
|
}
|
|
yield [key, value];
|
|
}
|
|
}
|
|
class CaseInsensitiveMap extends Map {
|
|
constructor(entries = []) {
|
|
super(entries.map(([k, v]) => [k.toLowerCase(), v]));
|
|
}
|
|
has(k) {
|
|
return super.has(k.toLowerCase());
|
|
}
|
|
get(k) {
|
|
return super.get(k.toLowerCase());
|
|
}
|
|
set(k, v) {
|
|
return super.set(k.toLowerCase(), v);
|
|
}
|
|
delete(k) {
|
|
return super.delete(k.toLowerCase());
|
|
}
|
|
}
|
|
function parseOptions(uri, mongoClient = undefined, options = {}) {
|
|
if (mongoClient != null && !(mongoClient instanceof mongo_client_1.MongoClient)) {
|
|
options = mongoClient;
|
|
mongoClient = undefined;
|
|
}
|
|
// validate BSONOptions
|
|
if (options.useBigInt64 && typeof options.promoteLongs === 'boolean' && !options.promoteLongs) {
|
|
throw new error_1.MongoAPIError('Must request either bigint or Long for int64 deserialization');
|
|
}
|
|
if (options.useBigInt64 && typeof options.promoteValues === 'boolean' && !options.promoteValues) {
|
|
throw new error_1.MongoAPIError('Must request either bigint or Long for int64 deserialization');
|
|
}
|
|
const url = new mongodb_connection_string_url_1.default(uri);
|
|
const { hosts, isSRV } = url;
|
|
const mongoOptions = Object.create(null);
|
|
// Feature flags
|
|
for (const flag of Object.getOwnPropertySymbols(options)) {
|
|
if (exports.FEATURE_FLAGS.has(flag)) {
|
|
mongoOptions[flag] = options[flag];
|
|
}
|
|
}
|
|
mongoOptions.hosts = isSRV ? [] : hosts.map(utils_1.HostAddress.fromString);
|
|
const urlOptions = new CaseInsensitiveMap();
|
|
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 = {
|
|
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)];
|
|
if (values.includes('')) {
|
|
throw new error_1.MongoAPIError('URI cannot contain options with no value');
|
|
}
|
|
if (!urlOptions.has(key)) {
|
|
urlOptions.set(key, values);
|
|
}
|
|
}
|
|
const objectOptions = new CaseInsensitiveMap(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 error_1.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)) {
|
|
throw new error_1.MongoParseError('Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.');
|
|
}
|
|
}
|
|
}
|
|
if (objectOptions.has('loadBalanced')) {
|
|
throw new error_1.MongoParseError('loadBalanced is only a valid option in the URI');
|
|
}
|
|
// All option collection
|
|
const allProvidedOptions = new CaseInsensitiveMap();
|
|
const allProvidedKeys = new Set([...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('tlsCertificateKeyFile') &&
|
|
!allProvidedOptions.has('tlsCertificateFile')) {
|
|
allProvidedOptions.set('tlsCertificateFile', allProvidedOptions.get('tlsCertificateKeyFile'));
|
|
}
|
|
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 error_1.MongoParseError('All values of tls/ssl must be the same.');
|
|
}
|
|
}
|
|
checkTLSOptions(allProvidedOptions);
|
|
const unsupportedOptions = (0, utils_1.setDifference)(allProvidedKeys, Array.from(Object.keys(exports.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 error_1.MongoParseError(`${optionWord} ${Array.from(unsupportedOptions).join(', ')} ${isOrAre} not supported`);
|
|
}
|
|
// Option parsing and setting
|
|
for (const [key, descriptor] of Object.entries(exports.OPTIONS)) {
|
|
const values = allProvidedOptions.get(key);
|
|
if (!values || values.length === 0) {
|
|
if (exports.DEFAULT_OPTIONS.has(key)) {
|
|
setOption(mongoOptions, key, descriptor, [exports.DEFAULT_OPTIONS.get(key)]);
|
|
}
|
|
}
|
|
else {
|
|
const { deprecated } = descriptor;
|
|
if (deprecated) {
|
|
const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : '';
|
|
(0, utils_1.emitWarning)(`${key} is a deprecated option${deprecatedMsg}`);
|
|
}
|
|
setOption(mongoOptions, key, descriptor, values);
|
|
}
|
|
}
|
|
if (mongoOptions.credentials) {
|
|
const isGssapi = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_GSSAPI;
|
|
const isX509 = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_X509;
|
|
const isAws = mongoOptions.credentials.mechanism === providers_1.AuthMechanism.MONGODB_AWS;
|
|
const isOidc = mongoOptions.credentials.mechanism === providers_1.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 error_1.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 = mongo_credentials_1.MongoCredentials.merge(mongoOptions.credentials, {
|
|
source: mongoOptions.dbName
|
|
});
|
|
}
|
|
if (isAws && mongoOptions.credentials.username && !mongoOptions.credentials.password) {
|
|
throw new error_1.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 === providers_1.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_1.Encrypter.checkForMongoCrypt();
|
|
mongoOptions.encrypter = new encrypter_1.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 error_1.MongoAPIError('SRV URI does not support directConnection');
|
|
}
|
|
if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') {
|
|
throw new error_1.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 error_1.MongoParseError('Cannot use srvMaxHosts or srvServiceName with a non-srv connection string');
|
|
}
|
|
}
|
|
if (mongoOptions.directConnection && mongoOptions.hosts.length !== 1) {
|
|
throw new error_1.MongoParseError('directConnection option requires exactly one host');
|
|
}
|
|
if (!mongoOptions.proxyHost &&
|
|
(mongoOptions.proxyPort || mongoOptions.proxyUsername || mongoOptions.proxyPassword)) {
|
|
throw new error_1.MongoParseError('Must specify proxyHost if other proxy options are passed');
|
|
}
|
|
if ((mongoOptions.proxyUsername && !mongoOptions.proxyPassword) ||
|
|
(!mongoOptions.proxyUsername && mongoOptions.proxyPassword)) {
|
|
throw new error_1.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 error_1.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 = {};
|
|
let loggerClientOptions = {};
|
|
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_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
|
|
};
|
|
}
|
|
mongoOptions.mongoLoggerOptions = mongo_logger_1.MongoLogger.resolveOptions(loggerEnvOptions, loggerClientOptions);
|
|
mongoOptions.metadata = (0, client_metadata_1.makeClientMetadata)(mongoOptions);
|
|
return mongoOptions;
|
|
}
|
|
exports.parseOptions = parseOptions;
|
|
/**
|
|
* #### 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, mongoOptions, isSrv) {
|
|
if (mongoOptions.loadBalanced) {
|
|
if (hosts.length > 1) {
|
|
throw new error_1.MongoParseError(LB_SINGLE_HOST_ERROR);
|
|
}
|
|
if (mongoOptions.replicaSet) {
|
|
throw new error_1.MongoParseError(LB_REPLICA_SET_ERROR);
|
|
}
|
|
if (mongoOptions.directConnection) {
|
|
throw new error_1.MongoParseError(LB_DIRECT_CONNECTION_ERROR);
|
|
}
|
|
if (isSrv && mongoOptions.srvMaxHosts > 0) {
|
|
throw new error_1.MongoParseError('Cannot limit srv hosts with loadBalanced enabled');
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
function setOption(mongoOptions, key, descriptor, values) {
|
|
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 (!(0, utils_1.isRecord)(values[0])) {
|
|
throw new error_1.MongoParseError(`${name} must be an object`);
|
|
}
|
|
mongoOptions[name] = values[0];
|
|
break;
|
|
case 'any':
|
|
mongoOptions[name] = values[0];
|
|
break;
|
|
default: {
|
|
if (!transform) {
|
|
throw new error_1.MongoParseError('Descriptors missing a type must define a transform');
|
|
}
|
|
const transformValue = transform({ name, options: mongoOptions, values });
|
|
mongoOptions[name] = transformValue;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
exports.OPTIONS = {
|
|
appName: {
|
|
type: 'string'
|
|
},
|
|
auth: {
|
|
target: 'credentials',
|
|
transform({ name, options, values: [value] }) {
|
|
if (!(0, utils_1.isRecord)(value, ['username', 'password'])) {
|
|
throw new error_1.MongoParseError(`${name} must be an object with 'username' and 'password' properties`);
|
|
}
|
|
return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
|
|
username: value.username,
|
|
password: value.password
|
|
});
|
|
}
|
|
},
|
|
authMechanism: {
|
|
target: 'credentials',
|
|
transform({ options, values: [value] }) {
|
|
const mechanisms = Object.values(providers_1.AuthMechanism);
|
|
const [mechanism] = mechanisms.filter(m => m.match(RegExp(String.raw `\b${value}\b`, 'i')));
|
|
if (!mechanism) {
|
|
throw new error_1.MongoParseError(`authMechanism one of ${mechanisms}, got ${value}`);
|
|
}
|
|
let source = options.credentials?.source;
|
|
if (mechanism === providers_1.AuthMechanism.MONGODB_PLAIN ||
|
|
providers_1.AUTH_MECHS_AUTH_SRC_EXTERNAL.has(mechanism)) {
|
|
// some mechanisms have '$external' as the Auth Source
|
|
source = '$external';
|
|
}
|
|
let password = options.credentials?.password;
|
|
if (mechanism === providers_1.AuthMechanism.MONGODB_X509 && password === '') {
|
|
password = undefined;
|
|
}
|
|
return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
|
|
mechanism,
|
|
source,
|
|
password
|
|
});
|
|
}
|
|
},
|
|
authMechanismProperties: {
|
|
target: 'credentials',
|
|
transform({ options, values }) {
|
|
// 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 (!(0, utils_1.isRecord)(optionValue)) {
|
|
throw new error_1.MongoParseError('AuthMechanismProperties must be an object');
|
|
}
|
|
mechanismProperties = { ...optionValue };
|
|
}
|
|
}
|
|
return mongo_credentials_1.MongoCredentials.merge(options.credentials, {
|
|
mechanismProperties
|
|
});
|
|
}
|
|
},
|
|
authSource: {
|
|
target: 'credentials',
|
|
transform({ options, values: [value] }) {
|
|
const source = String(value);
|
|
return mongo_credentials_1.MongoCredentials.merge(options.credentials, { source });
|
|
}
|
|
},
|
|
autoEncryption: {
|
|
type: 'record'
|
|
},
|
|
bsonRegExp: {
|
|
type: 'boolean'
|
|
},
|
|
serverApi: {
|
|
target: 'serverApi',
|
|
transform({ values: [version] }) {
|
|
const serverApiToValidate = typeof version === 'string' ? { version } : version;
|
|
const versionToValidate = serverApiToValidate && serverApiToValidate.version;
|
|
if (!versionToValidate) {
|
|
throw new error_1.MongoParseError(`Invalid \`serverApi\` property; must specify a version from the following enum: ["${Object.values(mongo_client_1.ServerApiVersion).join('", "')}"]`);
|
|
}
|
|
if (!Object.values(mongo_client_1.ServerApiVersion).some(v => v === versionToValidate)) {
|
|
throw new error_1.MongoParseError(`Invalid server API version=${versionToValidate}; must be in the following enum: ["${Object.values(mongo_client_1.ServerApiVersion).join('", "')}"]`);
|
|
}
|
|
return serverApiToValidate;
|
|
}
|
|
},
|
|
checkKeys: {
|
|
type: 'boolean'
|
|
},
|
|
compressors: {
|
|
default: 'none',
|
|
target: 'compressors',
|
|
transform({ values }) {
|
|
const compressionList = new Set();
|
|
for (const compVal of values) {
|
|
const compValArray = typeof compVal === 'string' ? compVal.split(',') : compVal;
|
|
if (!Array.isArray(compValArray)) {
|
|
throw new error_1.MongoInvalidArgumentError('compressors must be an array or a comma-delimited list of strings');
|
|
}
|
|
for (const c of compValArray) {
|
|
if (Object.keys(compression_1.Compressor).includes(String(c))) {
|
|
compressionList.add(String(c));
|
|
}
|
|
else {
|
|
throw new error_1.MongoInvalidArgumentError(`${c} is not a valid compression mechanism. Must be one of: ${Object.keys(compression_1.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] }) {
|
|
const transformValue = getIntFromOptions(name, value);
|
|
if (transformValue === 4 || transformValue === 6) {
|
|
return transformValue;
|
|
}
|
|
throw new error_1.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] }) {
|
|
const wc = write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
fsync: getBoolean(name, value)
|
|
}
|
|
});
|
|
if (!wc)
|
|
throw new error_1.MongoParseError(`Unable to make a writeConcern from fsync=${value}`);
|
|
return wc;
|
|
}
|
|
},
|
|
heartbeatFrequencyMS: {
|
|
default: 10000,
|
|
type: 'uint'
|
|
},
|
|
ignoreUndefined: {
|
|
type: 'boolean'
|
|
},
|
|
j: {
|
|
deprecated: 'Please use journal instead',
|
|
target: 'writeConcern',
|
|
transform({ name, options, values: [value] }) {
|
|
const wc = write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
journal: getBoolean(name, value)
|
|
}
|
|
});
|
|
if (!wc)
|
|
throw new error_1.MongoParseError(`Unable to make a writeConcern from journal=${value}`);
|
|
return wc;
|
|
}
|
|
},
|
|
journal: {
|
|
target: 'writeConcern',
|
|
transform({ name, options, values: [value] }) {
|
|
const wc = write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
journal: getBoolean(name, value)
|
|
}
|
|
});
|
|
if (!wc)
|
|
throw new error_1.MongoParseError(`Unable to make a writeConcern from journal=${value}`);
|
|
return wc;
|
|
}
|
|
},
|
|
keepAlive: {
|
|
default: true,
|
|
type: 'boolean',
|
|
deprecated: 'Will not be able to turn off in the future.'
|
|
},
|
|
keepAliveInitialDelay: {
|
|
default: 120000,
|
|
type: 'uint',
|
|
deprecated: 'Will not be configurable in the future.'
|
|
},
|
|
loadBalanced: {
|
|
default: false,
|
|
type: 'boolean'
|
|
},
|
|
localThresholdMS: {
|
|
default: 15,
|
|
type: 'uint'
|
|
},
|
|
maxConnecting: {
|
|
default: 2,
|
|
transform({ name, values: [value] }) {
|
|
const maxConnecting = getUIntFromOptions(name, value);
|
|
if (maxConnecting === 0) {
|
|
throw new error_1.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 read_preference_1.ReadPreference.fromOptions({
|
|
readPreference: { ...options.readPreference, maxStalenessSeconds }
|
|
});
|
|
}
|
|
else {
|
|
return new read_preference_1.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) };
|
|
}
|
|
},
|
|
noDelay: {
|
|
default: true,
|
|
type: 'boolean'
|
|
},
|
|
pkFactory: {
|
|
default: utils_1.DEFAULT_PK_FACTORY,
|
|
transform({ values: [value] }) {
|
|
if ((0, utils_1.isRecord)(value, ['createPk']) && typeof value.createPk === 'function') {
|
|
return value;
|
|
}
|
|
throw new error_1.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 read_concern_1.ReadConcern || (0, utils_1.isRecord)(value, ['level'])) {
|
|
return read_concern_1.ReadConcern.fromOptions({ ...options.readConcern, ...value });
|
|
}
|
|
throw new error_1.MongoParseError(`ReadConcern must be an object, got ${JSON.stringify(value)}`);
|
|
}
|
|
},
|
|
readConcernLevel: {
|
|
target: 'readConcern',
|
|
transform({ values: [level], options }) {
|
|
return read_concern_1.ReadConcern.fromOptions({
|
|
...options.readConcern,
|
|
level: level
|
|
});
|
|
}
|
|
},
|
|
readPreference: {
|
|
default: read_preference_1.ReadPreference.primary,
|
|
transform({ values: [value], options }) {
|
|
if (value instanceof read_preference_1.ReadPreference) {
|
|
return read_preference_1.ReadPreference.fromOptions({
|
|
readPreference: { ...options.readPreference, ...value },
|
|
...value
|
|
});
|
|
}
|
|
if ((0, utils_1.isRecord)(value, ['mode'])) {
|
|
const rp = read_preference_1.ReadPreference.fromOptions({
|
|
readPreference: { ...options.readPreference, ...value },
|
|
...value
|
|
});
|
|
if (rp)
|
|
return rp;
|
|
else
|
|
throw new error_1.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 read_preference_1.ReadPreference(value, options.readPreference?.tags, rpOpts);
|
|
}
|
|
throw new error_1.MongoParseError(`Unknown ReadPreference value: ${value}`);
|
|
}
|
|
},
|
|
readPreferenceTags: {
|
|
target: 'readPreference',
|
|
transform({ values, options }) {
|
|
const tags = Array.isArray(values[0])
|
|
? values[0]
|
|
: values;
|
|
const readPreferenceTags = [];
|
|
for (const tag of tags) {
|
|
const readPreferenceTag = Object.create(null);
|
|
if (typeof tag === 'string') {
|
|
for (const [k, v] of entriesFromString(tag)) {
|
|
readPreferenceTag[k] = v;
|
|
}
|
|
}
|
|
if ((0, utils_1.isRecord)(tag)) {
|
|
for (const [k, v] of Object.entries(tag)) {
|
|
readPreferenceTag[k] = v;
|
|
}
|
|
}
|
|
readPreferenceTags.push(readPreferenceTag);
|
|
}
|
|
return read_preference_1.ReadPreference.fromOptions({
|
|
readPreference: options.readPreference,
|
|
readPreferenceTags
|
|
});
|
|
}
|
|
},
|
|
replicaSet: {
|
|
type: 'string'
|
|
},
|
|
retryReads: {
|
|
default: true,
|
|
type: 'boolean'
|
|
},
|
|
retryWrites: {
|
|
default: true,
|
|
type: 'boolean'
|
|
},
|
|
serializeFunctions: {
|
|
type: 'boolean'
|
|
},
|
|
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'
|
|
},
|
|
sslCA: {
|
|
target: 'ca',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
sslCRL: {
|
|
target: 'crl',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
sslCert: {
|
|
target: 'cert',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
sslKey: {
|
|
target: 'key',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
sslPass: {
|
|
deprecated: true,
|
|
target: 'passphrase',
|
|
type: 'string'
|
|
},
|
|
sslValidate: {
|
|
target: 'rejectUnauthorized',
|
|
type: 'boolean'
|
|
},
|
|
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: {
|
|
target: 'ca',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
tlsCertificateFile: {
|
|
target: 'cert',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
tlsCertificateKeyFile: {
|
|
target: 'key',
|
|
transform({ values: [value] }) {
|
|
return fs.readFileSync(String(value), { encoding: 'ascii' });
|
|
}
|
|
},
|
|
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 write_concern_1.WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, w: value } });
|
|
}
|
|
},
|
|
waitQueueTimeoutMS: {
|
|
default: 0,
|
|
type: 'uint'
|
|
},
|
|
writeConcern: {
|
|
target: 'writeConcern',
|
|
transform({ values: [value], options }) {
|
|
if ((0, utils_1.isRecord)(value) || value instanceof write_concern_1.WriteConcern) {
|
|
return write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
...value
|
|
}
|
|
});
|
|
}
|
|
else if (value === 'majority' || typeof value === 'number') {
|
|
return write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
w: value
|
|
}
|
|
});
|
|
}
|
|
throw new error_1.MongoParseError(`Invalid WriteConcern cannot parse: ${JSON.stringify(value)}`);
|
|
}
|
|
},
|
|
wtimeout: {
|
|
deprecated: 'Please use wtimeoutMS instead',
|
|
target: 'writeConcern',
|
|
transform({ values: [value], options }) {
|
|
const wc = write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
wtimeout: getUIntFromOptions('wtimeout', value)
|
|
}
|
|
});
|
|
if (wc)
|
|
return wc;
|
|
throw new error_1.MongoParseError(`Cannot make WriteConcern from wtimeout`);
|
|
}
|
|
},
|
|
wtimeoutMS: {
|
|
target: 'writeConcern',
|
|
transform({ values: [value], options }) {
|
|
const wc = write_concern_1.WriteConcern.fromOptions({
|
|
writeConcern: {
|
|
...options.writeConcern,
|
|
wtimeoutMS: getUIntFromOptions('wtimeoutMS', value)
|
|
}
|
|
});
|
|
if (wc)
|
|
return wc;
|
|
throw new error_1.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, these are unused but left here to avoid errors with CSFLE lib
|
|
useNewUrlParser: { type: 'boolean' },
|
|
useUnifiedTopology: { type: 'boolean' },
|
|
// MongoLogger
|
|
// TODO(NODE-4849): Tighten the type of mongodbLogPath
|
|
mongodbLogPath: { type: 'any' }
|
|
};
|
|
exports.DEFAULT_OPTIONS = new CaseInsensitiveMap(Object.entries(exports.OPTIONS)
|
|
.filter(([, descriptor]) => descriptor.default != null)
|
|
.map(([k, d]) => [k, d.default]));
|
|
/**
|
|
* Set of permitted feature flags
|
|
* @internal
|
|
*/
|
|
exports.FEATURE_FLAGS = new Set([
|
|
Symbol.for('@@mdb.skipPingOnConnect'),
|
|
Symbol.for('@@mdb.enableMongoLogger'),
|
|
Symbol.for('@@mdb.internalLoggerConfig')
|
|
]);
|
|
//# sourceMappingURL=connection_string.js.map
|