diff --git a/src/collection.ts b/src/collection.ts index 3736954ad7e..095de86bc71 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -45,7 +45,9 @@ import { FindOneAndDeleteOperation, FindOneAndReplaceOperation, FindOneAndUpdateOperation, - FindAndModifyOptions + FindOneAndDeleteOptions, + FindOneAndReplaceOptions, + FindOneAndUpdateOptions } from './operations/find_and_modify'; import { InsertOneOperation, @@ -1098,15 +1100,15 @@ export class Collection { */ findOneAndDelete(filter: Document): Promise; findOneAndDelete(filter: Document, callback: Callback): void; - findOneAndDelete(filter: Document, options: FindAndModifyOptions): Promise; + findOneAndDelete(filter: Document, options: FindOneAndDeleteOptions): Promise; findOneAndDelete( filter: Document, - options: FindAndModifyOptions, + options: FindOneAndDeleteOptions, callback: Callback ): void; findOneAndDelete( filter: Document, - options?: FindAndModifyOptions | Callback, + options?: FindOneAndDeleteOptions | Callback, callback?: Callback ): Promise | void { if (typeof options === 'function') (callback = options), (options = {}); @@ -1131,18 +1133,18 @@ export class Collection { findOneAndReplace( filter: Document, replacement: Document, - options: FindAndModifyOptions + options: FindOneAndReplaceOptions ): Promise; findOneAndReplace( filter: Document, replacement: Document, - options: FindAndModifyOptions, + options: FindOneAndReplaceOptions, callback: Callback ): void; findOneAndReplace( filter: Document, replacement: Document, - options?: FindAndModifyOptions | Callback, + options?: FindOneAndReplaceOptions | Callback, callback?: Callback ): Promise | void { if (typeof options === 'function') (callback = options), (options = {}); @@ -1167,18 +1169,18 @@ export class Collection { findOneAndUpdate( filter: Document, update: Document, - options: FindAndModifyOptions + options: FindOneAndUpdateOptions ): Promise; findOneAndUpdate( filter: Document, update: Document, - options: FindAndModifyOptions, + options: FindOneAndUpdateOptions, callback: Callback ): void; findOneAndUpdate( filter: Document, update: Document, - options?: FindAndModifyOptions | Callback, + options?: FindOneAndUpdateOptions | Callback, callback?: Callback ): Promise | void { if (typeof options === 'function') (callback = options), (options = {}); diff --git a/src/index.ts b/src/index.ts index 5acbc023f99..f0ede7698d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -225,7 +225,11 @@ export type { EstimatedDocumentCountOptions } from './operations/estimated_docum export type { EvalOptions } from './operations/eval'; export type { FindOptions } from './operations/find'; export type { Sort, SortDirection } from './sort'; -export type { FindAndModifyOptions } from './operations/find_and_modify'; +export type { + FindOneAndDeleteOptions, + FindOneAndReplaceOptions, + FindOneAndUpdateOptions +} from './operations/find_and_modify'; export type { IndexSpecification, CreateIndexesOptions, diff --git a/src/operations/find_and_modify.ts b/src/operations/find_and_modify.ts index d0f5e87d846..4bd8a8f3f6a 100644 --- a/src/operations/find_and_modify.ts +++ b/src/operations/find_and_modify.ts @@ -1,11 +1,5 @@ import { ReadPreference } from '../read_preference'; -import { - maxWireVersion, - applyRetryableWrites, - decorateWithCollation, - hasAtomicOperators, - Callback -} from '../utils'; +import { maxWireVersion, decorateWithCollation, hasAtomicOperators, Callback } from '../utils'; import { MongoError } from '../error'; import { CommandOperation, CommandOperationOptions } from './command'; import { defineAspects, Aspect } from './operation'; @@ -16,15 +10,59 @@ import { Sort, formatSort } from '../sort'; import type { ClientSession } from '../sessions'; /** @public */ -export interface FindAndModifyOptions extends CommandOperationOptions { +export interface FindOneAndDeleteOptions extends CommandOperationOptions { + /** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/ + hint?: Document; + /** Limits the fields to return for all matching documents. */ + projection?: Document; + /** Determines which document the operation modifies if the query selects multiple documents. */ + sort?: Sort; +} + +/** @public */ +export interface FindOneAndReplaceOptions extends CommandOperationOptions { + /** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */ + bypassDocumentValidation?: boolean; + /** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/ + hint?: Document; + /** Limits the fields to return for all matching documents. */ + projection?: Document; + /** When false, returns the updated document rather than the original. The default is true. */ + returnOriginal?: boolean; + /** Determines which document the operation modifies if the query selects multiple documents. */ + sort?: Sort; + /** Upsert the document if it does not exist. */ + upsert?: boolean; +} + +/** @public */ +export interface FindOneAndUpdateOptions extends CommandOperationOptions { + /** Optional list of array filters referenced in filtered positional operators */ + arrayFilters?: Document[]; + /** Allow driver to bypass schema validation in MongoDB 3.2 or higher. */ + bypassDocumentValidation?: boolean; + /** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/ + hint?: Document; + /** Limits the fields to return for all matching documents. */ + projection?: Document; + /** When false, returns the updated document rather than the original. The default is true. */ + returnOriginal?: boolean; + /** Determines which document the operation modifies if the query selects multiple documents. */ + sort?: Sort; + /** Upsert the document if it does not exist. */ + upsert?: boolean; +} + +// TODO: NODE-1812 to deprecate returnOriginal for returnDocument + +/** @internal */ +interface FindAndModifyOptions extends CommandOperationOptions { /** When false, returns the updated document rather than the original. The default is true. */ returnOriginal?: boolean; /** Upsert the document if it does not exist. */ upsert?: boolean; /** Limits the fields to return for all matching documents. */ projection?: Document; - /** @deprecated use `projection` instead */ - fields?: Document; /** Determines which document the operation modifies if the query selects multiple documents. */ sort?: Sort; /** Optional list of array filters referenced in filtered positional operators */ @@ -33,27 +71,32 @@ export interface FindAndModifyOptions extends CommandOperationOptions { bypassDocumentValidation?: boolean; /** An optional hint for query optimization. See the {@link https://docs.mongodb.com/manual/reference/command/update/#update-command-hint|update command} reference for more information.*/ hint?: Document; - - // NOTE: These types are a misuse of options, can we think of a way to remove them? - update?: boolean; - remove?: boolean; - new?: boolean; } /** @internal */ -export class FindAndModifyOperation extends CommandOperation { +const OperationTypes = Object.freeze({ + deleteOne: 'deleteOne', + replaceOne: 'replaceOne', + updateOne: 'updateOne' +} as const); + +/** @internal */ +type OperationType = typeof OperationTypes[keyof typeof OperationTypes]; + +/** @internal */ +class FindAndModifyOperation extends CommandOperation { options: FindAndModifyOptions; collection: Collection; query: Document; sort?: Sort; doc?: Document; + operationType?: OperationType; constructor( collection: Collection, query: Document, - sort: Sort | undefined, doc: Document | undefined, - options?: FindAndModifyOptions + options: FindAndModifyOptions ) { super(collection, options); this.options = options ?? {}; @@ -63,7 +106,7 @@ export class FindAndModifyOperation extends CommandOperation { this.collection = collection; this.query = query; - this.sort = sort; + this.sort = options.sort; this.doc = doc; } @@ -72,7 +115,7 @@ export class FindAndModifyOperation extends CommandOperation { const query = this.query; const sort = formatSort(this.sort); const doc = this.doc; - let options = { ...this.options, ...this.bsonOptions }; + const options = { ...this.options, ...this.bsonOptions }; // Create findAndModify command object const cmd: Document = { @@ -80,48 +123,47 @@ export class FindAndModifyOperation extends CommandOperation { query: query }; - if (sort) { - cmd.sort = sort; + if (this.operationType === OperationTypes.deleteOne) { + cmd.remove = true; + } else { + cmd.remove = false; + // TODO: cmd.new and cmd.upsert were set in two nonequivalent ways, do we want the truthy or the boolean approach? + cmd.new = typeof options.returnOriginal === 'boolean' ? !options.returnOriginal : false; + // TODO: technically, cmd.upsert would previously be set "as is" if it were passed in for delete, do we want to preserve + // that behavior? + cmd.upsert = typeof options.upsert === 'boolean' ? options.upsert : false; + if (doc) { + cmd.update = doc; + } } - cmd.new = options.new ? true : false; - cmd.remove = options.remove ? true : false; - cmd.upsert = options.upsert ? true : false; - - const projection = options.projection || options.fields; + // TODO: arrayFilters is specific to update - do we want to add it to a type-specific block? + if (options.arrayFilters) { + cmd.arrayFilters = options.arrayFilters; + } - if (projection) { - cmd.fields = projection; + // TODO: bypassDocumentValidation is not applicable to delete - do we want to add it to the else block? + if (options.bypassDocumentValidation === true) { + cmd.bypassDocumentValidation = options.bypassDocumentValidation; } - if (options.arrayFilters) { - cmd.arrayFilters = options.arrayFilters; + if (sort) { + cmd.sort = sort; } - if (doc && !options.remove) { - cmd.update = doc; + if (options.projection) { + cmd.fields = options.projection; } if (options.maxTimeMS) { cmd.maxTimeMS = options.maxTimeMS; } - // No check on the documents - options.checkKeys = false; - - // Final options for retryable writes - options = applyRetryableWrites(options, coll.s.db); - // Decorate the findAndModify command with the write Concern if (options.writeConcern) { cmd.writeConcern = options.writeConcern; } - // Have we specified bypassDocumentValidation - if (options.bypassDocumentValidation === true) { - cmd.bypassDocumentValidation = options.bypassDocumentValidation; - } - // Have we specified collation try { decorateWithCollation(cmd, coll, options); @@ -159,18 +201,14 @@ export class FindAndModifyOperation extends CommandOperation { /** @internal */ export class FindOneAndDeleteOperation extends FindAndModifyOperation { - constructor(collection: Collection, filter: Document, options: FindAndModifyOptions) { - // Final options - const finalOptions = Object.assign({}, options); - finalOptions.fields = options.projection; - finalOptions.remove = true; - + constructor(collection: Collection, filter: Document, options: FindOneAndDeleteOptions) { // Basic validation if (filter == null || typeof filter !== 'object') { throw new TypeError('Filter parameter must be an object'); } - super(collection, filter, finalOptions.sort, undefined, finalOptions); + super(collection, filter, undefined, options); + this.operationType = OperationTypes.deleteOne; } } @@ -180,15 +218,8 @@ export class FindOneAndReplaceOperation extends FindAndModifyOperation { collection: Collection, filter: Document, replacement: Document, - options: FindAndModifyOptions + options: FindOneAndReplaceOptions ) { - // Final options - const finalOptions = Object.assign({}, options); - finalOptions.fields = options.projection; - finalOptions.update = true; - finalOptions.new = options.returnOriginal !== void 0 ? !options.returnOriginal : false; - finalOptions.upsert = options.upsert !== void 0 ? !!options.upsert : false; - if (filter == null || typeof filter !== 'object') { throw new TypeError('Filter parameter must be an object'); } @@ -201,7 +232,8 @@ export class FindOneAndReplaceOperation extends FindAndModifyOperation { throw new TypeError('Replacement document must not contain atomic operators'); } - super(collection, filter, finalOptions.sort, replacement, finalOptions); + super(collection, filter, replacement, options); + this.operationType = OperationTypes.replaceOne; } } @@ -211,16 +243,8 @@ export class FindOneAndUpdateOperation extends FindAndModifyOperation { collection: Collection, filter: Document, update: Document, - options: FindAndModifyOptions + options: FindOneAndUpdateOptions ) { - // Final options - const finalOptions = Object.assign({}, options); - finalOptions.fields = options.projection; - finalOptions.update = true; - finalOptions.new = - typeof options.returnOriginal === 'boolean' ? !options.returnOriginal : false; - finalOptions.upsert = typeof options.upsert === 'boolean' ? options.upsert : false; - if (filter == null || typeof filter !== 'object') { throw new TypeError('Filter parameter must be an object'); } @@ -233,7 +257,8 @@ export class FindOneAndUpdateOperation extends FindAndModifyOperation { throw new TypeError('Update document requires atomic operators'); } - super(collection, filter, finalOptions.sort, update, finalOptions); + super(collection, filter, update, options); + this.operationType = OperationTypes.updateOne; } }