diff --git a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift new file mode 100644 index 00000000..7915dc93 --- /dev/null +++ b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift @@ -0,0 +1,169 @@ +import NIO +import MongoClient +import MongoKittenCore + +extension MongoCollection { + // MARK: - Builder Functions (Composable/Chained API) + + /// Modifies and returns a single document. + /// - Parameters: + /// - query: The selection criteria for the modification. + /// - update: If passed a document with update operator expressions, performs the specified modification. If passed a replacement document performs a replacement. + /// - remove: Removes the document specified in the query field. Defaults to `false` + /// - returnValue: Wether to return the `original` or `modified` document. + public func findAndModify(where query: Document, + update document: Document = [:], + remove: Bool = false, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.update = document + command.remove = remove + command.new = returnValue == .modified + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Deletes a single document based on the query, returning the deleted document. + /// - Parameters: + /// - query: The selection criteria for the deletion. + public func findOneAndDelete(where query: Document) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.remove = true + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Replaces a single document based on the specified query. + /// - Parameters: + /// - query: The selection criteria for the upate. + /// - replacement: The replacement document. + /// - returnValue: Wether to return the `original` or `modified` document. + public func findOneAndReplace(where query: Document, + replacement document: Document, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Updates a single document based on the specified query. + /// - Parameters: + /// - query: The selection criteria for the upate. + /// - document: The update document. + /// - returnValue: Wether to return the `original` or `modified` document. + public func findOneAndUpdate(where query: Document, + to document: Document, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Modifies and returns a single document. + /// - Parameters: + /// - query: The selection criteria for the modification. + /// - update: If passed a document with update operator expressions, performs the specified modification. If passed a replacement document performs a replacement. + /// - remove: Removes the document specified in the query field. Defaults to `false` + /// - returnValue: Wether to return the `original` or `modified` document. + public func findAndModify(where query: Query, + update document: Document = [:], + remove: Bool = false, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query.makeDocument()) + command.update = document + command.remove = remove + command.new = returnValue == .modified + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Deletes a single document based on the query, returning the deleted document. + /// - Parameters: + /// - query: The selection criteria for the deletion. + public func findOneAndDelete(where query: Query) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query.makeDocument()) + command.remove = true + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Replaces a single document based on the specified query. + /// - Parameters: + /// - query: The selection criteria for the upate. + /// - replacement: The replacement document. + /// - returnValue: Wether to return the `original` or `modified` document. + public func findOneAndReplace(where query: Query, + replacement document: Document, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query.makeDocument()) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self) + } + + /// Updates a single document based on the specified query. + /// - Parameters: + /// - query: The selection criteria for the upate. + /// - document: The update document. + /// - returnValue: Wether to return the `original` or `modified` document. + public func findOneAndUpdate(where query: Query, + to document: Document, + returnValue: FindAndModifyReturnValue = .original) -> FindAndModifyBuilder { + var command = FindAndModifyCommand(collection: self.name, query: query.makeDocument()) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self) + } +} + +public final class FindAndModifyBuilder { + /// The underlying command to be executed. + public var command: FindAndModifyCommand + private let collection: MongoCollection + + init(command: FindAndModifyCommand, collection: MongoCollection) { + self.command = command + self.collection = collection + } + + /// Executes the command + public func execute() -> EventLoopFuture { + return collection.pool.next(for: .basic).flatMap { connection in + connection.executeCodable(self.command, + namespace: self.collection.database.commandNamespace, + in: self.collection.transaction, + sessionId: self.collection.sessionId ?? connection.implicitSessionId) + + } + .decode(FindAndModifyReply.self) + ._mongoHop(to: self.collection.hoppedEventLoop) + } + + public func sort(_ sort: Sort) -> FindAndModifyBuilder { + self.command.sort = sort.document + return self + } + + public func sort(_ sort: Document) -> FindAndModifyBuilder { + self.command.sort = sort + return self + } + + public func project(_ projection: Projection) -> FindAndModifyBuilder { + self.command.fields = projection.document + return self + } + + public func project(_ projection: Document) -> FindAndModifyBuilder { + self.command.fields = projection + return self + } + + public func writeConcern(_ concern: WriteConcern) -> FindAndModifyBuilder { + self.command.writeConcern = concern + return self + } + + public func collation(_ collation: Collation) -> FindAndModifyBuilder { + self.command.collation = collation + return self + } +} diff --git a/Sources/MongoKittenCore/Commands/FindAndModify.swift b/Sources/MongoKittenCore/Commands/FindAndModify.swift index a00b1167..97e703f8 100644 --- a/Sources/MongoKittenCore/Commands/FindAndModify.swift +++ b/Sources/MongoKittenCore/Commands/FindAndModify.swift @@ -1,8 +1,115 @@ -// -// File.swift -// -// -// Created by Joannis Orlandos on 21/06/2019. -// +import MongoCore -import Foundation +public struct FindAndModifyCommand: Codable { + /// The collection against which to run the command. + public private(set) var findAndModify: String + /// The selection criteria for the modification. + public var query: Document? + /// Determines which document the operation modifies if the query selects multiple documents. `findAndModify` modifies the first document in the sort order specified by this argument. + public var sort: Document? + /// Removes the document specified in the `query` field. Set this to `true` to remove the selected document . The default is `false`. + public var remove: Bool + /** + Performs an update of the selected document. + + * If passed a document with update operator expressions, `findAndModify` performs the specified modification. + * If passed a replacement document `{ : , ...}`, the `findAndModify` performs a replacement. + * Starting in MongoDB 4.2, if passed an aggregation pipeline `[ , , ... ]`, `findAndModify` modifies the document per the pipeline. The pipeline can consist of the following stages: + * `$addFields` and its alias `$set` + * `$project` and its alias `$unset` + * `$replaceRoot` and its alias `$replcaeWith` + */ + public var update: Document = [] + /// When true, returns the modified document rather than the original. The findAndModify method ignores the new option for remove operations. + public var new: Bool? + /// A subset of fields to return. The `fields` document specifies an inclusion of a field with `1`, as in: `fields: { : 1, : 1, ... }`. [See projection](https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/#read-operations-projection). + public var fields: Document? + /** + Used in conjuction with the update field. + + When true, `findAndModify()` either: + + * Creates a new document if no documents match the `query`. For more details see [upsert behavior](https://docs.mongodb.com/manual/reference/method/db.collection.update/#upsert-behavior). + * Updates a single document that matches `query`. + + To avoid multiple upserts, ensure that the query fields are uniquely indexed. + */ + public var upsert: Bool? + /// Enables findAndModify to bypass document validation during the operation. This lets you update documents that do not meet the validation requirements. + public var bypassDocumentValidation: Bool? + /** + A document expressing the write concern. Omit to use the default write concern. + + Do not explicitly set the write concern for the operation if run in a transaction. To use write concern with transactions, see [Transactions and Write Concern](https://docs.mongodb.com/manual/core/transactions/#transactions-write-concern). + */ + public var writeConcern: WriteConcern? + /// Specifies a time limit in milliseconds for processing the operation. + public var maxTimeMS: Int? + /// Specifies the collation to use for the operation. + public var collation: Collation? + /// An array of filter documents that determine which array elements to modify for an update operation on an array field. + public var arrayFilters: [Document]? + + public init(collection: String, + query: Document? = nil, + sort: Document? = nil, + remove: Bool = false, + update: Document = [], + new: Bool? = nil, + fields: Document? = nil, + upsert: Bool? = nil, + bypassDocumentValidation: Bool? = nil, + writeConcern: WriteConcern? = nil, + maxTimeMS: Int? = nil, + collation: Collation? = nil, + arrayFilters: [Document]? = nil) { + self.findAndModify = collection + self.query = query + self.sort = sort + self.remove = remove + self.update = update + self.new = new + self.fields = fields + self.upsert = upsert + self.bypassDocumentValidation = bypassDocumentValidation + self.writeConcern = writeConcern + self.maxTimeMS = maxTimeMS + self.collation = collation + self.arrayFilters = arrayFilters + } +} + +public struct FindAndModifyReply: Codable, Error { + private enum CodingKeys: String, CodingKey { + case ok + case value + case lastErrorObject + } + + /// Contains the command’s execution status. `1` on success, or `0` if an error occurred. + public let ok: Int + /** + Contains the command’s returned value. + + For `remove` operations, `value` contains the removed document if the query matches a document. If the query does not match a document to remove, `value` contains `nil`. + For update operations, the value embedded document contains the following: + * If the `new` parameter is not set or is `false`: + * the pre-modification document if the query matches a document; + * otherwise, `nil`. + + * if `new` is `true`: + * the modified document if the query returns a match; + * the inserted document if `upsert: true` and no document matches the query; + * otherwise, `nil`. + */ + public let value: Document? + /// Contains information about updated documents. + public let lastErrorObject: Document? +} + +public enum FindAndModifyReturnValue: String, Codable { + /// Return the modified Document. + case modified + /// Return the original Document. + case original +}