Skip to content

Commit

Permalink
Merge pull request #232 from OpenKitten/feature/findAndModify
Browse files Browse the repository at this point in the history
Initial implementation of findAndModify.
  • Loading branch information
Joannis authored Apr 21, 2020
2 parents 18025ba + e552b4e commit 6fcd238
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 7 deletions.
169 changes: 169 additions & 0 deletions Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift
Original file line number Diff line number Diff line change
@@ -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<Query: MongoKittenQuery>(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<Query: MongoKittenQuery>(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<Query: MongoKittenQuery>(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<Query: MongoKittenQuery>(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<FindAndModifyReply> {
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
}
}
121 changes: 114 additions & 7 deletions Sources/MongoKittenCore/Commands/FindAndModify.swift
Original file line number Diff line number Diff line change
@@ -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 `{ <field1>: <value1>, ...}`, the `findAndModify` performs a replacement.
* Starting in MongoDB 4.2, if passed an aggregation pipeline `[ <stage1>, <stage2>, ... ]`, `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: { <field1>: 1, <field2>: 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
}

0 comments on commit 6fcd238

Please sign in to comment.