From 7985beb706eb212d467fde6dcf3e166528062825 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 21 Aug 2024 21:33:13 +0200 Subject: [PATCH 1/7] feat(NODE-6327): implement client bulk write types and builders --- .../client_bulk_write/command_builder.ts | 302 ++++++++++ src/operations/client_bulk_write/common.ts | 133 +++++ test/mongodb.ts | 2 + .../client_bulk_write/command_builder.test.ts | 539 ++++++++++++++++++ 4 files changed, 976 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..a2c5e43cad --- /dev/null +++ b/src/operations/client_bulk_write/command_builder.ts @@ -0,0 +1,302 @@ +import { type Document } from '../../bson'; +import { DocumentSequence } from '../../cmap/commands'; +import { MongoInvalidArgumentError } from '../../error'; +import type { + AnyClientBulkWriteModel, + ClientBulkWriteOptions, + ClientDeleteManyModel, + ClientDeleteOneModel, + ClientInsertOneModel, + ClientReplaceOneModel, + ClientUpdateManyModel, + 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 + }; + if (this.model.hint) { + document.hint = this.model.hint; + } + if (this.model.upsert) { + document.upsert = this.model.upsert; + } + 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 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..8704c1823d --- /dev/null +++ b/test/unit/operations/client_bulk_write/command_builder.test.ts @@ -0,0 +1,539 @@ +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 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('.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 () { + describe('#buildOperation', function () { + context('with only required fields', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 } + }; + + 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 + }); + }); + }); + + context('with optional fields', function () { + const model: ClientReplaceOneModel = { + name: 'replaceOne', + namespace: 'test.coll', + filter: { name: 1 }, + replacement: { name: 2 }, + hint: 'test', + upsert: true + }; + + 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, + hint: 'test', + upsert: true + }); + }); + }); + }); + }); +}); From b521d501eab6b5daa6e5c1f409af5c252dfaf88a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 27 Aug 2024 20:02:26 +0200 Subject: [PATCH 2/7] refactor: use functions not objects --- .../client_bulk_write/command_builder.ts | 291 +++++------- .../client_bulk_write/command_builder.test.ts | 414 +++++++----------- 2 files changed, 262 insertions(+), 443 deletions(-) diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts index a2c5e43cad..73a3d56ba6 100644 --- a/src/operations/client_bulk_write/command_builder.ts +++ b/src/operations/client_bulk_write/command_builder.ts @@ -1,6 +1,5 @@ import { type Document } from '../../bson'; import { DocumentSequence } from '../../cmap/commands'; -import { MongoInvalidArgumentError } from '../../error'; import type { AnyClientBulkWriteModel, ClientBulkWriteOptions, @@ -12,6 +11,17 @@ import type { 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[]; @@ -40,22 +50,7 @@ export class ClientBulkWriteCommandBuilder { /** * 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; - } - + buildCommands(): ClientBulkWriteCommand[] { // Iterate the models to build the ops and nsInfo fields. const operations = []; let currentNamespaceIndex = 0; @@ -63,10 +58,10 @@ export class ClientBulkWriteCommandBuilder { for (const model of this.models) { const ns = model.namespace; if (namespaces.has(ns)) { - operations.push(builderFor(model).buildOperation(namespaces.get(ns) as number)); + operations.push(buildOperation(model, namespaces.get(ns) as number)); } else { namespaces.set(ns, currentNamespaceIndex); - operations.push(builderFor(model).buildOperation(currentNamespaceIndex)); + operations.push(buildOperation(model, currentNamespaceIndex)); currentNamespaceIndex++; } } @@ -74,89 +69,60 @@ export class ClientBulkWriteCommandBuilder { const nsInfo = Array.from(namespaces.keys()).map(ns => { return { ns: ns }; }); - command.ops = new DocumentSequence(operations); - command.nsInfo = new DocumentSequence(nsInfo); + + // 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 ('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; + } return [command]; } } -/** @internal */ -export interface OperationBuilder { - buildOperation(index: number): Document; -} - /** - * Builds insert one operations given the model. - * @internal + * Build the insert one operation. + * @param model - The insert one model. + * @param index - The namespace index. + * @returns the operation. */ -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; +export const buildInsertOneOperation = (model: ClientInsertOneModel, index: number): Document => { + const document: Document = { + insert: index, + document: model.document + }; + return document; +}; - /** - * Instantiate the builder. - * @param model - The client delete many model. - */ - constructor(model: ClientDeleteManyModel) { - this.model = model; - } +/** + * 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 operation. - * @param index - The namespace index. - * @returns the operation. - */ - buildOperation(index: number): Document { - return createDeleteOperation(this.model, index, true); - } -} +/** + * 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. @@ -180,49 +146,25 @@ function createDeleteOperation( 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 update one operation. + * @param model - The update one model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildUpdateOneOperation = (model: ClientUpdateOneModel, index: number): Document => { + return createUpdateOperation(model, index, false); +}; - /** - * Build the operation. - * @param index - The namespace index. - * @returns the operation. - */ - buildOperation(index: number): Document { - return createUpdateOperation(this.model, index, true); - } -} +/** + * 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): Document => { + return createUpdateOperation(model, index, true); +}; /** * Creates a delete operation based on the parameters. @@ -250,53 +192,42 @@ function createUpdateOperation( 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 replace one operation. + * @param model - The replace one model. + * @param index - The namespace index. + * @returns the operation. + */ +export const buildReplaceOneOperation = (model: ClientReplaceOneModel, index: number): Document => { + const document: Document = { + update: index, + multi: false, + filter: model.filter, + updateMods: model.replacement + }; + if (model.hint) { + document.hint = model.hint; } - - /** - * 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 - }; - if (this.model.hint) { - document.hint = this.model.hint; - } - if (this.model.upsert) { - document.upsert = this.model.upsert; - } - return document; + if (model.upsert) { + document.upsert = model.upsert; } -} - -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)); + return document; +}; /** @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}`); +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); } - return builder; } diff --git a/test/unit/operations/client_bulk_write/command_builder.test.ts b/test/unit/operations/client_bulk_write/command_builder.test.ts index 8704c1823d..b0e69e2b23 100644 --- a/test/unit/operations/client_bulk_write/command_builder.test.ts +++ b/test/unit/operations/client_bulk_write/command_builder.test.ts @@ -1,7 +1,12 @@ import { expect } from 'chai'; import { - builderFor, + buildDeleteManyOperation, + buildDeleteOneOperation, + buildInsertOneOperation, + buildReplaceOneOperation, + buildUpdateManyOperation, + buildUpdateOneOperation, ClientBulkWriteCommandBuilder, type ClientDeleteManyModel, type ClientDeleteOneModel, @@ -9,13 +14,7 @@ import { type ClientReplaceOneModel, type ClientUpdateManyModel, type ClientUpdateOneModel, - DeleteManyOperationBuilder, - DeleteOneOperationBuilder, - DocumentSequence, - InsertOneOperationBuilder, - ReplaceOneOperationBuilder, - UpdateManyOperationBuilder, - UpdateOneOperationBuilder + DocumentSequence } from '../../../mongodb'; describe('ClientBulkWriteCommandBuilder', function () { @@ -205,333 +204,222 @@ describe('ClientBulkWriteCommandBuilder', function () { }); }); - describe('.builderFor', function () { - context('when the model is an insert one', function () { - const model: ClientInsertOneModel = { - name: 'insertOne', - namespace: 'test.coll', - document: { name: 1 } - }; + describe('#buildInsertOneOperation', function () { + const model: ClientInsertOneModel = { + name: 'insertOne', + namespace: 'test.coll', + document: { name: 1 } + }; + const operation = buildInsertOneOperation(model, 5); - 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); - }); + it('generates the insert operation', function () { + expect(operation).to.deep.equal({ insert: 5, document: { name: 1 } }); }); + }); - context('when the model is an update many', function () { - const model: ClientUpdateManyModel = { - name: 'updateMany', + describe('#buildDeleteOneOperation', function () { + context('with only required fields', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', namespace: 'test.coll', - filter: { name: 1 }, - update: { $set: { name: 2 } } + filter: { name: 1 } }; + const operation = buildDeleteOneOperation(model, 5); - it('returns an update many operation builder', function () { - expect(builderFor(model)).to.be.instanceOf(UpdateManyOperationBuilder); + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ delete: 5, filter: { name: 1 }, multi: false }); }); }); - context('when the model is a replace one', function () { - const model: ClientReplaceOneModel = { - name: 'replaceOne', + context('with optional fields', function () { + const model: ClientDeleteOneModel = { + name: 'deleteOne', namespace: 'test.coll', filter: { name: 1 }, - replacement: { name: 2 } + hint: 'test', + collation: { locale: 'de' } }; + const operation = buildDeleteOneOperation(model, 5); - it('returns an replace one operation builder', function () { - expect(builderFor(model)).to.be.instanceOf(ReplaceOneOperationBuilder); + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ + delete: 5, + filter: { name: 1 }, + multi: false, + hint: 'test', + collation: { locale: 'de' } + }); }); }); + }); - context('when the model is a delete one', function () { - const model: ClientDeleteOneModel = { - name: 'deleteOne', + 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('returns an delete one operation builder', function () { - expect(builderFor(model)).to.be.instanceOf(DeleteOneOperationBuilder); + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ delete: 5, filter: { name: 1 }, multi: true }); }); }); - context('when the model is a delete many', function () { + context('with optional fields', function () { const model: ClientDeleteManyModel = { name: 'deleteMany', namespace: 'test.coll', - filter: { name: 1 } + filter: { name: 1 }, + hint: 'test', + collation: { locale: 'de' } }; + const operation = buildDeleteManyOperation(model, 5); - 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', + it('generates the delete operation', function () { + expect(operation).to.deep.equal({ + delete: 5, filter: { name: 1 }, + multi: true, 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 }); - }); - }); + 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); - context('with optional fields', function () { - const model: ClientDeleteManyModel = { - name: 'deleteMany', - namespace: 'test.coll', + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, 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' } - }); + updateMods: { $set: { name: 2 } }, + multi: false }); }); }); - }); - - 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 operation = buildUpdateOneOperation(model, 5); - context('with optional fields', function () { - const model: ClientUpdateOneModel = { - name: 'updateOne', - namespace: 'test.coll', + it('generates the update operation', function () { + expect(operation).to.deep.equal({ + update: 5, filter: { name: 1 }, - update: { $set: { name: 2 } }, + updateMods: { $set: { name: 2 } }, + multi: false, 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); + 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 - }); + 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', + 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 }, - update: { $set: { name: 2 } }, + updateMods: { $set: { name: 2 } }, + multi: true, 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 () { - describe('#buildOperation', function () { - context('with only required fields', function () { - const model: ClientReplaceOneModel = { - name: 'replaceOne', - namespace: 'test.coll', - filter: { name: 1 }, - replacement: { name: 2 } - }; - - const builder = new ReplaceOneOperationBuilder(model); - const operation = builder.buildOperation(5); + 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 - }); + 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', + 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 }, - replacement: { name: 2 }, + updateMods: { name: 2 }, + multi: false, hint: 'test', upsert: true - }; - - 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, - hint: 'test', - upsert: true - }); }); }); }); From a953109fc00c1ce4bea5d77355465a8592d2146f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 27 Aug 2024 20:21:24 +0200 Subject: [PATCH 3/7] docs: update comments --- src/operations/client_bulk_write/common.ts | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts index 063beeddf2..bb33708d4f 100644 --- a/src/operations/client_bulk_write/common.ts +++ b/src/operations/client_bulk_write/common.ts @@ -43,7 +43,10 @@ export interface ClientInsertOneModel export interface ClientDeleteOneModel extends ClientWriteModel { name: 'deleteOne'; - /** The filter to limit the deleted documents. */ + /** + * 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; @@ -55,7 +58,10 @@ export interface ClientDeleteOneModel export interface ClientDeleteManyModel extends ClientWriteModel { name: 'deleteMany'; - /** The filter to limit the deleted documents. */ + /** + * The filter used to determine if a document should be deleted. + * For a deleteOne operation, all matches are removed. + */ filter: Filter; /** Specifies a collation. */ collation?: CollationOptions; @@ -67,7 +73,10 @@ export interface ClientDeleteManyModel export interface ClientReplaceOneModel extends ClientWriteModel { name: 'replaceOne'; - /** The filter to limit the replaced document. */ + /** + * 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; @@ -83,7 +92,10 @@ export interface ClientReplaceOneModel export interface ClientUpdateOneModel extends ClientWriteModel { name: 'updateOne'; - /** The filter to limit the updated documents. */ + /** + * 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: @@ -105,7 +117,10 @@ export interface ClientUpdateOneModel export interface ClientUpdateManyModel extends ClientWriteModel { name: 'updateMany'; - /** The filter to limit the updated documents. */ + /** + * 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: From 94a9730b9bd1f6742990afd8a0a01b96901c3570 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 27 Aug 2024 20:26:58 +0200 Subject: [PATCH 4/7] docs: typo --- src/operations/client_bulk_write/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts index bb33708d4f..fba681e15f 100644 --- a/src/operations/client_bulk_write/common.ts +++ b/src/operations/client_bulk_write/common.ts @@ -60,7 +60,7 @@ export interface ClientDeleteManyModel name: 'deleteMany'; /** * The filter used to determine if a document should be deleted. - * For a deleteOne operation, all matches are removed. + * For a deleteMany operation, all matches are removed. */ filter: Filter; /** Specifies a collation. */ From 7d50499d1a3c96a309378979ef8a786b87cea208 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 27 Aug 2024 22:38:13 +0200 Subject: [PATCH 5/7] chore: more typing --- .../client_bulk_write/command_builder.ts | 72 +++++++++++++++---- src/operations/client_bulk_write/common.ts | 9 ++- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts index 73a3d56ba6..a0c6255d07 100644 --- a/src/operations/client_bulk_write/command_builder.ts +++ b/src/operations/client_bulk_write/command_builder.ts @@ -1,5 +1,8 @@ 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, @@ -66,9 +69,7 @@ export class ClientBulkWriteCommandBuilder { } } - const nsInfo = Array.from(namespaces.keys()).map(ns => { - return { ns: ns }; - }); + const nsInfo = Array.from(namespaces.keys(), ns => ({ ns })); // The base command. const command: ClientBulkWriteCommand = { @@ -83,13 +84,19 @@ export class ClientBulkWriteCommandBuilder { command.bypassDocumentValidation = this.options.bypassDocumentValidation; } // Add let if it was present in the options. - if ('let' in this.options) { + if (this.options.let) { command.let = this.options.let; } return [command]; } } +/** @internal */ +export interface InsertOperation { + insert: number; + document: OptionalId; +} + /** * Build the insert one operation. * @param model - The insert one model. @@ -97,13 +104,22 @@ export class ClientBulkWriteCommandBuilder { * @returns the operation. */ export const buildInsertOneOperation = (model: ClientInsertOneModel, index: number): Document => { - const document: Document = { + const document: InsertOperation = { insert: index, document: model.document }; return document; }; +/** @internal */ +export interface DeleteOperation { + delete: number; + multi: boolean; + filter: Filter; + hint?: Hint; + collation?: CollationOptions; +} + /** * Build the delete one operation. * @param model - The insert many model. @@ -131,8 +147,8 @@ function createDeleteOperation( model: ClientDeleteOneModel | ClientDeleteManyModel, index: number, multi: boolean -): Document { - const document: Document = { +): DeleteOperation { + const document: DeleteOperation = { delete: index, multi: multi, filter: model.filter @@ -146,13 +162,27 @@ function createDeleteOperation( return document; } +/** @internal */ +export interface UpdateOperation { + 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): Document => { +export const buildUpdateOneOperation = ( + model: ClientUpdateOneModel, + index: number +): UpdateOperation => { return createUpdateOperation(model, index, false); }; @@ -162,7 +192,10 @@ export const buildUpdateOneOperation = (model: ClientUpdateOneModel, index: numb * @param index - The namespace index. * @returns the operation. */ -export const buildUpdateManyOperation = (model: ClientUpdateManyModel, index: number): Document => { +export const buildUpdateManyOperation = ( + model: ClientUpdateManyModel, + index: number +): UpdateOperation => { return createUpdateOperation(model, index, true); }; @@ -173,8 +206,8 @@ function createUpdateOperation( model: ClientUpdateOneModel | ClientUpdateManyModel, index: number, multi: boolean -): Document { - const document: Document = { +): UpdateOperation { + const document: UpdateOperation = { update: index, multi: multi, filter: model.filter, @@ -192,14 +225,27 @@ function createUpdateOperation( return document; } +/** @internal */ +export interface ReplaceOneOperation { + 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): Document => { - const document: Document = { +export const buildReplaceOneOperation = ( + model: ClientReplaceOneModel, + index: number +): ReplaceOneOperation => { + const document: ReplaceOneOperation = { update: index, multi: false, filter: model.filter, diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts index fba681e15f..a5d401c5be 100644 --- a/src/operations/client_bulk_write/common.ts +++ b/src/operations/client_bulk_write/common.ts @@ -138,7 +138,14 @@ export interface ClientUpdateManyModel upsert?: boolean; } -/** @public */ +/** + * Used to represent any of the client bulk write models that can be passed as an array + * to MongoClient#bulkWrite. TSchema can be different on each of the individual models + * and must always match the appropriate namespace that it defines provided to each of the models. + * The schema is used on ClientInsertOneModel for the document field getting inserted, while all other + * models use it for the filter document field. + * @public + */ export type AnyClientBulkWriteModel = | ClientInsertOneModel | ClientReplaceOneModel From 9272b7a4421b058791a3d49b9c207463ebc316f3 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 27 Aug 2024 22:45:50 +0200 Subject: [PATCH 6/7] chore: update operation types --- .../client_bulk_write/command_builder.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts index a0c6255d07..1d1f1586df 100644 --- a/src/operations/client_bulk_write/command_builder.ts +++ b/src/operations/client_bulk_write/command_builder.ts @@ -60,8 +60,9 @@ export class ClientBulkWriteCommandBuilder { const namespaces = new Map(); for (const model of this.models) { const ns = model.namespace; - if (namespaces.has(ns)) { - operations.push(buildOperation(model, namespaces.get(ns) as number)); + const index = namespaces.get(ns); + if (index != null) { + operations.push(buildOperation(model, index)); } else { namespaces.set(ns, currentNamespaceIndex); operations.push(buildOperation(model, currentNamespaceIndex)); @@ -92,7 +93,7 @@ export class ClientBulkWriteCommandBuilder { } /** @internal */ -export interface InsertOperation { +interface ClientInsertOperation { insert: number; document: OptionalId; } @@ -103,8 +104,11 @@ export interface InsertOperation { * @param index - The namespace index. * @returns the operation. */ -export const buildInsertOneOperation = (model: ClientInsertOneModel, index: number): Document => { - const document: InsertOperation = { +export const buildInsertOneOperation = ( + model: ClientInsertOneModel, + index: number +): ClientInsertOperation => { + const document: ClientInsertOperation = { insert: index, document: model.document }; @@ -112,7 +116,7 @@ export const buildInsertOneOperation = (model: ClientInsertOneModel, index: numb }; /** @internal */ -export interface DeleteOperation { +export interface ClientDeleteOperation { delete: number; multi: boolean; filter: Filter; @@ -147,8 +151,8 @@ function createDeleteOperation( model: ClientDeleteOneModel | ClientDeleteManyModel, index: number, multi: boolean -): DeleteOperation { - const document: DeleteOperation = { +): ClientDeleteOperation { + const document: ClientDeleteOperation = { delete: index, multi: multi, filter: model.filter @@ -163,7 +167,7 @@ function createDeleteOperation( } /** @internal */ -export interface UpdateOperation { +export interface ClientUpdateOperation { update: number; multi: boolean; filter: Filter; @@ -182,7 +186,7 @@ export interface UpdateOperation { export const buildUpdateOneOperation = ( model: ClientUpdateOneModel, index: number -): UpdateOperation => { +): ClientUpdateOperation => { return createUpdateOperation(model, index, false); }; @@ -195,7 +199,7 @@ export const buildUpdateOneOperation = ( export const buildUpdateManyOperation = ( model: ClientUpdateManyModel, index: number -): UpdateOperation => { +): ClientUpdateOperation => { return createUpdateOperation(model, index, true); }; @@ -206,8 +210,8 @@ function createUpdateOperation( model: ClientUpdateOneModel | ClientUpdateManyModel, index: number, multi: boolean -): UpdateOperation { - const document: UpdateOperation = { +): ClientUpdateOperation { + const document: ClientUpdateOperation = { update: index, multi: multi, filter: model.filter, @@ -226,7 +230,7 @@ function createUpdateOperation( } /** @internal */ -export interface ReplaceOneOperation { +export interface ClientReplaceOneOperation { update: number; multi: boolean; filter: Filter; @@ -244,8 +248,8 @@ export interface ReplaceOneOperation { export const buildReplaceOneOperation = ( model: ClientReplaceOneModel, index: number -): ReplaceOneOperation => { - const document: ReplaceOneOperation = { +): ClientReplaceOneOperation => { + const document: ClientReplaceOneOperation = { update: index, multi: false, filter: model.filter, From fd1d467abb6016c192da016b8842531bb23b4106 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 28 Aug 2024 19:50:25 +0200 Subject: [PATCH 7/7] refactor: next batch comments --- .../client_bulk_write/command_builder.ts | 22 +++---- src/operations/client_bulk_write/common.ts | 59 ++++++++----------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts index 1d1f1586df..4d4d323de6 100644 --- a/src/operations/client_bulk_write/command_builder.ts +++ b/src/operations/client_bulk_write/command_builder.ts @@ -81,7 +81,7 @@ export class ClientBulkWriteCommandBuilder { nsInfo: new DocumentSequence(nsInfo) }; // Add bypassDocumentValidation if it was present in the options. - if ('bypassDocumentValidation' in this.options) { + if (this.options.bypassDocumentValidation != null) { command.bypassDocumentValidation = this.options.bypassDocumentValidation; } // Add let if it was present in the options. @@ -93,9 +93,9 @@ export class ClientBulkWriteCommandBuilder { } /** @internal */ -interface ClientInsertOperation { +interface ClientInsertOperation { insert: number; - document: OptionalId; + document: OptionalId; } /** @@ -116,10 +116,10 @@ export const buildInsertOneOperation = ( }; /** @internal */ -export interface ClientDeleteOperation { +export interface ClientDeleteOperation { delete: number; multi: boolean; - filter: Filter; + filter: Filter; hint?: Hint; collation?: CollationOptions; } @@ -167,11 +167,11 @@ function createDeleteOperation( } /** @internal */ -export interface ClientUpdateOperation { +export interface ClientUpdateOperation { update: number; multi: boolean; - filter: Filter; - updateMods: UpdateFilter | Document[]; + filter: Filter; + updateMods: UpdateFilter | Document[]; hint?: Hint; upsert?: boolean; arrayFilters?: Document[]; @@ -230,11 +230,11 @@ function createUpdateOperation( } /** @internal */ -export interface ClientReplaceOneOperation { +export interface ClientReplaceOneOperation { update: number; multi: boolean; - filter: Filter; - updateMods: WithoutId; + filter: Filter; + updateMods: WithoutId; hint?: Hint; upsert?: boolean; } diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts index a5d401c5be..e76fb5108f 100644 --- a/src/operations/client_bulk_write/common.ts +++ b/src/operations/client_bulk_write/common.ts @@ -32,22 +32,20 @@ export interface ClientWriteModel { } /** @public */ -export interface ClientInsertOneModel - extends ClientWriteModel { +export interface ClientInsertOneModel extends ClientWriteModel { name: 'insertOne'; /** The document to insert. */ - document: OptionalId; + document: OptionalId; } /** @public */ -export interface ClientDeleteOneModel - extends ClientWriteModel { +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; + 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. */ @@ -55,14 +53,13 @@ export interface ClientDeleteOneModel } /** @public */ -export interface ClientDeleteManyModel - extends ClientWriteModel { +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; + 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. */ @@ -70,16 +67,15 @@ export interface ClientDeleteManyModel } /** @public */ -export interface ClientReplaceOneModel - extends ClientWriteModel { +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; + filter: Filter; /** The document with which to replace the matched document. */ - replacement: WithoutId; + 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. */ @@ -89,20 +85,19 @@ export interface ClientReplaceOneModel } /** @public */ -export interface ClientUpdateOneModel - extends ClientWriteModel { +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; + filter: Filter; /** * The modifications to apply. The value can be either: - * UpdateFilter - A document that contains update operator expressions, + * UpdateFilter - A document that contains update operator expressions, * Document[] - an aggregation pipeline. */ - update: UpdateFilter | Document[]; + update: UpdateFilter | Document[]; /** A set of filters specifying to which array elements an update should apply. */ arrayFilters?: Document[]; /** Specifies a collation. */ @@ -114,20 +109,19 @@ export interface ClientUpdateOneModel } /** @public */ -export interface ClientUpdateManyModel - extends ClientWriteModel { +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; + filter: Filter; /** * The modifications to apply. The value can be either: - * UpdateFilter - A document that contains update operator expressions, + * UpdateFilter - A document that contains update operator expressions, * Document[] - an aggregation pipeline. */ - update: UpdateFilter | Document[]; + update: UpdateFilter | Document[]; /** A set of filters specifying to which array elements an update should apply. */ arrayFilters?: Document[]; /** Specifies a collation. */ @@ -140,16 +134,13 @@ export interface ClientUpdateManyModel /** * Used to represent any of the client bulk write models that can be passed as an array - * to MongoClient#bulkWrite. TSchema can be different on each of the individual models - * and must always match the appropriate namespace that it defines provided to each of the models. - * The schema is used on ClientInsertOneModel for the document field getting inserted, while all other - * models use it for the filter document field. + * to MongoClient#bulkWrite. * @public */ -export type AnyClientBulkWriteModel = - | ClientInsertOneModel - | ClientReplaceOneModel - | ClientUpdateOneModel - | ClientUpdateManyModel - | ClientDeleteOneModel - | ClientDeleteManyModel; +export type AnyClientBulkWriteModel = + | ClientInsertOneModel + | ClientReplaceOneModel + | ClientUpdateOneModel + | ClientUpdateManyModel + | ClientDeleteOneModel + | ClientDeleteManyModel;