From 6d65ae77835cf144b540a5a344e1f10bb1988bc5 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 28 Aug 2024 21:49:58 +0200 Subject: [PATCH] feat(NODE-6327): new client bulk write types and builders (#4205) --- .../client_bulk_write/command_builder.ts | 283 ++++++++++++ src/operations/client_bulk_write/common.ts | 146 ++++++ test/mongodb.ts | 2 + .../client_bulk_write/command_builder.test.ts | 427 ++++++++++++++++++ 4 files changed, 858 insertions(+) create mode 100644 src/operations/client_bulk_write/command_builder.ts create mode 100644 src/operations/client_bulk_write/common.ts create mode 100644 test/unit/operations/client_bulk_write/command_builder.test.ts 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..4d4d323de6 --- /dev/null +++ b/src/operations/client_bulk_write/command_builder.ts @@ -0,0 +1,283 @@ +import { type Document } from '../../bson'; +import { DocumentSequence } from '../../cmap/commands'; +import type { Filter, OptionalId, UpdateFilter, WithoutId } from '../../mongo_types'; +import { type CollationOptions } from '../command'; +import { type Hint } from '../operation'; +import type { + AnyClientBulkWriteModel, + ClientBulkWriteOptions, + ClientDeleteManyModel, + ClientDeleteOneModel, + ClientInsertOneModel, + ClientReplaceOneModel, + ClientUpdateManyModel, + ClientUpdateOneModel +} from './common'; + +/** @internal */ +export interface ClientBulkWriteCommand { + bulkWrite: 1; + errorsOnly: boolean; + ordered: boolean; + ops: DocumentSequence; + nsInfo: DocumentSequence; + bypassDocumentValidation?: boolean; + let?: Document; +} + +/** @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(): ClientBulkWriteCommand[] { + // 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; + const index = namespaces.get(ns); + if (index != null) { + operations.push(buildOperation(model, index)); + } else { + namespaces.set(ns, currentNamespaceIndex); + operations.push(buildOperation(model, currentNamespaceIndex)); + currentNamespaceIndex++; + } + } + + const nsInfo = Array.from(namespaces.keys(), ns => ({ ns })); + + // The base command. + const command: ClientBulkWriteCommand = { + bulkWrite: 1, + errorsOnly: this.errorsOnly, + ordered: this.options.ordered ?? true, + ops: new DocumentSequence(operations), + nsInfo: new DocumentSequence(nsInfo) + }; + // Add bypassDocumentValidation if it was present in the options. + if (this.options.bypassDocumentValidation != null) { + command.bypassDocumentValidation = this.options.bypassDocumentValidation; + } + // Add let if it was present in the options. + if (this.options.let) { + command.let = this.options.let; + } + return [command]; + } +} + +/** @internal */ +interface ClientInsertOperation { + insert: number; + document: OptionalId; +} + +/** + * Build the insert one operation. + * @param model - The insert one model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildInsertOneOperation = ( + model: ClientInsertOneModel, + index: number +): ClientInsertOperation => { + const document: ClientInsertOperation = { + insert: index, + document: model.document + }; + return document; +}; + +/** @internal */ +export interface ClientDeleteOperation { + delete: number; + multi: boolean; + filter: Filter; + hint?: Hint; + collation?: CollationOptions; +} + +/** + * Build the delete one operation. + * @param model - The insert many model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildDeleteOneOperation = (model: ClientDeleteOneModel, index: number): Document => { + return createDeleteOperation(model, index, false); +}; + +/** + * Build the delete many operation. + * @param model - The delete many model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildDeleteManyOperation = (model: ClientDeleteManyModel, index: number): Document => { + return createDeleteOperation(model, index, true); +}; + +/** + * Creates a delete operation based on the parameters. + */ +function createDeleteOperation( + model: ClientDeleteOneModel | ClientDeleteManyModel, + index: number, + multi: boolean +): ClientDeleteOperation { + const document: ClientDeleteOperation = { + 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 interface ClientUpdateOperation { + update: number; + multi: boolean; + filter: Filter; + updateMods: UpdateFilter | Document[]; + hint?: Hint; + upsert?: boolean; + arrayFilters?: Document[]; +} + +/** + * Build the update one operation. + * @param model - The update one model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildUpdateOneOperation = ( + model: ClientUpdateOneModel, + index: number +): ClientUpdateOperation => { + return createUpdateOperation(model, index, false); +}; + +/** + * Build the update many operation. + * @param model - The update many model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildUpdateManyOperation = ( + model: ClientUpdateManyModel, + index: number +): ClientUpdateOperation => { + return createUpdateOperation(model, index, true); +}; + +/** + * Creates a delete operation based on the parameters. + */ +function createUpdateOperation( + model: ClientUpdateOneModel | ClientUpdateManyModel, + index: number, + multi: boolean +): ClientUpdateOperation { + const document: ClientUpdateOperation = { + 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 interface ClientReplaceOneOperation { + update: number; + multi: boolean; + filter: Filter; + updateMods: WithoutId; + hint?: Hint; + upsert?: boolean; +} + +/** + * Build the replace one operation. + * @param model - The replace one model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildReplaceOneOperation = ( + model: ClientReplaceOneModel, + index: number +): ClientReplaceOneOperation => { + const document: ClientReplaceOneOperation = { + update: index, + multi: false, + filter: model.filter, + updateMods: model.replacement + }; + if (model.hint) { + document.hint = model.hint; + } + if (model.upsert) { + document.upsert = model.upsert; + } + return document; +}; + +/** @internal */ +export function buildOperation(model: AnyClientBulkWriteModel, index: number): Document { + switch (model.name) { + case 'insertOne': + return buildInsertOneOperation(model, index); + case 'deleteOne': + return buildDeleteOneOperation(model, index); + case 'deleteMany': + return buildDeleteManyOperation(model, index); + case 'updateOne': + return buildUpdateOneOperation(model, index); + case 'updateMany': + return buildUpdateManyOperation(model, index); + case 'replaceOne': + return buildReplaceOneOperation(model, index); + } +} diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts new file mode 100644 index 0000000000..e76fb5108f --- /dev/null +++ b/src/operations/client_bulk_write/common.ts @@ -0,0 +1,146 @@ +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 used to determine if a document should be deleted. + * For a deleteOne operation, the first match is removed. + */ + 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 used to determine if a document should be deleted. + * For a deleteMany operation, all matches are removed. + */ + 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 used to determine if a document should be replaced. + * For a replaceOne operation, the first match is replaced. + */ + 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 used to determine if a document should be updated. + * For an updateOne operation, the first match is updated. + */ + 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 used to determine if a document should be updated. + * For an updateMany operation, all matches are updated. + */ + 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; +} + +/** + * Used to represent any of the client bulk write models that can be passed as an array + * to MongoClient#bulkWrite. + * @public + */ +export type AnyClientBulkWriteModel = + | ClientInsertOneModel + | ClientReplaceOneModel + | ClientUpdateOneModel + | ClientUpdateManyModel + | ClientDeleteOneModel + | ClientDeleteManyModel; diff --git a/test/mongodb.ts b/test/mongodb.ts index 22a85de00f..b84a9ac3a0 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -157,6 +157,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..b0e69e2b23 --- /dev/null +++ b/test/unit/operations/client_bulk_write/command_builder.test.ts @@ -0,0 +1,427 @@ +import { expect } from 'chai'; + +import { + buildDeleteManyOperation, + buildDeleteOneOperation, + buildInsertOneOperation, + buildReplaceOneOperation, + buildUpdateManyOperation, + buildUpdateOneOperation, + ClientBulkWriteCommandBuilder, + type ClientDeleteManyModel, + type ClientDeleteOneModel, + type ClientInsertOneModel, + type ClientReplaceOneModel, + type ClientUpdateManyModel, + type ClientUpdateOneModel, + DocumentSequence +} 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 to the inverse of verboseResults', 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('when 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('when 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('when 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('#buildInsertOneOperation', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const operation = buildInsertOneOperation(model, 5); + + it('generates the insert operation', function () { + expect(operation).to.deep.equal({ insert: 5, document: { name: 1 } }); + }); + }); + + describe('#buildDeleteOneOperation', function () { + context('with only required fields', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', + namespace: 'test.coll', + filter: { name: 1 } + }; + const operation = buildDeleteOneOperation(model, 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 operation = buildDeleteOneOperation(model, 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('#buildDeleteManyOperation', function () { + context('with only required fields', function () { + const model: ClientDeleteManyModel = { + name: 'deleteMany', + namespace: 'test.coll', + filter: { name: 1 } + }; + const operation = buildDeleteManyOperation(model, 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 operation = buildDeleteManyOperation(model, 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('#buildUpdateOneOperation', function () { + context('with only required fields', function () { + const model: ClientUpdateOneModel = { + name: 'updateOne', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + const operation = buildUpdateOneOperation(model, 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 operation = buildUpdateOneOperation(model, 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('#buildUpdateManyOperation', function () { + context('with only required fields', function () { + const model: ClientUpdateManyModel = { + name: 'updateMany', + namespace: 'test.coll', + filter: { name: 1 }, + update: { $set: { name: 2 } } + }; + const operation = buildUpdateManyOperation(model, 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 operation = buildUpdateManyOperation(model, 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('#buildReplaceOneOperation', function () { + context('with only required fields', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 } + }; + const operation = buildReplaceOneOperation(model, 5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { name: 2 }, + multi: false + }); + }); + }); + + context('with optional fields', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 }, + hint: 'test', + upsert: true + }; + const operation = buildReplaceOneOperation(model, 5); + + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, + filter: { name: 1 }, + updateMods: { name: 2 }, + multi: false, + hint: 'test', + upsert: true + }); + }); + }); + }); +});