diff --git a/packages/i18n/src/locales/en_US.ts b/packages/i18n/src/locales/en_US.ts index a541ab43e8..311c526389 100644 --- a/packages/i18n/src/locales/en_US.ts +++ b/packages/i18n/src/locales/en_US.ts @@ -422,6 +422,43 @@ const translations: Catalog = { }, }, }, + ClientBulkWriteResult: { + help: { + description: 'ClientBulkWriteResult Class', + attributes: { + acknowledged: { + description: 'Acknowledged returned from the server', + }, + insertedCount: { + description: 'Number of documents inserted', + }, + matchedCount: { + description: 'Number of documents matched', + }, + modifiedCount: { + description: 'Number of documents modified', + }, + deletedCount: { + description: 'Number of documents deleted', + }, + upsertedCount: { + description: 'Number of documents upserted', + }, + insertResults: { + description: + 'The results of each individual insert operation that was successfully performed', + }, + updateResults: { + description: + 'The results of each individual update operation that was successfully performed', + }, + deleteResults: { + description: + 'The results of each individual delete operation that was successfully performed.', + }, + }, + }, + }, CommandResult: { help: { description: 'CommandResult Class', @@ -2240,6 +2277,12 @@ const translations: Catalog = { link: 'https://docs.mongodb.com/manual/reference/method/Mongo.startSession/', description: 'Starts a session for the connection.', }, + bulkWrite: { + link: 'https://docs.mongodb.com/manual/reference/method/Mongo.bulkWrite', + description: + 'Performs multiple write operations across databases and collections with controls for order of execution.', + example: 'db.getMongo().bulkWrite(operations, options)', + }, getCollection: { link: 'https://docs.mongodb.com/manual/reference/method/Mongo.getCollection', description: diff --git a/packages/service-provider-core/src/all-transport-types.ts b/packages/service-provider-core/src/all-transport-types.ts index c87f8cb130..84ccab65ae 100644 --- a/packages/service-provider-core/src/all-transport-types.ts +++ b/packages/service-provider-core/src/all-transport-types.ts @@ -2,9 +2,12 @@ export type { AggregateOptions, AggregationCursor, AnyBulkWriteOperation, + AnyClientBulkWriteModel, Batch, BulkWriteOptions, BulkWriteResult, + ClientBulkWriteResult, + ClientBulkWriteOptions, ChangeStream, ChangeStreamOptions, ClientSession, diff --git a/packages/service-provider-core/src/writable.ts b/packages/service-provider-core/src/writable.ts index 436b93b4db..169d1b0fc5 100644 --- a/packages/service-provider-core/src/writable.ts +++ b/packages/service-provider-core/src/writable.ts @@ -10,6 +10,9 @@ import type { FindOneAndUpdateOptions, BulkWriteOptions, AnyBulkWriteOperation, + AnyClientBulkWriteModel, + ClientBulkWriteResult, + ClientBulkWriteOptions, DeleteOptions, DeleteResult, InsertManyResult, @@ -108,6 +111,18 @@ export default interface Writable { dbOptions?: DbOptions ): Promise; + /** + * Executes a client bulk write operation, available on server 8.0+. + * @param models - The client bulk write models. + * @param options - The bulk write options. + * + * @returns {Promise} The promise of the result. + */ + clientBulkWrite( + models: AnyClientBulkWriteModel[], + options: ClientBulkWriteOptions + ): Promise; + /** * Delete multiple documents from the collection. * diff --git a/packages/service-provider-node-driver/src/node-driver-service-provider.ts b/packages/service-provider-node-driver/src/node-driver-service-provider.ts index ceb4683657..1216b5f847 100644 --- a/packages/service-provider-node-driver/src/node-driver-service-provider.ts +++ b/packages/service-provider-node-driver/src/node-driver-service-provider.ts @@ -23,8 +23,11 @@ import type { AggregateOptions, AggregationCursor, AnyBulkWriteOperation, + AnyClientBulkWriteModel, BulkWriteOptions, BulkWriteResult, + ClientBulkWriteResult, + ClientBulkWriteOptions, ClientSessionOptions, Collection, CountDocumentsOptions, @@ -630,6 +633,17 @@ export class NodeDriverServiceProvider .bulkWrite(requests, options); } + /** + * Executes a client bulk write operation, available on server 8.0+. + */ + clientBulkWrite( + models: AnyClientBulkWriteModel[], + options: ClientBulkWriteOptions = {} + ): Promise { + options = { ...this.baseCmdOptions, ...options }; + return this.mongoClient.bulkWrite(models, options); + } + /** * Close the connection. * @@ -997,7 +1011,7 @@ export class NodeDriverServiceProvider options = { ...this.baseCmdOptions, ...options }; return this.db(database, dbOptions) .collection(collection) - .replaceOne(filter, replacement, options) as Promise; + .replaceOne(filter, replacement, options); // `as UpdateResult` because we know we didn't request .explain() here. } diff --git a/packages/shell-api/src/mongo.spec.ts b/packages/shell-api/src/mongo.spec.ts index dcbd6556a5..303a57dfec 100644 --- a/packages/shell-api/src/mongo.spec.ts +++ b/packages/shell-api/src/mongo.spec.ts @@ -35,6 +35,7 @@ import { startSharedTestServer, } from '../../../testing/integration-testing-hooks'; import { dummyOptions } from './helpers.spec'; +import { ClientBulkWriteResult } from './result'; const sampleOpts = { causalConsistency: false, @@ -1015,6 +1016,76 @@ describe('Mongo', function () { ).toArray(); }); }); + + context('post-8.0', function () { + skipIfServerVersion(testServer, '< 8.0'); + let mongo: Mongo; + + describe('bulkWrite', function () { + beforeEach(async function () { + mongo = await instanceState.shellApi.Mongo(uri, undefined, { + api: { version: '1' }, + }); + }); + + it('should allow inserts across collections and databases', async function () { + expect( + await mongo.bulkWrite([ + { + name: 'insertOne', + namespace: 'db.authors', + document: { name: 'King' }, + }, + { + name: 'deleteOne', + namespace: 'db.authors', + filter: { name: 'King' }, + }, + { + name: 'insertOne', + namespace: 'db.moreAuthors', + document: { name: 'Queen' }, + }, + { + name: 'insertOne', + namespace: 'otherDb.authors', + document: { name: 'Prince' }, + }, + ]) + ).deep.equals( + new ClientBulkWriteResult({ + acknowledged: true, + insertedCount: 3, + upsertedCount: 0, + matchedCount: 0, + modifiedCount: 0, + deletedCount: 1, + insertResults: undefined, + updateResults: undefined, + deleteResults: undefined, + }) + ); + + expect( + await mongo.getDB('db').getCollection('authors').count() + ).equals(0); + + expect( + await mongo + .getDB('db') + .getCollection('moreAuthors') + .count({ name: 'Queen' }) + ).equals(1); + + expect( + await mongo + .getDB('otherDb') + .getCollection('authors') + .count({ name: 'Prince' }) + ).equals(1); + }); + }); + }); }); }); }); diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index e163f72a30..193b91c0d8 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -36,6 +36,8 @@ import type { ServerApi, ServerApiVersion, WriteConcern, + AnyClientBulkWriteModel, + ClientBulkWriteOptions, } from '@mongosh/service-provider-core'; import type { ConnectionInfo } from '@mongosh/arg-parser'; import { @@ -45,6 +47,7 @@ import { import type Collection from './collection'; import Database from './database'; import type ShellInstanceState from './shell-instance-state'; +import { ClientBulkWriteResult } from './result'; import { CommandResult } from './result'; import { redactURICredentials } from '@mongosh/history'; import { asPrintable, ServerVersions, Topologies } from './enums'; @@ -365,6 +368,40 @@ export default class Mongo extends ShellApiClass { return await this._listDatabases(options); } + @returnsPromise + @serverVersions(['8.0.0', ServerVersions.latest]) + @apiVersions([1]) + async bulkWrite( + models: AnyClientBulkWriteModel[], + options: ClientBulkWriteOptions = {} + ): Promise { + this._emitMongoApiCall('bulkWrite', { options }); + + const { + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + } = await this._serviceProvider.clientBulkWrite(models, options); + + return new ClientBulkWriteResult({ + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + }); + } + @returnsPromise @apiVersions([1]) async getDBNames(options: ListDatabasesOptions = {}): Promise { diff --git a/packages/shell-api/src/result.ts b/packages/shell-api/src/result.ts index f4d2b3162f..6b6b85bc4a 100644 --- a/packages/shell-api/src/result.ts +++ b/packages/shell-api/src/result.ts @@ -25,6 +25,69 @@ export class CommandResult extends ShellApiValueClass { } } +export type ClientInsertResult = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + insertedId: any; +}; + +export type ClientUpdateResult = { + matchedCount: number; + modifiedCount: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + upsertedId?: any; + didUpsert: boolean; +}; + +export type ClientDeleteResult = { + deletedCount: number; +}; + +@shellApiClassDefault +export class ClientBulkWriteResult extends ShellApiValueClass { + acknowledged: boolean; + insertedCount: number; + matchedCount: number; + modifiedCount: number; + deletedCount: number; + upsertedCount: number; + insertResults?: ReadonlyMap; + updateResults?: ReadonlyMap; + deleteResults?: ReadonlyMap; + + constructor({ + acknowledged, + insertedCount, + matchedCount, + modifiedCount, + deletedCount, + upsertedCount, + insertResults, + updateResults, + deleteResults, + }: { + acknowledged: boolean; + insertedCount: number; + matchedCount: number; + modifiedCount: number; + deletedCount: number; + upsertedCount: number; + insertResults?: ReadonlyMap; + updateResults?: ReadonlyMap; + deleteResults?: ReadonlyMap; + }) { + super(); + this.acknowledged = acknowledged; + this.insertedCount = insertedCount; + this.matchedCount = matchedCount; + this.modifiedCount = modifiedCount; + this.deletedCount = deletedCount; + this.upsertedCount = upsertedCount; + this.insertResults = insertResults; + this.updateResults = updateResults; + this.deleteResults = deleteResults; + } +} + @shellApiClassDefault export class BulkWriteResult extends ShellApiValueClass { acknowledged: boolean;