diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts new file mode 100644 index 0000000000..e87b16544a --- /dev/null +++ b/src/operations/client_bulk_write/command_builder.ts @@ -0,0 +1,296 @@ +import { type Document } from '../../bson'; +import { DocumentSequence } from '../../cmap/commands'; +import { MongoInvalidArgumentError } from '../../error'; +import { + type AnyClientBulkWriteModel, + type ClientBulkWriteOptions, + type ClientDeleteManyModel, + type ClientDeleteOneModel, + type ClientInsertOneModel, + type ClientReplaceOneModel, + type ClientUpdateManyModel, + type ClientUpdateOneModel +} from './common'; + +/** @internal */ +export class ClientBulkWriteCommandBuilder { + models: AnyClientBulkWriteModel[]; + options: ClientBulkWriteOptions; + + /** + * Create the command builder. + * @param models - The client write models. + */ + constructor(models: AnyClientBulkWriteModel[], options: ClientBulkWriteOptions) { + this.models = models; + this.options = options; + } + + /** + * Gets the errorsOnly value for the command, which is the inverse of the + * user provided verboseResults option. Defaults to true. + */ + get errorsOnly(): boolean { + if ('verboseResults' in this.options) { + return !this.options.verboseResults; + } + return true; + } + + /** + * Build the bulk write commands from the models. + */ + buildCommands(): Document[] { + // The base command. + const command: Document = { + bulkWrite: 1, + errorsOnly: this.errorsOnly, + ordered: this.options.ordered ?? true + }; + // Add bypassDocumentValidation if it was present in the options. + if ('bypassDocumentValidation' in this.options) { + command.bypassDocumentValidation = this.options.bypassDocumentValidation; + } + // Add let if it was present in the options. + if ('let' in this.options) { + command.let = this.options.let; + } + + // Iterate the models to build the ops and nsInfo fields. + const operations = []; + let currentNamespaceIndex = 0; + const namespaces = new Map(); + for (const model of this.models) { + const ns = model.namespace; + if (namespaces.has(ns)) { + operations.push(builderFor(model).buildOperation(namespaces.get(ns) as number)); + } else { + namespaces.set(ns, currentNamespaceIndex); + operations.push(builderFor(model).buildOperation(currentNamespaceIndex)); + currentNamespaceIndex++; + } + } + + const nsInfo = Array.from(namespaces.keys()).map(ns => { + return { ns: ns }; + }); + command.ops = new DocumentSequence(operations); + command.nsInfo = new DocumentSequence(nsInfo); + return [command]; + } +} + +/** @internal */ +export interface OperationBuilder { + buildOperation(index: number): Document; +} + +/** + * Builds insert one operations given the model. + * @internal + */ +export class InsertOneOperationBuilder implements OperationBuilder { + model: ClientInsertOneModel; + + /** + * Instantiate the builder. + * @param model - The client insert one model. + */ + constructor(model: ClientInsertOneModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + const document: Document = { + insert: index, + document: this.model.document + }; + return document; + } +} + +/** @internal */ +export class DeleteOneOperationBuilder implements OperationBuilder { + model: ClientDeleteOneModel; + + /** + * Instantiate the builder. + * @param model - The client delete one model. + */ + constructor(model: ClientDeleteOneModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + return createDeleteOperation(this.model, index, false); + } +} + +/** @internal */ +export class DeleteManyOperationBuilder implements OperationBuilder { + model: ClientDeleteManyModel; + + /** + * Instantiate the builder. + * @param model - The client delete many model. + */ + constructor(model: ClientDeleteManyModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + return createDeleteOperation(this.model, index, true); + } +} + +/** + * Creates a delete operation based on the parameters. + */ +function createDeleteOperation( + model: ClientDeleteOneModel | ClientDeleteManyModel, + index: number, + multi: boolean +): Document { + const document: Document = { + delete: index, + multi: multi, + filter: model.filter + }; + if (model.hint) { + document.hint = model.hint; + } + if (model.collation) { + document.collation = model.collation; + } + return document; +} + +/** @internal */ +export class UpdateOneOperationBuilder implements OperationBuilder { + model: ClientUpdateOneModel; + + /** + * Instantiate the builder. + * @param model - The client update one model. + */ + constructor(model: ClientUpdateOneModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + return createUpdateOperation(this.model, index, false); + } +} + +/** @internal */ +export class UpdateManyOperationBuilder implements OperationBuilder { + model: ClientUpdateManyModel; + + /** + * Instantiate the builder. + * @param model - The client update many model. + */ + constructor(model: ClientUpdateManyModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + return createUpdateOperation(this.model, index, true); + } +} + +/** + * Creates a delete operation based on the parameters. + */ +function createUpdateOperation( + model: ClientUpdateOneModel | ClientUpdateManyModel, + index: number, + multi: boolean +): Document { + const document: Document = { + update: index, + multi: multi, + filter: model.filter, + updateMods: model.update + }; + if (model.hint) { + document.hint = model.hint; + } + if (model.upsert) { + document.upsert = model.upsert; + } + if (model.arrayFilters) { + document.arrayFilters = model.arrayFilters; + } + return document; +} + +/** @internal */ +export class ReplaceOneOperationBuilder implements OperationBuilder { + model: ClientReplaceOneModel; + + /** + * Instantiate the builder. + * @param model - The client replace one model. + */ + constructor(model: ClientReplaceOneModel) { + this.model = model; + } + + /** + * Build the operation. + * @param index - The namespace index. + * @returns the operation. + */ + buildOperation(index: number): Document { + const document: Document = { + update: index, + multi: false, + filter: this.model.filter, + updateMods: this.model.replacement + }; + return document; + } +} + +const BUILDERS: Map OperationBuilder> = new Map(); +BUILDERS.set('insertOne', model => new InsertOneOperationBuilder(model as ClientInsertOneModel)); +BUILDERS.set('deleteMany', model => new DeleteManyOperationBuilder(model as ClientDeleteManyModel)); +BUILDERS.set('deleteOne', model => new DeleteOneOperationBuilder(model as ClientDeleteOneModel)); +BUILDERS.set('updateMany', model => new UpdateManyOperationBuilder(model as ClientUpdateManyModel)); +BUILDERS.set('updateOne', model => new UpdateOneOperationBuilder(model as ClientUpdateOneModel)); +BUILDERS.set('replaceOne', model => new ReplaceOneOperationBuilder(model as ClientReplaceOneModel)); + +/** @internal */ +export function builderFor(model: AnyClientBulkWriteModel): OperationBuilder { + const builder = BUILDERS.get(model.name)?.(model); + if (!builder) { + throw new MongoInvalidArgumentError(`Could not load builder for model ${model.name}`); + } + return builder; +} diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts new file mode 100644 index 0000000000..063beeddf2 --- /dev/null +++ b/src/operations/client_bulk_write/common.ts @@ -0,0 +1,133 @@ +import { type Document } from '../../bson'; +import type { Filter, OptionalId, UpdateFilter, WithoutId } from '../../mongo_types'; +import type { CollationOptions, CommandOperationOptions } from '../../operations/command'; +import type { Hint } from '../../operations/operation'; + +/** @public */ +export interface ClientBulkWriteOptions extends CommandOperationOptions { + /** + * If true, when an insert fails, don't execute the remaining writes. + * If false, continue with remaining inserts when one fails. + * @defaultValue `true` - inserts are ordered by default + */ + ordered?: boolean; + /** + * Allow driver to bypass schema validation. + * @defaultValue `false` - documents will be validated by default + **/ + bypassDocumentValidation?: boolean; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; + /** + * Whether detailed results for each successful operation should be included in the returned + * BulkWriteResult. + */ + verboseResults?: boolean; +} + +/** @public */ +export interface ClientWriteModel { + /** The namespace for the write. */ + namespace: string; +} + +/** @public */ +export interface ClientInsertOneModel + extends ClientWriteModel { + name: 'insertOne'; + /** The document to insert. */ + document: OptionalId; +} + +/** @public */ +export interface ClientDeleteOneModel + extends ClientWriteModel { + name: 'deleteOne'; + /** The filter to limit the deleted documents. */ + filter: Filter; + /** Specifies a collation. */ + collation?: CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: Hint; +} + +/** @public */ +export interface ClientDeleteManyModel + extends ClientWriteModel { + name: 'deleteMany'; + /** The filter to limit the deleted documents. */ + filter: Filter; + /** Specifies a collation. */ + collation?: CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: Hint; +} + +/** @public */ +export interface ClientReplaceOneModel + extends ClientWriteModel { + name: 'replaceOne'; + /** The filter to limit the replaced document. */ + filter: Filter; + /** The document with which to replace the matched document. */ + replacement: WithoutId; + /** Specifies a collation. */ + collation?: CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; +} + +/** @public */ +export interface ClientUpdateOneModel + extends ClientWriteModel { + name: 'updateOne'; + /** The filter to limit the updated documents. */ + filter: Filter; + /** + * The modifications to apply. The value can be either: + * UpdateFilter - A document that contains update operator expressions, + * Document[] - an aggregation pipeline. + */ + update: UpdateFilter | Document[]; + /** A set of filters specifying to which array elements an update should apply. */ + arrayFilters?: Document[]; + /** Specifies a collation. */ + collation?: CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; +} + +/** @public */ +export interface ClientUpdateManyModel + extends ClientWriteModel { + name: 'updateMany'; + /** The filter to limit the updated documents. */ + filter: Filter; + /** + * The modifications to apply. The value can be either: + * UpdateFilter - A document that contains update operator expressions, + * Document[] - an aggregation pipeline. + */ + update: UpdateFilter | Document[]; + /** A set of filters specifying to which array elements an update should apply. */ + arrayFilters?: Document[]; + /** Specifies a collation. */ + collation?: CollationOptions; + /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ + hint?: Hint; + /** When true, creates a new document if no document matches the query. */ + upsert?: boolean; +} + +/** @public */ +export type AnyClientBulkWriteModel = + | ClientInsertOneModel + | ClientReplaceOneModel + | ClientUpdateOneModel + | ClientUpdateManyModel + | ClientDeleteOneModel + | ClientDeleteManyModel; diff --git a/test/mongodb.ts b/test/mongodb.ts index 65562f56c7..c76e63ddeb 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -154,6 +154,8 @@ export * from '../src/mongo_logger'; export * from '../src/mongo_types'; export * from '../src/operations/aggregate'; export * from '../src/operations/bulk_write'; +export * from '../src/operations/client_bulk_write/command_builder'; +export * from '../src/operations/client_bulk_write/common'; export * from '../src/operations/collections'; export * from '../src/operations/command'; export * from '../src/operations/count'; diff --git a/test/unit/operations/client_bulk_write/command_builder.test.ts b/test/unit/operations/client_bulk_write/command_builder.test.ts new file mode 100644 index 0000000000..77eb51a918 --- /dev/null +++ b/test/unit/operations/client_bulk_write/command_builder.test.ts @@ -0,0 +1,512 @@ +import { expect } from 'chai'; + +import { + builderFor, + ClientBulkWriteCommandBuilder, + type ClientDeleteManyModel, + type ClientDeleteOneModel, + type ClientInsertOneModel, + type ClientReplaceOneModel, + type ClientUpdateManyModel, + type ClientUpdateOneModel, + DeleteManyOperationBuilder, + DeleteOneOperationBuilder, + DocumentSequence, + InsertOneOperationBuilder, + ReplaceOneOperationBuilder, + UpdateManyOperationBuilder, + UpdateOneOperationBuilder +} from '../../../mongodb'; + +describe('ClientBulkWriteCommandBuilder', function () { + describe('#buildCommand', function () { + context('when custom options are provided', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const builder = new ClientBulkWriteCommandBuilder([model], { + verboseResults: true, + bypassDocumentValidation: true, + ordered: false + }); + const commands = builder.buildCommands(); + + it('sets the bulkWrite command', function () { + expect(commands[0].bulkWrite).to.equal(1); + }); + + it('sets the errorsOnly field', function () { + expect(commands[0].errorsOnly).to.be.false; + }); + + it('sets the ordered field', function () { + expect(commands[0].ordered).to.be.false; + }); + + it('sets the bypassDocumentValidation field', function () { + expect(commands[0].bypassDocumentValidation).to.be.true; + }); + + it('sets the ops document sequence', function () { + expect(commands[0].ops).to.be.instanceOf(DocumentSequence); + expect(commands[0].ops.documents[0]).to.deep.equal({ insert: 0, document: { name: 1 } }); + }); + + it('sets the nsInfo document sequence', function () { + expect(commands[0].nsInfo).to.be.instanceOf(DocumentSequence); + expect(commands[0].nsInfo.documents[0]).to.deep.equal({ ns: 'test.coll' }); + }); + }); + + context('when no options are provided', function () { + context('when a single model is provided', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const builder = new ClientBulkWriteCommandBuilder([model], {}); + const commands = builder.buildCommands(); + + it('sets the bulkWrite command', function () { + expect(commands[0].bulkWrite).to.equal(1); + }); + + it('sets the default errorsOnly field', function () { + expect(commands[0].errorsOnly).to.be.true; + }); + + it('sets the default ordered field', function () { + expect(commands[0].ordered).to.be.true; + }); + + it('sets the ops document sequence', function () { + expect(commands[0].ops).to.be.instanceOf(DocumentSequence); + expect(commands[0].ops.documents[0]).to.deep.equal({ insert: 0, document: { name: 1 } }); + }); + + it('sets the nsInfo document sequence', function () { + expect(commands[0].nsInfo).to.be.instanceOf(DocumentSequence); + expect(commands[0].nsInfo.documents[0]).to.deep.equal({ ns: 'test.coll' }); + }); + }); + + context('when multiple models are provided', function () { + context('wheh the namespace is the same', function () { + const modelOne: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const modelTwo: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 2 } + }; + const builder = new ClientBulkWriteCommandBuilder([modelOne, modelTwo], {}); + const commands = builder.buildCommands(); + + it('sets the bulkWrite command', function () { + expect(commands[0].bulkWrite).to.equal(1); + }); + + it('sets the ops document sequence', function () { + expect(commands[0].ops).to.be.instanceOf(DocumentSequence); + expect(commands[0].ops.documents).to.deep.equal([ + { insert: 0, document: { name: 1 } }, + { insert: 0, document: { name: 2 } } + ]); + }); + + it('sets the nsInfo document sequence', function () { + expect(commands[0].nsInfo).to.be.instanceOf(DocumentSequence); + expect(commands[0].nsInfo.documents).to.deep.equal([{ ns: 'test.coll' }]); + }); + }); + + context('wheh the namespace differs', function () { + const modelOne: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const modelTwo: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll2', + document: { name: 2 } + }; + const builder = new ClientBulkWriteCommandBuilder([modelOne, modelTwo], {}); + const commands = builder.buildCommands(); + + it('sets the bulkWrite command', function () { + expect(commands[0].bulkWrite).to.equal(1); + }); + + it('sets the ops document sequence', function () { + expect(commands[0].ops).to.be.instanceOf(DocumentSequence); + expect(commands[0].ops.documents).to.deep.equal([ + { insert: 0, document: { name: 1 } }, + { insert: 1, document: { name: 2 } } + ]); + }); + + it('sets the nsInfo document sequence', function () { + expect(commands[0].nsInfo).to.be.instanceOf(DocumentSequence); + expect(commands[0].nsInfo.documents).to.deep.equal([ + { ns: 'test.coll' }, + { ns: 'test.coll2' } + ]); + }); + }); + + context('wheh the namespaces are intermixed', function () { + const modelOne: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const modelTwo: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll2', + document: { name: 2 } + }; + const modelThree: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 2 } + }; + const builder = new ClientBulkWriteCommandBuilder([modelOne, modelTwo, modelThree], {}); + const commands = builder.buildCommands(); + + it('sets the bulkWrite command', function () { + expect(commands[0].bulkWrite).to.equal(1); + }); + + it('sets the ops document sequence', function () { + expect(commands[0].ops).to.be.instanceOf(DocumentSequence); + expect(commands[0].ops.documents).to.deep.equal([ + { insert: 0, document: { name: 1 } }, + { insert: 1, document: { name: 2 } }, + { insert: 0, document: { name: 2 } } + ]); + }); + + it('sets the nsInfo document sequence', function () { + expect(commands[0].nsInfo).to.be.instanceOf(DocumentSequence); + expect(commands[0].nsInfo.documents).to.deep.equal([ + { ns: 'test.coll' }, + { ns: 'test.coll2' } + ]); + }); + }); + }); + }); + }); + + describe('.builderFor', function () { + context('when the model is an insert one', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + + it('returns an insert one operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(InsertOneOperationBuilder); + }); + }); + + context('when the model is an update one', function () { + const model: ClientUpdateOneModel = { + name: 'updateOne', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + + it('returns an update one operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(UpdateOneOperationBuilder); + }); + }); + + context('when the model is an update many', function () { + const model: ClientUpdateManyModel = { + name: 'updateMany', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + + it('returns an update many operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(UpdateManyOperationBuilder); + }); + }); + + context('when the model is a replace one', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 } + }; + + it('returns an replace one operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(ReplaceOneOperationBuilder); + }); + }); + + context('when the model is a delete one', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', + namespace: 'test.coll', + filter: { name: 1 } + }; + + it('returns an delete one operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(DeleteOneOperationBuilder); + }); + }); + + context('when the model is a delete many', function () { + const model: ClientDeleteManyModel = { + name: 'deleteMany', + namespace: 'test.coll', + filter: { name: 1 } + }; + + it('returns an delete many operation builder', function () { + expect(builderFor(model)).to.be.instanceOf(DeleteManyOperationBuilder); + }); + }); + }); + + describe('InsertOneOperationBuilder', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + + describe('#buildOperation', function () { + const builder = new InsertOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the insert operation', function () { + expect(operation).to.deep.equal({ insert: 5, document: { name: 1 } }); + }); + }); + }); + + describe('DeleteOneOperationBuilder', function () { + describe('#buildOperation', function () { + context('with only required fields', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', + namespace: 'test.coll', + filter: { name: 1 } + }; + + const builder = new DeleteOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ delete: 5, filter: { name: 1 }, multi: false }); + }); + }); + + context('with optional fields', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', + namespace: 'test.coll', + filter: { name: 1 }, + hint: 'test', + collation: { locale: 'de' } + }; + + const builder = new DeleteOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ + delete: 5, + filter: { name: 1 }, + multi: false, + hint: 'test', + collation: { locale: 'de' } + }); + }); + }); + }); + }); + + describe('DeleteManyOperationBuilder', function () { + describe('#buildOperation', function () { + context('with only required fields', function () { + const model: ClientDeleteManyModel = { + name: 'deleteMany', + namespace: 'test.coll', + filter: { name: 1 } + }; + + const builder = new DeleteManyOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ delete: 5, filter: { name: 1 }, multi: true }); + }); + }); + + context('with optional fields', function () { + const model: ClientDeleteManyModel = { + name: 'deleteMany', + namespace: 'test.coll', + filter: { name: 1 }, + hint: 'test', + collation: { locale: 'de' } + }; + + const builder = new DeleteManyOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ + delete: 5, + filter: { name: 1 }, + multi: true, + hint: 'test', + collation: { locale: 'de' } + }); + }); + }); + }); + }); + + describe('UpdateOneOperationBuilder', function () { + describe('#buildOperation', function () { + context('with only required fields', function () { + const model: ClientUpdateOneModel = { + name: 'updateOne', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + + const builder = new UpdateOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { $set: { name: 2 } }, + multi: false + }); + }); + }); + + context('with optional fields', function () { + const model: ClientUpdateOneModel = { + name: 'updateOne', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } }, + hint: 'test', + upsert: true, + arrayFilters: [{ test: 1 }] + }; + + const builder = new UpdateOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { $set: { name: 2 } }, + multi: false, + hint: 'test', + upsert: true, + arrayFilters: [{ test: 1 }] + }); + }); + }); + }); + }); + + describe('UpdateManyOperationBuilder', function () { + describe('#buildOperation', function () { + context('with only required fields', function () { + const model: ClientUpdateManyModel = { + name: 'updateMany', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + + const builder = new UpdateManyOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { $set: { name: 2 } }, + multi: true + }); + }); + }); + + context('with optional fields', function () { + const model: ClientUpdateManyModel = { + name: 'updateMany', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } }, + hint: 'test', + upsert: true, + arrayFilters: [{ test: 1 }] + }; + + const builder = new UpdateManyOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { $set: { name: 2 } }, + multi: true, + hint: 'test', + upsert: true, + arrayFilters: [{ test: 1 }] + }); + }); + }); + }); + }); + + describe('ReplaceOneOperationBuilder', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 } + }; + + describe('#buildOperation', function () { + const builder = new ReplaceOneOperationBuilder(model); + const operation = builder.buildOperation(5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { name: 2 }, + multi: false + }); + }); + }); + }); +});