"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDriverServiceProvider = void 0;
const service_provider_core_1 = require("@mongosh/service-provider-core");
const errors_1 = require("@mongosh/errors");
const mongodb_connection_string_url_1 = require("mongodb-connection-string-url");
const events_1 = require("events");
const util_1 = require("util");
const driver = __importStar(require("mongodb"));
const mongodb_1 = require("mongodb");
const devtools_connect_1 = require("@mongodb-js/devtools-connect");
const mongodb_build_info_1 = require("mongodb-build-info");
const bsonlib = () => {
    const { Binary, Code, DBRef, Double, Int32, Long, MinKey, MaxKey, ObjectId, Timestamp, Decimal128, BSONSymbol, BSONRegExp, UUID, BSON, } = driver;
    return {
        Binary,
        Code,
        DBRef,
        Double,
        Int32,
        Long,
        MinKey,
        MaxKey,
        ObjectId,
        Timestamp,
        Decimal128,
        BSONSymbol,
        calculateObjectSize: BSON.calculateObjectSize,
        EJSON: BSON.EJSON,
        UUID,
        BSONRegExp,
    };
};
const DEFAULT_DRIVER_OPTIONS = Object.freeze({
    __skipPingOnConnect: false,
});
const DEFAULT_BASE_OPTIONS = Object.freeze({
    serializeFunctions: true,
    promoteLongs: false,
});
function normalizeEndpointAndAuthConfiguration(uri, opts) {
    const search = uri.typedSearchParams();
    const authMechProps = new mongodb_connection_string_url_1.CommaAndColonSeparatedRecord(search.get('authMechanismProperties'));
    return [
        uri.protocol,
        uri.hosts,
        opts.auth?.username ?? uri.username,
        opts.auth?.password ?? uri.password,
        opts.authMechanism ?? search.get('authMechanism'),
        opts.authSource ?? search.get('authSource'),
        { ...Object.fromEntries(authMechProps), ...opts.authMechanismProperties },
    ];
}
class NodeDriverServiceProvider extends service_provider_core_1.ServiceProviderCore {
    static async connect(uri, driverOptions, cliOptions = {}, bus = new events_1.EventEmitter()) {
        const connectionString = new mongodb_connection_string_url_1.ConnectionString(uri || 'mongodb://nodb/');
        const clientOptions = this.processDriverOptions(null, connectionString, driverOptions);
        if (process.env.MONGOSH_TEST_FORCE_API_STRICT) {
            clientOptions.serverApi = {
                version: typeof clientOptions.serverApi === 'string'
                    ? clientOptions.serverApi
                    : clientOptions.serverApi?.version ?? '1',
                strict: true,
                deprecationErrors: true,
            };
        }
        let client;
        let state;
        let lastSeenTopology;
        if (cliOptions.nodb) {
            const clientOptionsCopy = {
                ...clientOptions,
            };
            delete clientOptionsCopy.productName;
            delete clientOptionsCopy.productDocsLink;
            delete clientOptionsCopy.oidc;
            delete clientOptionsCopy.parentHandle;
            delete clientOptionsCopy.parentState;
            delete clientOptionsCopy.proxy;
            delete clientOptionsCopy.applyProxyToOIDC;
            client = new mongodb_1.MongoClient(connectionString.toString(), clientOptionsCopy);
        }
        else {
            ({ client, state, lastSeenTopology } = await this.connectMongoClient(connectionString.toString(), clientOptions, bus));
        }
        clientOptions.parentState = state;
        return new this(client, bus, clientOptions, connectionString, lastSeenTopology);
    }
    constructor(mongoClient, bus, clientOptions, uri, lastSeenTopology) {
        super(bsonlib());
        this.bus = bus;
        this.mongoClient = mongoClient;
        this.uri = uri;
        this._lastSeenTopology = lastSeenTopology;
        this.platform = 'CLI';
        try {
            this.initialDb = mongoClient.options.dbName || service_provider_core_1.DEFAULT_DB;
        }
        catch (err) {
            this.initialDb = service_provider_core_1.DEFAULT_DB;
        }
        this.currentClientOptions = clientOptions;
        this.baseCmdOptions = { ...DEFAULT_BASE_OPTIONS };
        this.dbcache = new WeakMap();
        this.mongoClient.on?.('topologyDescriptionChanged', (evt) => {
            this._lastSeenTopology = evt.newDescription;
        });
    }
    static getVersionInformation() {
        return {
            nodeDriverVersion: (() => {
                try {
                    return require('mongodb/package.json').version;
                }
                catch {
                    return undefined;
                }
            })(),
            libmongocryptVersion: (() => {
                try {
                    return mongodb_1.ClientEncryption.libmongocryptVersion;
                }
                catch {
                    return undefined;
                }
            })(),
            libmongocryptNodeBindingsVersion: (() => {
                try {
                    return require('mongodb-client-encryption/package.json').version;
                }
                catch {
                    return undefined;
                }
            })(),
            kerberosVersion: (() => {
                try {
                    return require('kerberos/package.json').version;
                }
                catch {
                    return undefined;
                }
            })(),
        };
    }
    static maybeThrowBetterMissingOptionalDependencyError(err) {
        if (err.message.includes('kerberos')) {
            try {
                require('kerberos');
            }
            catch (cause) {
                if (typeof cause === 'object' &&
                    cause &&
                    'message' in cause &&
                    typeof cause.message === 'string') {
                    throw new Error(`Could not load kerberos package: ${cause.message}`, {
                        cause,
                    });
                }
            }
        }
        if (err.message.includes('mongodb-client-encryption')) {
            try {
                require('mongodb-client-encryption');
            }
            catch (cause) {
                if (typeof cause === 'object' &&
                    cause &&
                    'message' in cause &&
                    typeof cause.message === 'string') {
                    const extra = 'boxednode' in process
                        ? ''
                        : '\n(If you are installing mongosh through homebrew or npm, consider downlading mongosh from https://www.mongodb.com/try/download/shell instead)';
                    throw new Error(`Could not load mongodb-client-encryption package: ${cause.message}${extra}`, { cause });
                }
            }
        }
        throw err;
    }
    static async connectMongoClient(connectionString, clientOptions, bus) {
        let lastSeenTopology;
        class MongoshMongoClient extends mongodb_1.MongoClient {
            constructor(url, options) {
                super(url, options);
                this.on('topologyDescriptionChanged', (evt) => {
                    lastSeenTopology = evt.newDescription;
                });
            }
        }
        try {
            const result = await (0, devtools_connect_1.connectMongoClient)(connectionString.toString(), clientOptions, bus, MongoshMongoClient);
            return { ...result, lastSeenTopology };
        }
        catch (err) {
            if (typeof err === 'object' &&
                err &&
                'name' in err &&
                err.name === 'MongoMissingDependencyError') {
                this.maybeThrowBetterMissingOptionalDependencyError(err);
            }
            throw err;
        }
    }
    async getNewConnection(uri, options = {}) {
        const connectionString = new mongodb_connection_string_url_1.ConnectionString(uri);
        const clientOptions = this.processDriverOptions(connectionString, options);
        const { client, state, lastSeenTopology } = await NodeDriverServiceProvider.connectMongoClient(connectionString.toString(), clientOptions, this.bus);
        clientOptions.parentState = state;
        return new NodeDriverServiceProvider(client, this.bus, clientOptions, connectionString, lastSeenTopology ?? this._lastSeenTopology);
    }
    _getHostnameForConnection(topology) {
        return topology?.servers?.values().next().value?.hostAddress.host;
    }
    async getConnectionInfo() {
        const buildInfoPromise = this.runCommandWithCheck('admin', { buildInfo: 1 }, this.baseCmdOptions).catch(() => ({}));
        const [buildInfo, atlasVersion = null, fcv = null, atlascliInfo, serverName,] = await Promise.all([
            buildInfoPromise,
            this.runCommandWithCheck('admin', { atlasVersion: 1 }, this.baseCmdOptions).catch(() => { }),
            this.runCommandWithCheck('admin', { getParameter: 1, featureCompatibilityVersion: 1 }, this.baseCmdOptions).catch(() => { }),
            this.countDocuments('admin', 'atlascli', {
                managedClusterType: 'atlasCliLocalDevCluster',
            }).catch(() => 0),
            (0, mongodb_build_info_1.identifyServerName)({
                connectionString: this.uri?.toString() ?? '',
                adminCommand: (command) => {
                    if (command.buildInfo) {
                        return buildInfoPromise;
                    }
                    return this.runCommandWithCheck('admin', command, this.baseCmdOptions);
                },
            }),
        ]);
        const resolvedHostname = this._getHostnameForConnection(this._lastSeenTopology);
        const extraConnectionInfo = (0, service_provider_core_1.getConnectExtraInfo)({
            connectionString: this.uri,
            buildInfo,
            atlasVersion,
            resolvedHostname,
            isLocalAtlas: !!atlascliInfo,
            serverName,
        });
        return {
            buildInfo,
            resolvedHostname,
            extraInfo: {
                ...extraConnectionInfo,
                fcv: fcv?.featureCompatibilityVersion?.version,
            },
        };
    }
    async renameCollection(database, oldName, newName, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return await this.db(database, dbOptions).renameCollection(oldName, newName, options);
    }
    db(name, dbOptions = {}) {
        const key = `${name}-${JSON.stringify(dbOptions)}`;
        const dbcache = this.getDBCache();
        const cached = dbcache.get(key);
        if (cached) {
            return cached;
        }
        const db = this.mongoClient.db(name, dbOptions);
        dbcache.set(key, db);
        return db;
    }
    _dbTestWrapper(name, dbOptions) {
        return this.db(name, dbOptions);
    }
    getDBCache() {
        const existing = this.dbcache.get(this.mongoClient);
        if (existing) {
            return existing;
        }
        this.dbcache.set(this.mongoClient, new Map());
        return this.getDBCache();
    }
    hasUnifiedAggregateOptions() {
        return true;
    }
    aggregate(database, collection, pipeline = [], options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .aggregate(pipeline, options);
    }
    aggregateDb(database, pipeline = [], options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        const db = this.db(database, dbOptions);
        return db.aggregate(pipeline, options);
    }
    bulkWrite(database, collection, requests, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .bulkWrite(requests, options);
    }
    clientBulkWrite(models, options = {}) {
        options = { ...this.baseCmdOptions, ...options };
        return this.mongoClient.bulkWrite(models, options);
    }
    async close() {
        this.dbcache.set(this.mongoClient, new Map());
        await this.mongoClient.close();
    }
    async suspend() {
        await this.close();
        return async () => {
            await this.resetConnectionOptions({});
        };
    }
    count(database, collection, query = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .count(query, options);
    }
    countDocuments(database, collection, filter = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .countDocuments(filter, options);
    }
    deleteMany(database, collection, filter = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .deleteMany(filter, options);
    }
    deleteOne(database, collection, filter = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .deleteOne(filter, options);
    }
    distinct(database, collection, fieldName, filter = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .distinct(fieldName, filter, options);
    }
    estimatedDocumentCount(database, collection, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .estimatedDocumentCount(options);
    }
    find(database, collection, filter = {}, options = {}, dbOptions) {
        const findOptions = { ...this.baseCmdOptions, ...options };
        if ('allowPartialResults' in findOptions) {
            findOptions.partial = findOptions.allowPartialResults;
        }
        if ('noCursorTimeout' in findOptions) {
            findOptions.timeout = findOptions.noCursorTimeout;
        }
        return this.db(database, dbOptions)
            .collection(collection)
            .find(filter, findOptions);
    }
    findOneAndDelete(database, collection, filter = {}, options = {}, dbOptions) {
        options = {
            includeResultMetadata: true,
            ...this.baseCmdOptions,
            ...options,
        };
        return this.db(database, dbOptions)
            .collection(collection)
            .findOneAndDelete(filter, options);
    }
    findOneAndReplace(database, collection, filter = {}, replacement = {}, options = {}, dbOptions) {
        const findOneAndReplaceOptions = {
            includeResultMetadata: true,
            ...this.baseCmdOptions,
            ...options,
        };
        return this.db(database, dbOptions).collection(collection).findOneAndReplace(filter, replacement, findOneAndReplaceOptions);
    }
    findOneAndUpdate(database, collection, filter = {}, update = {}, options = {}, dbOptions) {
        const findOneAndUpdateOptions = {
            includeResultMetadata: true,
            ...this.baseCmdOptions,
            ...options,
        };
        return this.db(database, dbOptions)
            .collection(collection)
            .findOneAndUpdate(filter, update, findOneAndUpdateOptions);
    }
    insertMany(database, collection, docs = [], options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .insertMany(docs, options);
    }
    async insertOne(database, collection, doc = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .insertOne(doc, options);
    }
    replaceOne(database, collection, filter = {}, replacement = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .replaceOne(filter, replacement, options);
    }
    runCommand(database, spec = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        const db = this.db(database, dbOptions);
        return db.command(spec, options);
    }
    async runCommandWithCheck(database, spec = {}, options = {}, dbOptions) {
        const result = await this.runCommand(database, spec, options, dbOptions);
        if (result.ok === 0) {
            throw new errors_1.MongoshCommandFailed(JSON.stringify(spec));
        }
        return result;
    }
    runCursorCommand(database, spec = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        const db = this.db(database, dbOptions);
        return db.runCursorCommand(spec, options);
    }
    listDatabases(database, options = {}) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database).admin().listDatabases(options);
    }
    async updateMany(database, collection, filter = {}, update = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return await this.db(database, dbOptions)
            .collection(collection)
            .updateMany(filter, update, options);
    }
    updateOne(database, collection, filter = {}, update = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .updateOne(filter, update, options);
    }
    getTopologyDescription() {
        return this._lastSeenTopology;
    }
    async dropDatabase(db, options = {}, dbOptions = {}) {
        const opts = { ...this.baseCmdOptions, ...options };
        const nativeResult = await this.db(db, dbOptions).dropDatabase(opts);
        const ok = nativeResult ? 1 : 0;
        return {
            ok,
            ...(ok ? { dropped: db } : {}),
        };
    }
    async createIndexes(database, collection, indexSpecs, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return this.db(database, dbOptions)
            .collection(collection)
            .createIndexes(indexSpecs, options);
    }
    async getIndexes(database, collection, options = {}, dbOptions) {
        return await this.db(database, dbOptions)
            .collection(collection)
            .listIndexes({ ...this.baseCmdOptions, ...options })
            .toArray();
    }
    async listCollections(database, filter = {}, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        return await this.db(database, dbOptions)
            .listCollections(filter, options)
            .toArray();
    }
    async dropCollection(database, collection, options = {}, dbOptions) {
        return this.db(database, dbOptions)
            .collection(collection)
            .drop({ ...this.baseCmdOptions, ...options });
    }
    async authenticate(authDoc) {
        const auth = { username: authDoc.user, password: authDoc.pwd };
        await this.resetConnectionOptions({
            auth,
            ...(authDoc.mechanism
                ? { authMechanism: authDoc.mechanism }
                : {}),
            ...(authDoc.authDb ? { authSource: authDoc.authDb } : {}),
        });
        return { ok: 1 };
    }
    async createCollection(dbName, collName, options = {}, dbOptions) {
        options = { ...this.baseCmdOptions, ...options };
        await this.db(dbName, dbOptions).createCollection(collName, options);
        return { ok: 1 };
    }
    async createEncryptedCollection(dbName, collName, options, libmongocrypt) {
        return await libmongocrypt.createEncryptedCollection(this.db(dbName), collName, options);
    }
    async initializeBulkOp(dbName, collName, ordered, options = {}, dbOptions) {
        if (ordered) {
            return this.db(dbName, dbOptions)
                .collection(collName)
                .initializeOrderedBulkOp(options);
        }
        return this.db(dbName, dbOptions)
            .collection(collName)
            .initializeUnorderedBulkOp(options);
    }
    getReadPreference() {
        return this.mongoClient.readPreference;
    }
    getReadConcern() {
        return this.mongoClient.readConcern;
    }
    getWriteConcern() {
        return this.mongoClient.writeConcern;
    }
    readPreferenceFromOptions(options) {
        return mongodb_1.ReadPreference.fromOptions(options);
    }
    async resetConnectionOptions(options) {
        this.bus.emit('mongosh-sp:reset-connection-options');
        this.currentClientOptions = {
            ...this.currentClientOptions,
            ...options,
        };
        const clientOptions = this.processDriverOptions(this.uri, this.currentClientOptions);
        const { client, state, lastSeenTopology } = await NodeDriverServiceProvider.connectMongoClient(this.uri.toString(), clientOptions, this.bus);
        try {
            await this.mongoClient.close();
        }
        catch { }
        this.mongoClient = client;
        this.currentClientOptions.parentState = state;
        if (lastSeenTopology)
            this._lastSeenTopology = lastSeenTopology;
    }
    startSession(options) {
        return this.mongoClient.startSession(options);
    }
    watch(pipeline, options, dbOptions = {}, db, coll) {
        if (db === undefined && coll === undefined) {
            return this.mongoClient.watch(pipeline, options);
        }
        else if (db !== undefined && coll === undefined) {
            return this.db(db, dbOptions).watch(pipeline, options);
        }
        else if (db !== undefined && coll !== undefined) {
            return this.db(db, dbOptions).collection(coll).watch(pipeline, options);
        }
        throw new errors_1.MongoshInternalError('Cannot call watch with defined collection but undefined db');
    }
    getRawClient() {
        return this.mongoClient;
    }
    getURI() {
        return this.uri?.href;
    }
    getFleOptions() {
        return this.currentClientOptions.autoEncryption;
    }
    static processDriverOptions(currentProviderInstance, uri, opts) {
        const processedOptions = { ...DEFAULT_DRIVER_OPTIONS, ...opts };
        if (currentProviderInstance?.currentClientOptions) {
            for (const key of ['productName', 'productDocsLink']) {
                processedOptions[key] =
                    currentProviderInstance.currentClientOptions[key];
            }
            processedOptions.oidc ??= {};
            for (const key of [
                'redirectURI',
                'openBrowser',
                'openBrowserTimeout',
                'notifyDeviceFlow',
                'allowedFlows',
            ]) {
                ((key) => {
                    const value = currentProviderInstance.currentClientOptions.oidc?.[key];
                    if (value) {
                        processedOptions.oidc[key] = value;
                    }
                })(key);
            }
        }
        if (processedOptions.parentState ||
            processedOptions.parentHandle ||
            !currentProviderInstance) {
            return processedOptions;
        }
        const currentOpts = currentProviderInstance.currentClientOptions;
        const currentUri = currentProviderInstance.uri;
        if (currentUri &&
            (0, util_1.isDeepStrictEqual)(normalizeEndpointAndAuthConfiguration(currentUri, currentOpts), normalizeEndpointAndAuthConfiguration(uri, processedOptions))) {
            if (currentOpts.parentState) {
                processedOptions.parentState = currentOpts.parentState;
            }
            else if (currentOpts.parentHandle) {
                processedOptions.parentHandle = currentOpts.parentHandle;
            }
        }
        return processedOptions;
    }
    processDriverOptions(uri, opts) {
        return NodeDriverServiceProvider.processDriverOptions(this, uri, {
            productName: this.currentClientOptions.productName,
            productDocsLink: this.currentClientOptions.productDocsLink,
            ...opts,
        });
    }
    getSearchIndexes(database, collection, indexName, options, dbOptions) {
        const col = this.db(database, dbOptions).collection(collection);
        if (indexName === undefined) {
            return col.listSearchIndexes(options).toArray();
        }
        else {
            return col.listSearchIndexes(indexName, options).toArray();
        }
    }
    createSearchIndexes(database, collection, specs, dbOptions) {
        return this.db(database, dbOptions)
            .collection(collection)
            .createSearchIndexes(specs);
    }
    dropSearchIndex(database, collection, indexName, dbOptions) {
        return this.db(database, dbOptions)
            .collection(collection)
            .dropSearchIndex(indexName);
    }
    updateSearchIndex(database, collection, indexName, definition, dbOptions) {
        return this.db(database, dbOptions)
            .collection(collection)
            .updateSearchIndex(indexName, definition);
    }
    createClientEncryption(options) {
        return new mongodb_1.ClientEncryption(this.mongoClient, options);
    }
}
exports.NodeDriverServiceProvider = NodeDriverServiceProvider;
//# sourceMappingURL=node-driver-service-provider.js.map