opusdev/vector-similarity-api
1
1import * as dns from 'dns';2import ConnectionString from 'mongodb-connection-string-url';3import { URLSearchParams } from 'url';4 5import type { Document } from './bson';6import { MongoCredentials } from './cmap/auth/mongo_credentials';7import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';8import { Compressor, type CompressorName } from './cmap/wire_protocol/compression';9import { Encrypter } from './encrypter';10import {11 MongoAPIError,12 MongoInvalidArgumentError,13 MongoMissingCredentialsError,14 MongoParseError15} from './error';16import {17 MongoClient,18 type MongoClientOptions,19 type MongoOptions,20 type PkFactory,21 type ServerApi,22 ServerApiVersion23} from './mongo_client';24import { MongoLoggableComponent, MongoLogger, SeverityLevel } from './mongo_logger';25import { ReadConcern, type ReadConcernLevel } from './read_concern';26import { ReadPreference, type ReadPreferenceMode } from './read_preference';27import { ServerMonitoringMode } from './sdam/monitor';28import type { TagSet } from './sdam/server_description';29import {30 checkParentDomainMatch,31 DEFAULT_PK_FACTORY,32 emitWarning,33 HostAddress,34 isRecord,35 parseInteger,36 setDifference,37 squashError38} from './utils';39import { type W, WriteConcern } from './write_concern';40 41const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced'];42 43const LB_SINGLE_HOST_ERROR = 'loadBalanced option only supported with a single host in the URI';44const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSet option';45const LB_DIRECT_CONNECTION_ERROR =46 'loadBalanced option not supported when directConnection is provided';47 48function retryDNSTimeoutFor(api: 'resolveSrv'): (a: string) => Promise<dns.SrvRecord[]>;49function retryDNSTimeoutFor(api: 'resolveTxt'): (a: string) => Promise<string[][]>;50function retryDNSTimeoutFor(51 api: 'resolveSrv' | 'resolveTxt'52): (a: string) => Promise<dns.SrvRecord[] | string[][]> {53 return async function dnsReqRetryTimeout(lookupAddress: string) {54 try {55 return await dns.promises[api](lookupAddress);56 } catch (firstDNSError) {57 if (firstDNSError.code === dns.TIMEOUT) {58 return await dns.promises[api](lookupAddress);59 } else {60 throw firstDNSError;61 }62 }63 };64}65 66const resolveSrv = retryDNSTimeoutFor('resolveSrv');67const resolveTxt = retryDNSTimeoutFor('resolveTxt');68 69/**70 * Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal71 * connection string.72 *73 * @param uri - The connection string to parse74 * @param options - Optional user provided connection string options75 */76export async function resolveSRVRecord(options: MongoOptions): Promise<HostAddress[]> {77 if (typeof options.srvHost !== 'string') {78 throw new MongoAPIError('Option "srvHost" must not be empty');79 }80 81 // Asynchronously start TXT resolution so that we do not have to wait until82 // the SRV record is resolved before starting a second DNS query.83 const lookupAddress = options.srvHost;84 const txtResolutionPromise = resolveTxt(lookupAddress);85 86 txtResolutionPromise.then(undefined, squashError); // rejections will be handled later87 88 const hostname = `_${options.srvServiceName}._tcp.${lookupAddress}`;89 // Resolve the SRV record and use the result as the list of hosts to connect to.90 const addresses = await resolveSrv(hostname);91 92 if (addresses.length === 0) {93 throw new MongoAPIError('No addresses found at host');94 }95 96 for (const { name } of addresses) {97 checkParentDomainMatch(name, lookupAddress);98 }99 100 const hostAddresses = addresses.map(r => HostAddress.fromString(`${r.name}:${r.port ?? 27017}`));101 102 validateLoadBalancedOptions(hostAddresses, options, true);103 104 // Use the result of resolving the TXT record and add options from there if they exist.105 let record;106 try {107 record = await txtResolutionPromise;108 } catch (error) {109 if (error.code !== 'ENODATA' && error.code !== 'ENOTFOUND') {110 throw error;111 }112 return hostAddresses;113 }114 115 if (record.length > 1) {116 throw new MongoParseError('Multiple text records not allowed');117 }118 119 const txtRecordOptions = new URLSearchParams(record[0].join(''));120 const txtRecordOptionKeys = [...txtRecordOptions.keys()];121 if (txtRecordOptionKeys.some(key => !VALID_TXT_RECORDS.includes(key))) {122 throw new MongoParseError(`Text record may only set any of: ${VALID_TXT_RECORDS.join(', ')}`);123 }124 125 if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {126 throw new MongoParseError('Cannot have empty URI params in DNS TXT Record');127 }128 129 const source = txtRecordOptions.get('authSource') ?? undefined;130 const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;131 const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;132 133 if (134 !options.userSpecifiedAuthSource &&135 source &&136 options.credentials &&137 !AUTH_MECHS_AUTH_SRC_EXTERNAL.has(options.credentials.mechanism)138 ) {139 options.credentials = MongoCredentials.merge(options.credentials, { source });140 }141 142 if (!options.userSpecifiedReplicaSet && replicaSet) {143 options.replicaSet = replicaSet;144 }145 146 if (loadBalanced === 'true') {147 options.loadBalanced = true;148 }149 150 if (options.replicaSet && options.srvMaxHosts > 0) {151 throw new MongoParseError('Cannot combine replicaSet option with srvMaxHosts');152 }153 154 validateLoadBalancedOptions(hostAddresses, options, true);155 156 return hostAddresses;157}158 159/**160 * Checks if TLS options are valid161 *162 * @param allOptions - All options provided by user or included in default options map163 * @throws MongoAPIError if TLS options are invalid164 */165function checkTLSOptions(allOptions: CaseInsensitiveMap): void {166 if (!allOptions) return;167 const check = (a: string, b: string) => {168 if (allOptions.has(a) && allOptions.has(b)) {169 throw new MongoAPIError(`The '${a}' option cannot be used with the '${b}' option`);170 }171 };172 check('tlsInsecure', 'tlsAllowInvalidCertificates');173 check('tlsInsecure', 'tlsAllowInvalidHostnames');174}175function getBoolean(name: string, value: unknown): boolean {176 if (typeof value === 'boolean') return value;177 switch (value) {178 case 'true':179 return true;180 case 'false':181 return false;182 default:183 throw new MongoParseError(`${name} must be either "true" or "false"`);184 }185}186 187function getIntFromOptions(name: string, value: unknown): number {188 const parsedInt = parseInteger(value);189 if (parsedInt != null) {190 return parsedInt;191 }192 throw new MongoParseError(`Expected ${name} to be stringified int value, got: ${value}`);193}194 195function getUIntFromOptions(name: string, value: unknown): number {196 const parsedValue = getIntFromOptions(name, value);197 if (parsedValue < 0) {198 throw new MongoParseError(`${name} can only be a positive int value, got: ${value}`);199 }200 return parsedValue;201}202 203function* entriesFromString(value: string): Generator<[string, string]> {204 if (value === '') {205 return;206 }207 const keyValuePairs = value.split(',');208 for (const keyValue of keyValuePairs) {209 const [key, value] = keyValue.split(/:(.*)/);210 if (value == null) {211 throw new MongoParseError('Cannot have undefined values in key value pairs');212 }213 214 yield [key, value];215 }216}217 218class CaseInsensitiveMap<Value = any> extends Map<string, Value> {219 constructor(entries: Array<[string, any]> = []) {220 super(entries.map(([k, v]) => [k.toLowerCase(), v]));221 }222 override has(k: string) {223 return super.has(k.toLowerCase());224 }225 override get(k: string) {226 return super.get(k.toLowerCase());227 }228 override set(k: string, v: any) {229 return super.set(k.toLowerCase(), v);230 }231 override delete(k: string): boolean {232 return super.delete(k.toLowerCase());233 }234}235 236export function parseOptions(237 uri: string,238 mongoClient: MongoClient | MongoClientOptions | undefined = undefined,239 options: MongoClientOptions = {}240): MongoOptions {241 if (mongoClient != null && !(mongoClient instanceof MongoClient)) {242 options = mongoClient;243 mongoClient = undefined;244 }245 246 // validate BSONOptions247 if (options.useBigInt64 && typeof options.promoteLongs === 'boolean' && !options.promoteLongs) {248 throw new MongoAPIError('Must request either bigint or Long for int64 deserialization');249 }250 251 if (options.useBigInt64 && typeof options.promoteValues === 'boolean' && !options.promoteValues) {252 throw new MongoAPIError('Must request either bigint or Long for int64 deserialization');253 }254 255 const url = new ConnectionString(uri);256 const { hosts, isSRV } = url;257 258 const mongoOptions = Object.create(null);259 260 mongoOptions.hosts = isSRV ? [] : hosts.map(HostAddress.fromString);261 262 const urlOptions = new CaseInsensitiveMap<unknown[]>();263 264 if (url.pathname !== '/' && url.pathname !== '') {265 const dbName = decodeURIComponent(266 url.pathname[0] === '/' ? url.pathname.slice(1) : url.pathname267 );268 if (dbName) {269 urlOptions.set('dbName', [dbName]);270 }271 }272 273 if (url.username !== '') {274 const auth: Document = {275 username: decodeURIComponent(url.username)276 };277 278 if (typeof url.password === 'string') {279 auth.password = decodeURIComponent(url.password);280 }281 282 urlOptions.set('auth', [auth]);283 }284 285 for (const key of url.searchParams.keys()) {286 const values = url.searchParams.getAll(key);287 288 const isReadPreferenceTags = /readPreferenceTags/i.test(key);289 290 if (!isReadPreferenceTags && values.length > 1) {291 throw new MongoInvalidArgumentError(292 `URI option "${key}" cannot appear more than once in the connection string`293 );294 }295 296 if (!isReadPreferenceTags && values.includes('')) {297 throw new MongoAPIError(`URI option "${key}" cannot be specified with no value`);298 }299 300 if (!urlOptions.has(key)) {301 urlOptions.set(key, values);302 }303 }304 305 const objectOptions = new CaseInsensitiveMap<unknown>(306 Object.entries(options).filter(([, v]) => v != null)307 );308 309 // Validate options that can only be provided by one of uri or object310 311 if (urlOptions.has('serverApi')) {312 throw new MongoParseError(313 'URI cannot contain `serverApi`, it can only be passed to the client'314 );315 }316 317 const uriMechanismProperties = urlOptions.get('authMechanismProperties');318 if (uriMechanismProperties) {319 for (const property of uriMechanismProperties) {320 if (/(^|,)ALLOWED_HOSTS:/.test(property as string)) {321 throw new MongoParseError(322 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.'323 );324 }325 }326 }327 328 if (objectOptions.has('loadBalanced')) {329 throw new MongoParseError('loadBalanced is only a valid option in the URI');330 }331 332 // All option collection333 334 const allProvidedOptions = new CaseInsensitiveMap<unknown[]>();335 336 const allProvidedKeys = new Set<string>([...urlOptions.keys(), ...objectOptions.keys()]);337 338 for (const key of allProvidedKeys) {339 const values = [];340 const objectOptionValue = objectOptions.get(key);341 if (objectOptionValue != null) {342 values.push(objectOptionValue);343 }344 345 const urlValues = urlOptions.get(key) ?? [];346 values.push(...urlValues);347 allProvidedOptions.set(key, values);348 }349 350 if (allProvidedOptions.has('tls') || allProvidedOptions.has('ssl')) {351 const tlsAndSslOpts = (allProvidedOptions.get('tls') || [])352 .concat(allProvidedOptions.get('ssl') || [])353 .map(getBoolean.bind(null, 'tls/ssl'));354 if (new Set(tlsAndSslOpts).size !== 1) {355 throw new MongoParseError('All values of tls/ssl must be the same.');356 }357 }358 359 checkTLSOptions(allProvidedOptions);360 361 const unsupportedOptions = setDifference(362 allProvidedKeys,363 Array.from(Object.keys(OPTIONS)).map(s => s.toLowerCase())364 );365 if (unsupportedOptions.size !== 0) {366 const optionWord = unsupportedOptions.size > 1 ? 'options' : 'option';367 const isOrAre = unsupportedOptions.size > 1 ? 'are' : 'is';368 throw new MongoParseError(369 `${optionWord} ${Array.from(unsupportedOptions).join(', ')} ${isOrAre} not supported`370 );371 }372 373 // Option parsing and setting374 375 for (const [key, descriptor] of Object.entries(OPTIONS)) {376 const values = allProvidedOptions.get(key);377 if (!values || values.length === 0) {378 if (DEFAULT_OPTIONS.has(key)) {379 setOption(mongoOptions, key, descriptor, [DEFAULT_OPTIONS.get(key)]);380 }381 } else {382 const { deprecated } = descriptor;383 if (deprecated) {384 const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : '';385 emitWarning(`${key} is a deprecated option${deprecatedMsg}`);386 }387 388 setOption(mongoOptions, key, descriptor, values);389 }390 }391 392 if (mongoOptions.credentials) {393 const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI;394 const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509;395 const isAws = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_AWS;396 const isOidc = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_OIDC;397 if (398 (isGssapi || isX509) &&399 allProvidedOptions.has('authSource') &&400 mongoOptions.credentials.source !== '$external'401 ) {402 // If authSource was explicitly given and its incorrect, we error403 throw new MongoParseError(404 `authMechanism ${mongoOptions.credentials.mechanism} requires an authSource of '$external'`405 );406 }407 408 if (409 !(isGssapi || isX509 || isAws || isOidc) &&410 mongoOptions.dbName &&411 !allProvidedOptions.has('authSource')412 ) {413 // inherit the dbName unless GSSAPI or X509, then silently ignore dbName414 // and there was no specific authSource given415 mongoOptions.credentials = MongoCredentials.merge(mongoOptions.credentials, {416 source: mongoOptions.dbName417 });418 }419 420 if (isAws && mongoOptions.credentials.username && !mongoOptions.credentials.password) {421 throw new MongoMissingCredentialsError(422 `When using ${mongoOptions.credentials.mechanism} password must be set when a username is specified`423 );424 }425 426 mongoOptions.credentials.validate();427 428 // Check if the only auth related option provided was authSource, if so we can remove credentials429 if (430 mongoOptions.credentials.password === '' &&431 mongoOptions.credentials.username === '' &&432 mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_DEFAULT &&433 Object.keys(mongoOptions.credentials.mechanismProperties).length === 0434 ) {435 delete mongoOptions.credentials;436 }437 }438 439 if (!mongoOptions.dbName) {440 // dbName default is applied here because of the credential validation above441 mongoOptions.dbName = 'test';442 }443 444 validateLoadBalancedOptions(hosts, mongoOptions, isSRV);445 446 if (mongoClient && mongoOptions.autoEncryption) {447 Encrypter.checkForMongoCrypt();448 mongoOptions.encrypter = new Encrypter(mongoClient, uri, options);449 mongoOptions.autoEncrypter = mongoOptions.encrypter.autoEncrypter;450 }451 452 // Potential SRV Overrides and SRV connection string validations453 454 mongoOptions.userSpecifiedAuthSource =455 objectOptions.has('authSource') || urlOptions.has('authSource');456 mongoOptions.userSpecifiedReplicaSet =457 objectOptions.has('replicaSet') || urlOptions.has('replicaSet');458 459 if (isSRV) {460 // SRV Record is resolved upon connecting461 mongoOptions.srvHost = hosts[0];462 463 if (mongoOptions.directConnection) {464 throw new MongoAPIError('SRV URI does not support directConnection');465 }466 467 if (mongoOptions.srvMaxHosts > 0 && typeof mongoOptions.replicaSet === 'string') {468 throw new MongoParseError('Cannot use srvMaxHosts option with replicaSet');469 }470 471 // SRV turns on TLS by default, but users can override and turn it off472 const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');473 const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');474 if (noUserSpecifiedTLS && noUserSpecifiedSSL) {475 mongoOptions.tls = true;476 }477 } else {478 const userSpecifiedSrvOptions =479 urlOptions.has('srvMaxHosts') ||480 objectOptions.has('srvMaxHosts') ||481 urlOptions.has('srvServiceName') ||482 objectOptions.has('srvServiceName');483 484 if (userSpecifiedSrvOptions) {485 throw new MongoParseError(486 'Cannot use srvMaxHosts or srvServiceName with a non-srv connection string'487 );488 }489 }490 491 if (mongoOptions.directConnection && mongoOptions.hosts.length !== 1) {492 throw new MongoParseError('directConnection option requires exactly one host');493 }494 495 if (496 !mongoOptions.proxyHost &&497 (mongoOptions.proxyPort || mongoOptions.proxyUsername || mongoOptions.proxyPassword)498 ) {499 throw new MongoParseError('Must specify proxyHost if other proxy options are passed');500 }501 502 if (503 (mongoOptions.proxyUsername && !mongoOptions.proxyPassword) ||504 (!mongoOptions.proxyUsername && mongoOptions.proxyPassword)505 ) {506 throw new MongoParseError('Can only specify both of proxy username/password or neither');507 }508 509 const proxyOptions = ['proxyHost', 'proxyPort', 'proxyUsername', 'proxyPassword'].map(510 key => urlOptions.get(key) ?? []511 );512 513 if (proxyOptions.some(options => options.length > 1)) {514 throw new MongoParseError(515 'Proxy options cannot be specified multiple times in the connection string'516 );517 }518 519 mongoOptions.mongoLoggerOptions = MongoLogger.resolveOptions(520 {521 MONGODB_LOG_COMMAND: process.env.MONGODB_LOG_COMMAND,522 MONGODB_LOG_TOPOLOGY: process.env.MONGODB_LOG_TOPOLOGY,523 MONGODB_LOG_SERVER_SELECTION: process.env.MONGODB_LOG_SERVER_SELECTION,524 MONGODB_LOG_CONNECTION: process.env.MONGODB_LOG_CONNECTION,525 MONGODB_LOG_CLIENT: process.env.MONGODB_LOG_CLIENT,526 MONGODB_LOG_ALL: process.env.MONGODB_LOG_ALL,527 MONGODB_LOG_MAX_DOCUMENT_LENGTH: process.env.MONGODB_LOG_MAX_DOCUMENT_LENGTH,528 MONGODB_LOG_PATH: process.env.MONGODB_LOG_PATH529 },530 {531 mongodbLogPath: mongoOptions.mongodbLogPath,532 mongodbLogComponentSeverities: mongoOptions.mongodbLogComponentSeverities,533 mongodbLogMaxDocumentLength: mongoOptions.mongodbLogMaxDocumentLength534 }535 );536 537 return mongoOptions;538}539 540/**541 * #### Throws if LB mode is true:542 * - hosts contains more than one host543 * - there is a replicaSet name set544 * - directConnection is set545 * - if srvMaxHosts is used when an srv connection string is passed in546 *547 * @throws MongoParseError548 */549function validateLoadBalancedOptions(550 hosts: HostAddress[] | string[],551 mongoOptions: MongoOptions,552 isSrv: boolean553): void {554 if (mongoOptions.loadBalanced) {555 if (hosts.length > 1) {556 throw new MongoParseError(LB_SINGLE_HOST_ERROR);557 }558 if (mongoOptions.replicaSet) {559 throw new MongoParseError(LB_REPLICA_SET_ERROR);560 }561 if (mongoOptions.directConnection) {562 throw new MongoParseError(LB_DIRECT_CONNECTION_ERROR);563 }564 565 if (isSrv && mongoOptions.srvMaxHosts > 0) {566 throw new MongoParseError('Cannot limit srv hosts with loadBalanced enabled');567 }568 }569 return;570}571 572function setOption(573 mongoOptions: any,574 key: string,575 descriptor: OptionDescriptor,576 values: unknown[]577) {578 const { target, type, transform } = descriptor;579 const name = target ?? key;580 581 switch (type) {582 case 'boolean':583 mongoOptions[name] = getBoolean(name, values[0]);584 break;585 case 'int':586 mongoOptions[name] = getIntFromOptions(name, values[0]);587 break;588 case 'uint':589 mongoOptions[name] = getUIntFromOptions(name, values[0]);590 break;591 case 'string':592 if (values[0] == null) {593 break;594 }595 // The value should always be a string here, but since the array is typed as unknown596 // there still needs to be an explicit cast.597 // eslint-disable-next-line @typescript-eslint/no-base-to-string598 mongoOptions[name] = String(values[0]);599 break;600 case 'record':601 if (!isRecord(values[0])) {602 throw new MongoParseError(`${name} must be an object`);603 }604 mongoOptions[name] = values[0];605 break;606 case 'any':607 mongoOptions[name] = values[0];608 break;609 default: {610 if (!transform) {611 throw new MongoParseError('Descriptors missing a type must define a transform');612 }613 const transformValue = transform({ name, options: mongoOptions, values });614 mongoOptions[name] = transformValue;615 break;616 }617 }618}619 620interface OptionDescriptor {621 target?: string;622 type?: 'boolean' | 'int' | 'uint' | 'record' | 'string' | 'any';623 default?: any;624 625 deprecated?: boolean | string;626 /**627 * @param name - the original option name628 * @param options - the options so far for resolution629 * @param values - the possible values in precedence order630 */631 transform?: (args: { name: string; options: MongoOptions; values: unknown[] }) => unknown;632}633 634export const OPTIONS = {635 appName: {636 type: 'string'637 },638 auth: {639 target: 'credentials',640 transform({ name, options, values: [value] }): MongoCredentials {641 if (!isRecord(value, ['username', 'password'] as const)) {642 throw new MongoParseError(643 `${name} must be an object with 'username' and 'password' properties`644 );645 }646 return MongoCredentials.merge(options.credentials, {647 username: value.username,648 password: value.password649 });650 }651 },652 authMechanism: {653 target: 'credentials',654 transform({ options, values: [value] }): MongoCredentials {655 const mechanisms = Object.values(AuthMechanism);656 const [mechanism] = mechanisms.filter(m => m.match(RegExp(String.raw`\b${value}\b`, 'i')));657 if (!mechanism) {658 throw new MongoParseError(`authMechanism one of ${mechanisms}, got ${value}`);659 }660 let source = options.credentials?.source;661 if (662 mechanism === AuthMechanism.MONGODB_PLAIN ||663 AUTH_MECHS_AUTH_SRC_EXTERNAL.has(mechanism)664 ) {665 // some mechanisms have '$external' as the Auth Source666 source = '$external';667 }668 669 let password = options.credentials?.password;670 if (mechanism === AuthMechanism.MONGODB_X509 && password === '') {671 password = undefined;672 }673 return MongoCredentials.merge(options.credentials, {674 mechanism,675 source,676 password677 });678 }679 },680 // Note that if the authMechanismProperties contain a TOKEN_RESOURCE that has a681 // comma in it, it MUST be supplied as a MongoClient option instead of in the682 // connection string.683 authMechanismProperties: {684 target: 'credentials',685 transform({ options, values }): MongoCredentials {686 // We can have a combination of options passed in the URI and options passed687 // as an object to the MongoClient. So we must transform the string options688 // as well as merge them together with a potentially provided object.689 let mechanismProperties = Object.create(null);690 691 for (const optionValue of values) {692 if (typeof optionValue === 'string') {693 for (const [key, value] of entriesFromString(optionValue)) {694 try {695 mechanismProperties[key] = getBoolean(key, value);696 } catch {697 mechanismProperties[key] = value;698 }699 }700 } else {701 if (!isRecord(optionValue)) {702 throw new MongoParseError('AuthMechanismProperties must be an object');703 }704 mechanismProperties = { ...optionValue };705 }706 }707 return MongoCredentials.merge(options.credentials, {708 mechanismProperties709 });710 }711 },712 authSource: {713 target: 'credentials',714 transform({ options, values: [value] }): MongoCredentials {715 const source = String(value);716 return MongoCredentials.merge(options.credentials, { source });717 }718 },719 autoEncryption: {720 type: 'record'721 },722 autoSelectFamily: {723 type: 'boolean',724 default: true725 },726 autoSelectFamilyAttemptTimeout: {727 type: 'uint'728 },729 bsonRegExp: {730 type: 'boolean'731 },732 serverApi: {733 target: 'serverApi',734 transform({ values: [version] }): ServerApi {735 const serverApiToValidate =736 typeof version === 'string' ? ({ version } as ServerApi) : (version as ServerApi);737 const versionToValidate = serverApiToValidate && serverApiToValidate.version;738 if (!versionToValidate) {739 throw new MongoParseError(740 `Invalid \`serverApi\` property; must specify a version from the following enum: ["${Object.values(741 ServerApiVersion742 ).join('", "')}"]`743 );744 }745 if (!Object.values(ServerApiVersion).some(v => v === versionToValidate)) {746 throw new MongoParseError(747 `Invalid server API version=${versionToValidate}; must be in the following enum: ["${Object.values(748 ServerApiVersion749 ).join('", "')}"]`750 );751 }752 return serverApiToValidate;753 }754 },755 checkKeys: {756 type: 'boolean'757 },758 compressors: {759 default: 'none',760 target: 'compressors',761 transform({ values }) {762 const compressionList = new Set();763 for (const compVal of values as (CompressorName[] | string)[]) {764 const compValArray = typeof compVal === 'string' ? compVal.split(',') : compVal;765 if (!Array.isArray(compValArray)) {766 throw new MongoInvalidArgumentError(767 'compressors must be an array or a comma-delimited list of strings'768 );769 }770 for (const c of compValArray) {771 if (Object.keys(Compressor).includes(String(c))) {772 compressionList.add(String(c));773 } else {774 throw new MongoInvalidArgumentError(775 `${c} is not a valid compression mechanism. Must be one of: ${Object.keys(776 Compressor777 )}.`778 );779 }780 }781 }782 return [...compressionList];783 }784 },785 connectTimeoutMS: {786 default: 30000,787 type: 'uint'788 },789 dbName: {790 type: 'string'791 },792 directConnection: {793 default: false,794 type: 'boolean'795 },796 driverInfo: {797 default: {},798 type: 'record'799 },800 enableUtf8Validation: { type: 'boolean', default: true },801 family: {802 transform({ name, values: [value] }): 4 | 6 {803 const transformValue = getIntFromOptions(name, value);804 if (transformValue === 4 || transformValue === 6) {805 return transformValue;806 }807 throw new MongoParseError(`Option 'family' must be 4 or 6 got ${transformValue}.`);808 }809 },810 fieldsAsRaw: {811 type: 'record'812 },813 forceServerObjectId: {814 default: false,815 type: 'boolean'816 },817 fsync: {818 deprecated: 'Please use journal instead',819 target: 'writeConcern',820 transform({ name, options, values: [value] }): WriteConcern {821 const wc = WriteConcern.fromOptions({822 writeConcern: {823 ...options.writeConcern,824 fsync: getBoolean(name, value)825 }826 });827 if (!wc) throw new MongoParseError(`Unable to make a writeConcern from fsync=${value}`);828 return wc;829 }830 } as OptionDescriptor,831 heartbeatFrequencyMS: {832 default: 10000,833 type: 'uint'834 },835 ignoreUndefined: {836 type: 'boolean'837 },838 j: {839 deprecated: 'Please use journal instead',840 target: 'writeConcern',841 transform({ name, options, values: [value] }): WriteConcern {842 const wc = WriteConcern.fromOptions({843 writeConcern: {844 ...options.writeConcern,845 journal: getBoolean(name, value)846 }847 });848 if (!wc) throw new MongoParseError(`Unable to make a writeConcern from journal=${value}`);849 return wc;850 }851 } as OptionDescriptor,852 journal: {853 target: 'writeConcern',854 transform({ name, options, values: [value] }): WriteConcern {855 const wc = WriteConcern.fromOptions({856 writeConcern: {857 ...options.writeConcern,858 journal: getBoolean(name, value)859 }860 });861 if (!wc) throw new MongoParseError(`Unable to make a writeConcern from journal=${value}`);862 return wc;863 }864 },865 loadBalanced: {866 default: false,867 type: 'boolean'868 },869 localThresholdMS: {870 default: 15,871 type: 'uint'872 },873 maxConnecting: {874 default: 2,875 transform({ name, values: [value] }): number {876 const maxConnecting = getUIntFromOptions(name, value);877 if (maxConnecting === 0) {878 throw new MongoInvalidArgumentError('maxConnecting must be > 0 if specified');879 }880 return maxConnecting;881 }882 },883 maxIdleTimeMS: {884 default: 0,885 type: 'uint'886 },887 maxPoolSize: {888 default: 100,889 type: 'uint'890 },891 maxStalenessSeconds: {892 target: 'readPreference',893 transform({ name, options, values: [value] }) {894 const maxStalenessSeconds = getUIntFromOptions(name, value);895 if (options.readPreference) {896 return ReadPreference.fromOptions({897 readPreference: { ...options.readPreference, maxStalenessSeconds }898 });899 } else {900 return new ReadPreference('secondary', undefined, { maxStalenessSeconds });901 }902 }903 },904 minInternalBufferSize: {905 type: 'uint'906 },907 minPoolSize: {908 default: 0,909 type: 'uint'910 },911 minHeartbeatFrequencyMS: {912 default: 500,913 type: 'uint'914 },915 monitorCommands: {916 default: false,917 type: 'boolean'918 },919 name: {920 target: 'driverInfo',921 transform({ values: [value], options }) {922 return { ...options.driverInfo, name: String(value) };923 }924 } as OptionDescriptor,925 noDelay: {926 default: true,927 type: 'boolean'928 },929 pkFactory: {930 default: DEFAULT_PK_FACTORY,931 transform({ values: [value] }): PkFactory {932 if (isRecord(value, ['createPk'] as const) && typeof value.createPk === 'function') {933 return value as PkFactory;934 }935 throw new MongoParseError(936 `Option pkFactory must be an object with a createPk function, got ${value}`937 );938 }939 },940 promoteBuffers: {941 type: 'boolean'942 },943 promoteLongs: {944 type: 'boolean'945 },946 promoteValues: {947 type: 'boolean'948 },949 useBigInt64: {950 type: 'boolean'951 },952 proxyHost: {953 type: 'string'954 },955 proxyPassword: {956 type: 'string'957 },958 proxyPort: {959 type: 'uint'960 },961 proxyUsername: {962 type: 'string'963 },964 raw: {965 default: false,966 type: 'boolean'967 },968 readConcern: {969 transform({ values: [value], options }) {970 if (value instanceof ReadConcern || isRecord(value, ['level'] as const)) {971 return ReadConcern.fromOptions({ ...options.readConcern, ...value } as any);972 }973 throw new MongoParseError(`ReadConcern must be an object, got ${JSON.stringify(value)}`);974 }975 },976 readConcernLevel: {977 target: 'readConcern',978 transform({ values: [level], options }) {979 return ReadConcern.fromOptions({980 ...options.readConcern,981 level: level as ReadConcernLevel982 });983 }984 },985 readPreference: {986 default: ReadPreference.primary,987 transform({ values: [value], options }) {988 if (value instanceof ReadPreference) {989 return ReadPreference.fromOptions({990 readPreference: { ...options.readPreference, ...value },991 ...value992 } as any);993 }994 if (isRecord(value, ['mode'] as const)) {995 const rp = ReadPreference.fromOptions({996 readPreference: { ...options.readPreference, ...value },997 ...value998 } as any);999 if (rp) return rp;1000 else throw new MongoParseError(`Cannot make read preference from ${JSON.stringify(value)}`);1001 }1002 if (typeof value === 'string') {1003 const rpOpts = {1004 hedge: options.readPreference?.hedge,1005 maxStalenessSeconds: options.readPreference?.maxStalenessSeconds1006 };1007 return new ReadPreference(1008 value as ReadPreferenceMode,1009 options.readPreference?.tags,1010 rpOpts1011 );1012 }1013 throw new MongoParseError(`Unknown ReadPreference value: ${value}`);1014 }1015 },1016 readPreferenceTags: {1017 target: 'readPreference',1018 transform({1019 values,1020 options1021 }: {1022 values: Array<string | Record<string, string>[]>;1023 options: MongoClientOptions;1024 }) {1025 const tags: Array<string | Record<string, string>> = Array.isArray(values[0])1026 ? values[0]1027 : (values as Array<string>);1028 const readPreferenceTags = [];1029 for (const tag of tags) {1030 const readPreferenceTag: TagSet = Object.create(null);1031 if (typeof tag === 'string') {1032 for (const [k, v] of entriesFromString(tag)) {1033 readPreferenceTag[k] = v;1034 }1035 }1036 if (isRecord(tag)) {1037 for (const [k, v] of Object.entries(tag)) {1038 readPreferenceTag[k] = v;1039 }1040 }1041 readPreferenceTags.push(readPreferenceTag);1042 }1043 return ReadPreference.fromOptions({1044 readPreference: options.readPreference,1045 readPreferenceTags1046 });1047 }1048 },1049 replicaSet: {1050 type: 'string'1051 },1052 retryReads: {1053 default: true,1054 type: 'boolean'1055 },1056 retryWrites: {1057 default: true,1058 type: 'boolean'1059 },1060 serializeFunctions: {1061 type: 'boolean'1062 },1063 serverMonitoringMode: {1064 default: 'auto',1065 transform({ values: [value] }) {1066 if (!Object.values(ServerMonitoringMode).includes(value as any)) {1067 throw new MongoParseError(1068 'serverMonitoringMode must be one of `auto`, `poll`, or `stream`'1069 );1070 }1071 return value;1072 }1073 },1074 serverSelectionTimeoutMS: {1075 default: 30000,1076 type: 'uint'1077 },1078 servername: {1079 type: 'string'1080 },1081 socketTimeoutMS: {1082 // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead',1083 default: 0,1084 type: 'uint'1085 },1086 srvMaxHosts: {1087 type: 'uint',1088 default: 01089 },1090 srvServiceName: {1091 type: 'string',1092 default: 'mongodb'1093 },1094 ssl: {1095 target: 'tls',1096 type: 'boolean'1097 },1098 timeoutMS: {1099 type: 'uint'1100 },1101 tls: {1102 type: 'boolean'1103 },1104 tlsAllowInvalidCertificates: {1105 target: 'rejectUnauthorized',1106 transform({ name, values: [value] }) {1107 // allowInvalidCertificates is the inverse of rejectUnauthorized1108 return !getBoolean(name, value);1109 }1110 },1111 tlsAllowInvalidHostnames: {1112 target: 'checkServerIdentity',1113 transform({ name, values: [value] }) {1114 // tlsAllowInvalidHostnames means setting the checkServerIdentity function to a noop1115 return getBoolean(name, value) ? () => undefined : undefined;1116 }1117 },1118 tlsCAFile: {1119 type: 'string'1120 },1121 tlsCRLFile: {1122 type: 'string'1123 },1124 tlsCertificateKeyFile: {1125 type: 'string'1126 },1127 tlsCertificateKeyFilePassword: {1128 target: 'passphrase',1129 type: 'any'1130 },1131 tlsInsecure: {1132 transform({ name, options, values: [value] }) {1133 const tlsInsecure = getBoolean(name, value);1134 if (tlsInsecure) {1135 options.checkServerIdentity = () => undefined;1136 options.rejectUnauthorized = false;1137 } else {1138 options.checkServerIdentity = options.tlsAllowInvalidHostnames1139 ? () => undefined1140 : undefined;1141 options.rejectUnauthorized = options.tlsAllowInvalidCertificates ? false : true;1142 }1143 return tlsInsecure;1144 }1145 },1146 w: {1147 target: 'writeConcern',1148 transform({ values: [value], options }) {1149 return WriteConcern.fromOptions({ writeConcern: { ...options.writeConcern, w: value as W } });1150 }1151 },1152 waitQueueTimeoutMS: {1153 // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead',1154 default: 0,1155 type: 'uint'1156 },1157 writeConcern: {1158 target: 'writeConcern',1159 transform({ values: [value], options }) {1160 if (isRecord(value) || value instanceof WriteConcern) {1161 return WriteConcern.fromOptions({1162 writeConcern: {1163 ...options.writeConcern,1164 ...value1165 }1166 });1167 } else if (value === 'majority' || typeof value === 'number') {1168 return WriteConcern.fromOptions({1169 writeConcern: {1170 ...options.writeConcern,1171 w: value1172 }1173 });1174 }1175 1176 throw new MongoParseError(`Invalid WriteConcern cannot parse: ${JSON.stringify(value)}`);1177 }1178 },1179 wtimeout: {1180 deprecated: 'Please use wtimeoutMS instead',1181 target: 'writeConcern',1182 transform({ values: [value], options }) {1183 const wc = WriteConcern.fromOptions({1184 writeConcern: {1185 ...options.writeConcern,1186 wtimeout: getUIntFromOptions('wtimeout', value)1187 }1188 });1189 if (wc) return wc;1190 throw new MongoParseError(`Cannot make WriteConcern from wtimeout`);1191 }1192 } as OptionDescriptor,1193 wtimeoutMS: {1194 target: 'writeConcern',1195 transform({ values: [value], options }) {1196 const wc = WriteConcern.fromOptions({1197 writeConcern: {1198 ...options.writeConcern,1199 wtimeoutMS: getUIntFromOptions('wtimeoutMS', value)1200 }