UNPKG

quosdolorem

Version:
1,200 lines (1,123 loc) 36.4 kB
import { Auth, AuthMechanismId, MongoClient, ReadPreference, Binary, Code, DBRef, Double, Int32, Long, MinKey, MaxKey, ObjectId, Timestamp, Decimal128, BSONSymbol, ClientMetadata, Topology, ReadPreferenceFromOptions, ReadPreferenceLike, OperationOptions, ServerHeartbeatFailedEvent } from 'mongodb'; import { ServiceProvider, getConnectInfo, ReplPlatform, DEFAULT_DB, ServiceProviderCore, ShellAuthOptions, // Driver types: AggregateOptions, AggregationCursor, AnyBulkWriteOperation, BulkWriteOptions, BulkWriteResult, ClientSessionOptions, CollStatsOptions, Collection, CountDocumentsOptions, CountOptions, CreateCollectionOptions, CreateIndexesOptions, FindCursor, Db, DbOptions, DeleteOptions, DeleteResult, DistinctOptions, Document, DropCollectionOptions, DropDatabaseOptions, EstimatedDocumentCountOptions, FindAndModifyOptions, FindOptions, IndexDescription, InsertManyResult, InsertOneOptions, InsertOneResult, ListCollectionsOptions, ListDatabasesOptions, ListIndexesOptions, MongoClientOptions, ReadConcern, RenameOptions, ReplaceOptions, RunCommandOptions, ClientSession, UpdateOptions, UpdateResult, WriteConcern, ChangeStreamOptions, ChangeStream, bson as BSON, ConnectionString, FLE, AutoEncryptionOptions, isFastFailureConnectionError } from '@mongosh/service-provider-core'; import { MongoshCommandFailed, MongoshInternalError, MongoshRuntimeError } from '@mongosh/errors'; import { ensureMongoNodeNativePatchesAreApplied } from './mongodb-patches'; const bsonlib = { Binary, Code, DBRef, Double, Int32, Long, MinKey, MaxKey, ObjectId, Timestamp, Decimal128, BSONSymbol, Map: BSON.Map, calculateObjectSize: BSON.calculateObjectSize, EJSON: BSON.EJSON }; type DropDatabaseResult = { ok: 0 | 1; dropped?: string; }; type ConnectionInfo = { buildInfo: any; topology: Topology; extraInfo: ExtraConnectionInfo; }; type ExtraConnectionInfo = ReturnType<typeof getConnectInfo>; /** * Default driver options we always use. */ const DEFAULT_DRIVER_OPTIONS: MongoClientOptions = Object.freeze({ }); function processDriverOptions(opts: MongoClientOptions): MongoClientOptions { const ret = { ...DEFAULT_DRIVER_OPTIONS, ...opts }; if (ret.tlsCertificateKeyFile && !ret.tlsCertificateFile) { ret.tlsCertificateFile = ret.tlsCertificateKeyFile; } return ret; } /** * Default driver method options we always use. */ const DEFAULT_BASE_OPTIONS: OperationOptions = Object.freeze({ serializeFunctions: true }); /** * Takes an unconnected MongoClient and connects it, but fails fast for certain * errors. */ async function connectWithFailFast(client: MongoClient): Promise<void> { let failFastErr; const heartbeatFailureListener = ({ failure }: ServerHeartbeatFailedEvent) => { if (isFastFailureConnectionError(failure)) { failFastErr = failure; client.close(); } }; client.addListener('serverHeartbeatFailed', heartbeatFailureListener); try { await client.connect(); } catch (err) { throw failFastErr || err; } finally { client.removeListener('serverHeartbeatFailed', heartbeatFailureListener); } } /** * Connect a MongoClient. If AutoEncryption is requested, first connect without the encryption options and verify that * the connection is to an enterprise cluster. If not, then error, otherwise close the connection and reconnect with the * options the user initially specified. Provide the client class as an additional argument in order to test. * @param uri {String} * @param clientOptions {MongoClientOptions} * @param MClient {MongoClient} */ export async function connectMongoClient(uri: string, clientOptions: MongoClientOptions, MClient = MongoClient): Promise<MongoClient> { if (clientOptions.autoEncryption !== undefined && !clientOptions.autoEncryption.bypassAutoEncryption) { // connect first without autoEncryptionOptions const optionsWithoutFLE = { ...clientOptions }; delete optionsWithoutFLE.autoEncryption; const client = new MClient(uri, optionsWithoutFLE); await connectWithFailFast(client); const buildInfo = await client.db('admin').admin().command({ buildInfo: 1 }); if ( !(buildInfo.modules?.includes('enterprise')) && !(buildInfo.gitVersion?.match(/enterprise/)) ) { await client.close(); throw new MongoshRuntimeError('Automatic encryption is only available with Atlas and MongoDB Enterprise'); } await client.close(); } const client = new MClient(uri, clientOptions); await connectWithFailFast(client); return client; } /** * Encapsulates logic for the service provider for the mongosh CLI. */ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider { /** * Create a new CLI service provider from the provided URI. * * @param {String} uri - The URI. * @param {MongoClientOptions} driverOptions - The options. * @param {Object} cliOptions - Options passed through CLI. Right now only being used for nodb. * * @returns {Promise} The promise with cli service provider. */ static async connect( this: typeof CliServiceProvider, uri: string, driverOptions: MongoClientOptions = {}, cliOptions: { nodb?: boolean } = {} ): Promise<CliServiceProvider> { const connectionString = new ConnectionString(uri || 'mongodb://nodb/'); const clientOptions = processDriverOptions(driverOptions); const mongoClient = !cliOptions.nodb ? await connectMongoClient( connectionString.toString(), clientOptions ) : new MongoClient(connectionString.toString(), clientOptions); return new this(mongoClient, clientOptions, connectionString); } public readonly platform: ReplPlatform; public readonly initialDb: string; public mongoClient: MongoClient; // public for testing private readonly uri?: ConnectionString; private currentClientOptions: MongoClientOptions; private dbcache: WeakMap<MongoClient, Map<string, Db>>; public baseCmdOptions: OperationOptions; // public for testing public fle: FLE | undefined; /** * Instantiate a new CliServiceProvider with the Node driver's connected * MongoClient instance. * * @param {MongoClient} mongoClient - The Node drivers' MongoClient instance. * @param {MongoClientOptions} clientOptions * @param {string} uri - optional URI for telemetry. */ constructor(mongoClient: MongoClient, clientOptions: MongoClientOptions = {}, uri?: ConnectionString) { super(bsonlib); ensureMongoNodeNativePatchesAreApplied(); this.mongoClient = mongoClient; this.uri = uri; this.platform = ReplPlatform.CLI; try { this.initialDb = (mongoClient as any).s.options.dbName || DEFAULT_DB; } catch (err) { this.initialDb = DEFAULT_DB; } this.currentClientOptions = clientOptions; this.baseCmdOptions = { ... DEFAULT_BASE_OPTIONS }; // currently do not have any user-specified connection-wide command options, but I imagine we will eventually this.dbcache = new WeakMap(); try { this.fle = require('mongodb-client-encryption'); } catch { /* not empty */ } } async getNewConnection(uri: string, options: MongoClientOptions = {}): Promise<CliServiceProvider> { const connectionString = new ConnectionString(uri); const clientOptions = processDriverOptions(options); const mongoClient = await connectMongoClient( connectionString.toString(), clientOptions ); return new CliServiceProvider(mongoClient, clientOptions, connectionString); } async getConnectionInfo(): Promise<ConnectionInfo> { // buildInfo try/catch can be removed after MONGOCRYPT-308 let buildInfo; try { buildInfo = await this.runCommandWithCheck('admin', { buildInfo: 1 }, this.baseCmdOptions); } catch (e) { if (e.message.includes('not supported for auto encryption')) { const options = { ...this.currentClientOptions }; delete options.autoEncryption; const unencrypted = await this.getNewConnection( (this.uri as ConnectionString).toString(), options); try { return await unencrypted.getConnectionInfo(); } finally { await unencrypted.close(true); } } } const topology = this.getTopology() as Topology; const { version } = require('../package.json'); const [cmdLineOpts = null, atlasVersion = null] = await Promise.all([ this.runCommandWithCheck('admin', { getCmdLineOpts: 1 }, this.baseCmdOptions).catch(() => {}), this.runCommandWithCheck('admin', { atlasVersion: 1 }, this.baseCmdOptions).catch(() => {}) ]); const extraConnectionInfo = getConnectInfo( this.uri?.toString() ?? '', version, buildInfo, cmdLineOpts, atlasVersion, topology ); return { buildInfo: buildInfo, topology: topology, extraInfo: extraConnectionInfo }; } async renameCollection( database: string, oldName: string, newName: string, options: RenameOptions = {}, dbOptions?: DbOptions): Promise<Collection> { options = { ...this.baseCmdOptions, ...options }; return await this.db(database, dbOptions).renameCollection(oldName, newName, options); } /** * Get the Db object from the client. * * @param {String} name - The database name. * @param dbOptions * * @returns {Db} The database. */ private db(name: string, dbOptions: DbOptions = {}): Db { 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; } /** * Wrapper to make this available for testing. */ _dbTestWrapper(name: string, dbOptions?: DbOptions): Db { return this.db(name, dbOptions); } /** * Return the db cache for the current MongoClient. */ private getDBCache(): Map<string, Db> { const existing = this.dbcache.get(this.mongoClient); if (existing) { return existing; } this.dbcache.set(this.mongoClient, new Map()); return this.getDBCache(); } /** * Run an aggregation pipeline. * * @param {String} database - the db name * @param {String} collection - the collection name * @param pipeline * @param options * allowDiskUse: Optional<Boolean>; * batchSize: Optional<Int32>; * bypassDocumentValidation: Optional<Boolean>; * collation: Optional<Document>; * maxTimeMS: Optional<Int64>; * maxAwaitTimeMS: Optional<Int64>; * comment: Optional<String>; * hint: Optional<(String | Document = {})>; * @param dbOptions * @returns {Cursor} The aggregation cursor. */ aggregate( database: string, collection: string, pipeline: Document[] = [], options: AggregateOptions = {}, dbOptions?: DbOptions): AggregationCursor { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .aggregate(pipeline, options); } /** * @param {String} database - the db name * @param pipeline * @param options * allowDiskUse: Optional<Boolean>; * batchSize: Optional<Int32>; * bypassDocumentValidation: Optional<Boolean>; * collation: Optional<Document>; * maxTimeMS: Optional<Int64>; * maxAwaitTimeMS: Optional<Int64>; * comment: Optional<String>; * hint: Optional<(String | Document = {})>; * @param dbOptions * j: Optional<Boolean> * w: Optional<Int32 | String> * wtimeoutMS: Optional<Int64> * @return {any} */ aggregateDb( database: string, pipeline: Document[] = [], options: AggregateOptions = {}, dbOptions?: DbOptions): AggregationCursor { options = { ...this.baseCmdOptions, ...options }; const db: any = (this.db(database, dbOptions) as any); return db.aggregate(pipeline, options); } /** * @param {String} database - the db name * @param {String} collection - the collection name * @param requests * @param options * ordered: Boolean; * bypassDocumentValidation: Optional<Boolean>; * @param dbOptions * j: Optional<Boolean> * w: Optional<Int32 | String> * wtimeoutMS: Optional<Int64> * readConcern: * level: <String local|majority|linearizable|available> * @return {any} */ bulkWrite( database: string, collection: string, requests: AnyBulkWriteOperation[], options: BulkWriteOptions = {}, dbOptions?: DbOptions): Promise<BulkWriteResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .bulkWrite(requests, options); } /** * Close the connection. * * @param {boolean} force - Whether to force close the connection. */ close(force: boolean): Promise<void> { this.dbcache.set(this.mongoClient, new Map()); return this.mongoClient.close(force); } async suspend(): Promise<() => Promise<void>> { await this.close(true); return async() => { await this.resetConnectionOptions({}); }; } /** * Deprecated count command. * * @param {String} database - the db name * @param {String} collection - the collection name * @param query * @param options * collation: Optional<Document> * hint: Optional<(String | Document = {})>; * limit: Optional<Int64>; * maxTimeMS: Optional<Int64>; * skip: Optional<Int64>; * @param dbOptions * readConcern: * level: <String local|majority|linearizable|available> * @return {Promise<any>} */ count( database: string, collection: string, query: Document = {}, options: CountOptions = {}, dbOptions?: DbOptions): Promise<number> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .count(query, options); } /** * Get an exact document count from the collection. * * @param {String} database - the db name * @param {String} collection - the collection name * @param filter * @param options * hint: Optional<(String | Document = {})>; * limit: Optional<Int64>; * maxTimeMS: Optional<Int64>; * skip: Optional<Int64>; * @param dbOptions * @return {any} */ countDocuments( database: string, collection: string, filter: Document = {}, options: CountDocumentsOptions = {}, dbOptions?: DbOptions): Promise<number> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .countDocuments(filter, options); } /** * Delete multiple documents from the collection. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} options - The delete many options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ deleteMany( database: string, collection: string, filter: Document = {}, options: DeleteOptions = {}, dbOptions?: DbOptions): Promise<DeleteResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .deleteMany(filter, options); } /** * Delete one document from the collection. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} options - The delete one options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ deleteOne( database: string, collection: string, filter: Document = {}, options: DeleteOptions = {}, dbOptions?: DbOptions): Promise<DeleteResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .deleteOne(filter, options); } /** * Get distinct values for the field. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {String} fieldName - The field name. * @param {Object} filter - The filter. * @param {Object} options - The distinct options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Cursor} The cursor. */ distinct( database: string, collection: string, fieldName: string, filter: Document = {}, options: DistinctOptions = {}, dbOptions?: DbOptions): Promise<Document[]> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .distinct(fieldName, filter, options); } /** * Get an estimated document count from the collection. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} options - The count options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ estimatedDocumentCount( database: string, collection: string, options: EstimatedDocumentCountOptions = {}, dbOptions?: DbOptions): Promise<number> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .estimatedDocumentCount(options); } /** * Find documents in the collection. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} options - The find options. * * @param dbOptions * @returns {Cursor} The cursor. */ find( database: string, collection: string, filter: Document = {}, options: FindOptions = {}, dbOptions?: DbOptions): FindCursor { const findOptions: any = { ...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); } /** * Find one document and delete it. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} options - The find options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ findOneAndDelete( database: string, collection: string, filter: Document = {}, options: FindAndModifyOptions = {}, dbOptions?: DbOptions): Promise<Document> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection). findOneAndDelete(filter, options); } /** * Find one document and replace it. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} replacement - The replacement. * @param {Object} options - The find options. * * @param dbOptions * @returns {Promise} The promise of the result. */ findOneAndReplace( database: string, collection: string, filter: Document = {}, replacement: Document = {}, options: FindAndModifyOptions = {}, dbOptions?: DbOptions): Promise<Document> { const findOneAndReplaceOptions: any = { ...this.baseCmdOptions, ...options }; return ( this.db(database, dbOptions).collection(collection) as any ).findOneAndReplace(filter, replacement, findOneAndReplaceOptions); } /** * Find one document and update it. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {(Object|Array)} update - The update. * @param {Object} options - The find options. * * @param dbOptions * @returns {Promise} The promise of the result. */ findOneAndUpdate( database: string, collection: string, filter: Document = {}, update: Document | Document[] = {}, options: FindAndModifyOptions = {}, dbOptions?: DbOptions): Promise<Document> { const findOneAndUpdateOptions = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .findOneAndUpdate( filter, update, findOneAndUpdateOptions ) as any; } /** * Insert many documents into the collection. * * @param {string} database - The database name. * @param {string} collection - The collection name. * @param {Document[]} [docs=[]] - The documents. * @param {Document} [options={}] - options - The insert many options. * @param {DbOptions} [dbOptions] - The database options. * * @returns {Promise<InsertManyResult>} */ insertMany( database: string, collection: string, docs: Document[] = [], options: BulkWriteOptions = {}, dbOptions?: DbOptions): Promise<InsertManyResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .insertMany(docs, options); } /** * Insert one document into the collection. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} doc - The document. * @param {Object} options - The insert one options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ async insertOne( database: string, collection: string, doc: Document = {}, options: InsertOneOptions = {}, dbOptions?: DbOptions): Promise<InsertOneResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .insertOne(doc, options); } /** * Is the collection capped? * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param dbOptions * @returns {Promise} The promise of the result. */ isCapped( database: string, collection: string, dbOptions?: DbOptions): Promise<boolean> { return this.db(database, dbOptions).collection(collection).isCapped(); } /** * Deprecated remove command. * * @param {String} database - The db name. * @param {String} collection - The collection name. * @param {Object} query - The query. * @param {Object} options - The options. * @param dbOptions * @return {Promise} */ remove( database: string, collection: string, query: Document = {}, options: DeleteOptions = {}, dbOptions?: DbOptions): Promise<DeleteResult> { options = { ...this.baseCmdOptions, ...options }; /* NOTE: the 4.x branch of the driver does not define a separate def for remove that doesn't take a callback, and since remove is deprecated it's not worth asking for a change to the driver. So ts-ignore it is. */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this.db(database, dbOptions).collection(collection).remove(query, options); } /** * Replace a document with another. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {Object} replacement - The replacement document for matches. * @param {Object} options - The replace options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ replaceOne( database: string, collection: string, filter: Document = {}, replacement: Document = {}, options: ReplaceOptions = {}, dbOptions?: DbOptions ): Promise<UpdateResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .replaceOne(filter, replacement, options) as Promise<UpdateResult>; // `as UpdateResult` because we know we didn't request .explain() here. } /** * Run a command against the database. * * @param {String} database - The database name. * @param {Object} spec - The command specification. * @param {Object} options - The database options. * @param {Object} dbOptions - The connection-wide database options. * * @returns {Promise} The promise of command results. */ runCommand( database: string, spec: Document = {}, options: RunCommandOptions = {}, dbOptions?: DbOptions ): Promise<Document> { options = { ...this.baseCmdOptions, ...options }; const db = this.db(database, dbOptions); return db.command( spec, options ); } /** * Run a command against the database and check the results for ok: 0. * * @param {String} database - The database name. * @param {Object} spec - The command specification. * @param {Object} options - The database options. * @param {Object} dbOptions - The connection-wide database options. * * @returns {Promise} The promise of command results. */ async runCommandWithCheck( database: string, spec: Document = {}, options: RunCommandOptions = {}, dbOptions?: DbOptions ): Promise<Document> { const result = await this.runCommand(database, spec, options, dbOptions); if (result.ok === 0) { throw new MongoshCommandFailed(JSON.stringify(spec)); } return result as { ok: 1 }; } /** * list databases. * * @param {String} database - The database name. * * @returns {Promise} The promise of command results. */ listDatabases(database: string, options: ListDatabasesOptions = {}): Promise<Document> { options = { ...this.baseCmdOptions, ...options }; return this.db(database).admin().listDatabases(options); } /** * Update many document. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {(Object|Array)} update - The updates. * @param {Object} options - The update options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ async updateMany( database: string, collection: string, filter: Document = {}, update: Document = {}, options: UpdateOptions = {}, dbOptions?: DbOptions): Promise<UpdateResult> { options = { ...this.baseCmdOptions, ...options }; return await this.db(database, dbOptions) .collection(collection) .updateMany(filter, update, options) as Promise<UpdateResult>; // `as UpdateResult` because we know we didn't request .explain() here. } /** * Update a document. * * @param {String} database - The database name. * @param {String} collection - The collection name. * @param {Object} filter - The filter. * @param {(Object|Array)} update - The updates. * @param {Object} options - The update options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @returns {Promise} The promise of the result. */ updateOne( database: string, collection: string, filter: Document = {}, update: Document = {}, options: UpdateOptions = {}, dbOptions?: DbOptions): Promise<UpdateResult> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .updateOne(filter, update, options) as Promise<UpdateResult>; // `as UpdateResult` because we know we didn't request .explain() here. } /** * Get currently known topology information. */ getTopology(): Topology | undefined { return this.mongoClient.topology; } /** * Drop a database * * @param {String} db - The database name. * @param {Document} options - The write concern. * * @param dbOptions * @returns {Promise<Document>} The result of the operation. */ async dropDatabase( db: string, options: DropDatabaseOptions = {}, dbOptions: DbOptions = {} ): Promise<DropDatabaseResult> { const opts = { ...this.baseCmdOptions, ...options } as DropDatabaseOptions; const nativeResult = await this.db(db, dbOptions).dropDatabase(opts); const ok = nativeResult ? 1 : 0; return { ok, ...(ok ? { dropped: db } : {}) }; } /** * Adds new indexes to a collection. * * @param {String} database - The db name. * @param {String} collection - The collection name. * @param {Object[]} indexSpecs the spec of the intexes to be created. * @param {Object} options - The command options. * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * @return {Promise} */ async createIndexes( database: string, collection: string, indexSpecs: IndexDescription[], options: CreateIndexesOptions = {}, dbOptions?: DbOptions): Promise<string[]> { options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) .createIndexes(indexSpecs, options); } /** * Returns an array that holds a list of documents that identify and * describe the existing indexes on the collection. * * @param {String} database - The db name. * @param {String} collection - The collection name. * @param options * @param {Object} dbOptions - The database options * (i.e. readConcern, writeConcern. etc). * * @return {Promise} */ async getIndexes( database: string, collection: string, options: ListIndexesOptions = {}, dbOptions?: DbOptions): Promise<Document[]> { return this.db(database, dbOptions) .collection(collection) .listIndexes({ ...this.baseCmdOptions, ...options }) .toArray(); } /** * Returns an array of collection infos * * @param {String} database - The db name. * @param {Document} filter - The filter. * @param {Document} options - The command options. * @param {Object} dbOptions - The database options * (i.e. readConcern, writeConcern. etc). * * @return {Promise} */ async listCollections( database: string, filter: Document = {}, options: ListCollectionsOptions = {}, dbOptions?: DbOptions): Promise<Document[]> { options = { ...this.baseCmdOptions, ...options }; return await this.db(database, dbOptions).listCollections( filter, options ).toArray(); } /** * Get all the collection statistics. * * @param {String} database - The db name. * @param {String} collection - The collection name. * @param {Object} options - The count options. * @param {Object} dbOptions - The database options * @return {Promise} returns Promise */ async stats( database: string, collection: string, options: CollStatsOptions = {}, dbOptions?: DbOptions): Promise<Document> { options = { ...this.baseCmdOptions, ...options }; return await this.db(database, dbOptions) .collection(collection) .stats(options as { scale: 1 }); } /** * Drops a the collection. * * @param {String} database - The db name. * @param {String} collection - The collection name. * @param options * @param {Object} dbOptions - The database options (i.e. readConcern, writeConcern. etc). * * @return {Promise} */ async dropCollection( database: string, collection: string, options: DropCollectionOptions = {}, dbOptions?: DbOptions ): Promise<boolean> { return this.db(database, dbOptions) .collection(collection) .drop({ ...this.baseCmdOptions, ...options } as DropCollectionOptions); } /** * Authenticate * * @param authDoc */ async authenticate( authDoc: ShellAuthOptions ): Promise<{ ok: 1 }> { // NOTE: we keep all the original options and just overwrite the auth ones. const auth: Auth = { username: authDoc.user, password: authDoc.pwd }; await this.resetConnectionOptions({ auth, ...(authDoc.mechanism ? { authMechanism: authDoc.mechanism as AuthMechanismId } : {}), ...(authDoc.authDb ? { authSource: authDoc.authDb } : {}) }); return { ok: 1 }; } async createCollection( dbName: string, collName: string, options: CreateCollectionOptions = {}, dbOptions?: DbOptions ): Promise<{ ok: number }> { options = { ...this.baseCmdOptions, ...options }; await this.db(dbName, dbOptions).createCollection( collName, options ); return { ok: 1 }; } async initializeBulkOp( dbName: string, collName: string, ordered: boolean, options: BulkWriteOptions = {}, dbOptions?: any ): Promise<any> { // Node 4.0 returns any type for bulk ops. if (ordered) { return await this.db(dbName, dbOptions).collection(collName).initializeOrderedBulkOp(options); } return await this.db(dbName, dbOptions).collection(collName).initializeUnorderedBulkOp(options); } getReadPreference(): ReadPreference { return this.mongoClient.readPreference; } getReadConcern(): ReadConcern | undefined { return this.mongoClient.readConcern; } getWriteConcern(): WriteConcern | undefined { return this.mongoClient.writeConcern; } readPreferenceFromOptions(options?: Omit<ReadPreferenceFromOptions, 'session'>): ReadPreferenceLike | undefined { return ReadPreference.fromOptions(options); } /** * For instances where a user wants to set a option that requires a new MongoClient. * * @param options */ async resetConnectionOptions(options: MongoClientOptions): Promise<void> { this.currentClientOptions = { ...this.currentClientOptions, ...options }; const clientOptions = processDriverOptions(this.currentClientOptions); const mc = await connectMongoClient( (this.uri as ConnectionString).toString(), clientOptions ); try { await this.mongoClient.close(); // eslint-disable-next-line no-empty } catch {} this.mongoClient = mc; } startSession(options: ClientSessionOptions): ClientSession { return this.mongoClient.startSession(options); } watch(pipeline: Document[], options: ChangeStreamOptions, dbOptions: DbOptions = {}, db?: string, coll?: string): ChangeStream { if (db === undefined && coll === undefined) { // TODO: watch not exported, see NODE-2934 return (this.mongoClient as any).watch(pipeline, options); } else if (db !== undefined && coll === undefined) { return (this.db(db, dbOptions) as any).watch(pipeline, options); } else if (db !== undefined && coll !== undefined) { return (this.db(db, dbOptions).collection(coll) as any).watch(pipeline, options); } throw new MongoshInternalError('Cannot call watch with defined collection but undefined db'); } get driverMetadata(): ClientMetadata | undefined { return this.getTopology()?.clientMetadata; } getRawClient(): MongoClient { return this.mongoClient; } getURI(): string | undefined { return this.uri?.href; } getFleOptions(): AutoEncryptionOptions | undefined { return this.currentClientOptions.autoEncryption; } } export default CliServiceProvider;