From 1b4ed4ce440387d84a08f8c420853f93ddda8e97 Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Wed, 8 Apr 2020 00:29:34 -0400 Subject: [PATCH 1/9] Initial implementation of findAndModify. --- .../Collection+FindAndModify.swift | 176 ++++++++++++++++++ .../Commands/FindAndModify.swift | 121 +++++++++++- 2 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift diff --git a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift new file mode 100644 index 00000000..5a5d9305 --- /dev/null +++ b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift @@ -0,0 +1,176 @@ +import NIO +import MongoClient +import MongoKittenCore + +extension MongoCollection { + + // MARK: - Convenience Functions (Simple API) + + /// Modifies and returns a single document. + /// - Parameters: + /// - where: 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) -> EventLoopFuture { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.update = document + command.remove = remove + command.new = returnValue == .modified + return FindAndModifyBuilder(command: command, collection: self).execute() + } + + /// 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) -> EventLoopFuture { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.remove = true + return FindAndModifyBuilder(command: command, collection: self) + .execute() + .map { $0.value } + } + + /// 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) -> EventLoopFuture { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self).execute() + } + + /// 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) -> EventLoopFuture { + var command = FindAndModifyCommand(collection: self.name, query: query) + command.new = returnValue == .modified + command.update = document + return FindAndModifyBuilder(command: command, collection: self) + .execute() + .map { $0.value } + } + + // MARK: - Builder Functions (Composable/Chained API) + + /// Modifies and returns a single document. + /// - Parameters: + /// - where: 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) + } +} + +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..1927e3eb 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 { + 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 +} From 19a3590a9e2358a4d46abdf531e54c7b360ab37a Mon Sep 17 00:00:00 2001 From: JoachimM Date: Thu, 9 Apr 2020 16:17:45 +0200 Subject: [PATCH 2/9] WIP: First start of adding some documentation --- Sources/MongoKitten/Aggregate.swift | 31 ++++++ Sources/MongoKitten/AggregateBuilder.swift | 104 +++++++++++++++++++++ Sources/MongoKitten/AggregateStage.swift | 83 ++++++++++++++++ 3 files changed, 218 insertions(+) diff --git a/Sources/MongoKitten/Aggregate.swift b/Sources/MongoKitten/Aggregate.swift index 6690828c..ccaf0eba 100644 --- a/Sources/MongoKitten/Aggregate.swift +++ b/Sources/MongoKitten/Aggregate.swift @@ -3,6 +3,37 @@ import MongoKittenCore import MongoClient extension MongoCollection { + /// The `aggregate` command will create an `AggregateBuilderPipeline` where data can be aggregated + /// and be transformed in multiple `AggregateStage` operations + /// + /// # Hint: + /// With Swift > 5.1 you can also use the function builders. See the documentation at `buildAggregate` + /// + /// # Example: + /// ``` + /// let pipeline = collection.aggregate([ + /// .match("name" == "Superman"), + /// .unwind(fieldPath: "$arrayItem") + /// ]) + /// + /// pipeline.decode(SomeDecodableType.self).forEach { yourStruct in + /// // do sth. with your struct + /// }.whenFailure { error in + /// // do sth. with the error + /// } + /// ``` + /// + /// The same example with function builders: + /// + /// ``` + /// let pipeline = collection.buildAggregate { + /// match("name" == "Superman") + /// unwind(fieldPath: "$arrayItem") + /// } + /// ``` + /// + /// - Parameter stages: an array of `AggregateBuilderStage`. + /// - Returns: returns an `AggregateBuilderPipeline` public func aggregate(_ stages: [AggregateBuilderStage]) -> AggregateBuilderPipeline { var pipeline = AggregateBuilderPipeline(stages: stages) pipeline.collection = self diff --git a/Sources/MongoKitten/AggregateBuilder.swift b/Sources/MongoKitten/AggregateBuilder.swift index b2585395..8d5ba143 100644 --- a/Sources/MongoKitten/AggregateBuilder.swift +++ b/Sources/MongoKitten/AggregateBuilder.swift @@ -52,6 +52,27 @@ public struct AggregateBuilder { } extension MongoCollection { + /// The `aggregate` command will create an `AggregateBuilderPipeline` where data can be aggregated + /// and be transformed in multiple `AggregateStage` operations + /// + /// With Swift > 5.1 you can use the function builders instead of the `aggregate(_ stages: [AggregateBuilderStage]) -> AggregateBuilderPipeline` function. + /// + /// # Example: + /// ``` + /// let pipeline = collection.buildAggregate { + /// match("name" == "Superman") + /// unwind(fieldPath: "$arrayItem") + /// } + /// + /// pipeline.decode(SomeDecodableType.self).forEach { yourStruct in + /// // do sth. with your struct + /// }.whenFailure { error in + /// // do sth. with the error + /// } + /// ``` + /// + /// - Parameter build: the `AggregateBuilderStage` as function builders + /// - Returns: returns an `AggregateBuilderPipeline` public func buildAggregate(@AggregateBuilder build: () -> AggregateBuilderStage) -> AggregateBuilderPipeline { var pipeline = AggregateBuilderPipeline(stages: [build()]) pipeline.collection = self @@ -75,6 +96,21 @@ public func skip(_ n: Int) -> AggregateBuilderStage { return .skip(n) } +/// The `limit` aggregation limits the number of resulting documents to the given number +/// +/// # MongoDB-Documentation: +/// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/) +/// +/// # Example: +/// ``` +/// let pipeline = myCollection.aggregate([.match("myCondition" == true), .limit(5)]) +/// pipeline.execute().whenComplete { result in +/// ... +/// } +/// ``` +/// +/// - Parameter n: the maximum number of documents +/// - Returns: returns an `AggregateBuilderStage` public func limit(_ n: Int) -> AggregateBuilderStage { return .limit(n) } @@ -97,6 +133,41 @@ public func project(_ fields: String...) -> AggregateBuilderStage { return .project(projection) } +/// The `lookup` aggregation performs a join from another collection in the same database. This aggregation will add a new array to +/// your document including the matching documents. +/// +/// # MongoDB-Documentation: +/// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/) +/// +/// # Example: +/// There are two collections, named `users` and `userCategories`. In the `users` collection there is a reference to the _id +/// of the `userCategories`, because every user belongs to a category. +/// +/// If you now want to aggregate all users and the corresponding user category, you can use the `$lookup` like this: +/// +/// ``` +/// let pipeline = userCollection.aggregate([.lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory")]) +/// pipeline.execute().whenComplete { result in +/// ... +/// } +/// ``` +/// +/// # Hint: +/// Because the matched documents will be inserted as an array no matter if there is only one item or more, you may want to unwind the joined documents: +/// +/// ``` +/// let pipeline = myCollection.aggregate([ +/// .lookup(from: ..., newName: "newName"), +/// .unwind(fieldPath: "$newName") +/// ]) +/// ``` +/// +/// - Parameters: +/// - from: the foreign collection, where the documents will be looked up +/// - localField: the name of the field in the input collection that shall match the `foreignField` in the `from` collection +/// - foreignField: the name of the field in the `fromCollection` that shall match the `localField` in the input collection +/// - newName: the collecting matches will be inserted as an array to the input collection, named as `newName` +/// - Returns: returns an `AggregateBuilderStage` public func lookup( from: String, localField: String, @@ -111,6 +182,39 @@ public func lookup( ) } +/// The `unwind` aggregation will deconstruct a field, that contains an array. It will return as many documents as are included +/// in the array and every output includes the original document with each item of the array +/// +/// # MongoDB-Documentation: +/// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/) +/// +/// # Example: +/// The original document: +/// +/// ``` +/// { "_id": 1, "boolItem": true, "arrayItem": ["a", "b", "c"] } +/// ``` +/// +/// The command in Swift: +/// +/// ``` +/// let pipeline = collection.aggregate([ +/// .match("_id" == 1), +/// .unwind(fieldPath: "$arrayItem") +/// ]) +/// ``` +/// +/// This will return three documents: +/// ``` +/// { "_id": 1, "boolItem": true, "arrayItem": "a" } +/// { "_id": 1, "boolItem": true, "arrayItem": "b" } +/// { "_id": 1, "boolItem": true, "arrayItem": "c" } +/// ``` +/// - Parameters: +/// - fieldPath: the field path to an array field. You have to prefix the path with "$" +/// - includeArrayIndex: this parameter is optional. If given, the new documents will hold a new field with the name of `includeArrayIndex` and this field will contain the array index +/// - preserveNullAndEmptyArrays: this parameter is optional. If it is set to `true`, the aggregation will also include the documents, that don't have an array that can be unwinded. default is `false`, so the `unwind` aggregation will remove all documents, where there is no value or an empty array at `fieldPath` +/// - Returns: returns an `AggregateBuilderStage` public func unwind( fieldPath: String, includeArrayIndex: String? = nil, diff --git a/Sources/MongoKitten/AggregateStage.swift b/Sources/MongoKitten/AggregateStage.swift index 966dfc95..9ac6fb41 100644 --- a/Sources/MongoKitten/AggregateStage.swift +++ b/Sources/MongoKitten/AggregateStage.swift @@ -66,6 +66,21 @@ public struct AggregateBuilderStage { ]) } + /// The `limit` aggregation limits the number of resulting documents to the given number + /// + /// # MongoDB-Documentation: + /// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/) + /// + /// # Example: + /// ``` + /// let pipeline = myCollection.aggregate([.match("myCondition" == true), .limit(5)]) + /// pipeline.execute().whenComplete { result in + /// ... + /// } + /// ``` + /// + /// - Parameter n: the maximum number of documents + /// - Returns: returns an `AggregateBuilderStage` public static func limit(_ n: Int) -> AggregateBuilderStage { assert(n > 0) @@ -82,6 +97,41 @@ public struct AggregateBuilderStage { ]) } + /// The `lookup` aggregation performs a join from another collection in the same database. This aggregation will add a new array to + /// your document including the matching documents. + /// + /// # MongoDB-Documentation: + /// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/) + /// + /// # Example: + /// There are two collections, named `users` and `userCategories`. In the `users` collection there is a reference to the _id + /// of the `userCategories`, because every user belongs to a category. + /// + /// If you now want to aggregate all users and the corresponding user category, you can use the `$lookup` like this: + /// + /// ``` + /// let pipeline = userCollection.aggregate([.lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory")]) + /// pipeline.execute().whenComplete { result in + /// ... + /// } + /// ``` + /// + /// # Hint: + /// Because the matched documents will be inserted as an array no matter if there is only one item or more, you may want to unwind the joined documents: + /// + /// ``` + /// let pipeline = myCollection.aggregate([ + /// .lookup(from: ..., newName: "newName"), + /// .unwind(fieldPath: "$newName") + /// ]) + /// ``` + /// + /// - Parameters: + /// - from: the foreign collection, where the documents will be looked up + /// - localField: the name of the field in the input collection that shall match the `foreignField` in the `from` collection + /// - foreignField: the name of the field in the `fromCollection` that shall match the `localField` in the input collection + /// - newName: the collecting matches will be inserted as an array to the input collection, named as `newName` + /// - Returns: returns an `AggregateBuilderStage` public static func lookup( from: String, localField: String, @@ -98,6 +148,39 @@ public struct AggregateBuilderStage { ]) } + /// The `unwind` aggregation will deconstruct a field, that contains an array. It will return as many documents as are included + /// in the array and every output includes the original document with each item of the array + /// + /// # MongoDB-Documentation: + /// [Link to the MongoDB-Documentation](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/) + /// + /// # Example: + /// The original document: + /// + /// ``` + /// { "_id": 1, "boolItem": true, "arrayItem": ["a", "b", "c"] } + /// ``` + /// + /// The command in Swift: + /// + /// ``` + /// let pipeline = collection.aggregate([ + /// .match("_id" == 1), + /// .unwind(fieldPath: "$arrayItem") + /// ]) + /// ``` + /// + /// This will return three documents: + /// ``` + /// { "_id": 1, "boolItem": true, "arrayItem": "a" } + /// { "_id": 1, "boolItem": true, "arrayItem": "b" } + /// { "_id": 1, "boolItem": true, "arrayItem": "c" } + /// ``` + /// - Parameters: + /// - fieldPath: the field path to an array field. You have to prefix the path with "$" + /// - includeArrayIndex: this parameter is optional. If given, the new documents will hold a new field with the name of `includeArrayIndex` and this field will contain the array index + /// - preserveNullAndEmptyArrays: this parameter is optional. If it is set to `true`, the aggregation will also include the documents, that don't have an array that can be unwinded. default is `false`, so the `unwind` aggregation will remove all documents, where there is no value or an empty array at `fieldPath` + /// - Returns: returns an `AggregateBuilderStage` public static func unwind( fieldPath: String, includeArrayIndex: String? = nil, From eba7228ea3434ee68ffe739064b2106e17a7a49b Mon Sep 17 00:00:00 2001 From: JoachimM Date: Thu, 9 Apr 2020 16:28:26 +0200 Subject: [PATCH 3/9] improved the readability of the doc --- Sources/MongoKitten/AggregateBuilder.swift | 15 +++++++++++---- Sources/MongoKitten/AggregateStage.swift | 11 +++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Sources/MongoKitten/AggregateBuilder.swift b/Sources/MongoKitten/AggregateBuilder.swift index 8d5ba143..0d5ff5af 100644 --- a/Sources/MongoKitten/AggregateBuilder.swift +++ b/Sources/MongoKitten/AggregateBuilder.swift @@ -103,9 +103,13 @@ public func skip(_ n: Int) -> AggregateBuilderStage { /// /// # Example: /// ``` -/// let pipeline = myCollection.aggregate([.match("myCondition" == true), .limit(5)]) +/// let pipeline = myCollection.aggregate([ +/// .match("myCondition" == true), +/// .limit(5) +/// ]) +/// /// pipeline.execute().whenComplete { result in -/// ... +/// // ... /// } /// ``` /// @@ -146,9 +150,12 @@ public func project(_ fields: String...) -> AggregateBuilderStage { /// If you now want to aggregate all users and the corresponding user category, you can use the `$lookup` like this: /// /// ``` -/// let pipeline = userCollection.aggregate([.lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory")]) +/// let pipeline = userCollection.aggregate([ +/// .lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory") +/// ]) +/// /// pipeline.execute().whenComplete { result in -/// ... +/// // ... /// } /// ``` /// diff --git a/Sources/MongoKitten/AggregateStage.swift b/Sources/MongoKitten/AggregateStage.swift index 9ac6fb41..92add050 100644 --- a/Sources/MongoKitten/AggregateStage.swift +++ b/Sources/MongoKitten/AggregateStage.swift @@ -73,7 +73,11 @@ public struct AggregateBuilderStage { /// /// # Example: /// ``` - /// let pipeline = myCollection.aggregate([.match("myCondition" == true), .limit(5)]) + /// let pipeline = myCollection.aggregate([ + /// .match("myCondition" == true), + /// .limit(5) + /// ]) + /// /// pipeline.execute().whenComplete { result in /// ... /// } @@ -110,7 +114,10 @@ public struct AggregateBuilderStage { /// If you now want to aggregate all users and the corresponding user category, you can use the `$lookup` like this: /// /// ``` - /// let pipeline = userCollection.aggregate([.lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory")]) + /// let pipeline = userCollection.aggregate([ + /// .lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory") + /// ]) + /// /// pipeline.execute().whenComplete { result in /// ... /// } From e3bc475f034b4e0d33777d2bf3eba60eb266384d Mon Sep 17 00:00:00 2001 From: JoachimM Date: Thu, 9 Apr 2020 16:53:36 +0200 Subject: [PATCH 4/9] changed the example of aggregate and buildAggregate and changed the return --- Sources/MongoKitten/Aggregate.swift | 5 +++-- Sources/MongoKitten/AggregateBuilder.swift | 11 ++++++----- Sources/MongoKitten/AggregateStage.swift | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Sources/MongoKitten/Aggregate.swift b/Sources/MongoKitten/Aggregate.swift index ccaf0eba..7659b7fb 100644 --- a/Sources/MongoKitten/Aggregate.swift +++ b/Sources/MongoKitten/Aggregate.swift @@ -13,7 +13,8 @@ extension MongoCollection { /// ``` /// let pipeline = collection.aggregate([ /// .match("name" == "Superman"), - /// .unwind(fieldPath: "$arrayItem") + /// .lookup(from: "addresses", "localField": "_id", "foreignField": "superheroID", newName: "address"), + /// .unwind(fieldPath: "$address") /// ]) /// /// pipeline.decode(SomeDecodableType.self).forEach { yourStruct in @@ -33,7 +34,7 @@ extension MongoCollection { /// ``` /// /// - Parameter stages: an array of `AggregateBuilderStage`. - /// - Returns: returns an `AggregateBuilderPipeline` + /// - Returns: an `AggregateBuilderPipeline` that should be executed to get results public func aggregate(_ stages: [AggregateBuilderStage]) -> AggregateBuilderPipeline { var pipeline = AggregateBuilderPipeline(stages: stages) pipeline.collection = self diff --git a/Sources/MongoKitten/AggregateBuilder.swift b/Sources/MongoKitten/AggregateBuilder.swift index 0d5ff5af..6956654a 100644 --- a/Sources/MongoKitten/AggregateBuilder.swift +++ b/Sources/MongoKitten/AggregateBuilder.swift @@ -61,7 +61,8 @@ extension MongoCollection { /// ``` /// let pipeline = collection.buildAggregate { /// match("name" == "Superman") - /// unwind(fieldPath: "$arrayItem") + /// lookup(from: "addresses", "localField": "_id", "foreignField": "superheroID", newName: "address") + /// unwind(fieldPath: "$address") /// } /// /// pipeline.decode(SomeDecodableType.self).forEach { yourStruct in @@ -72,7 +73,7 @@ extension MongoCollection { /// ``` /// /// - Parameter build: the `AggregateBuilderStage` as function builders - /// - Returns: returns an `AggregateBuilderPipeline` + /// - Returns: an `AggregateBuilderPipeline` that should be executed to get results public func buildAggregate(@AggregateBuilder build: () -> AggregateBuilderStage) -> AggregateBuilderPipeline { var pipeline = AggregateBuilderPipeline(stages: [build()]) pipeline.collection = self @@ -114,7 +115,7 @@ public func skip(_ n: Int) -> AggregateBuilderStage { /// ``` /// /// - Parameter n: the maximum number of documents -/// - Returns: returns an `AggregateBuilderStage` +/// - Returns: an `AggregateBuilderStage` public func limit(_ n: Int) -> AggregateBuilderStage { return .limit(n) } @@ -174,7 +175,7 @@ public func project(_ fields: String...) -> AggregateBuilderStage { /// - localField: the name of the field in the input collection that shall match the `foreignField` in the `from` collection /// - foreignField: the name of the field in the `fromCollection` that shall match the `localField` in the input collection /// - newName: the collecting matches will be inserted as an array to the input collection, named as `newName` -/// - Returns: returns an `AggregateBuilderStage` +/// - Returns: an `AggregateBuilderStage` public func lookup( from: String, localField: String, @@ -221,7 +222,7 @@ public func lookup( /// - fieldPath: the field path to an array field. You have to prefix the path with "$" /// - includeArrayIndex: this parameter is optional. If given, the new documents will hold a new field with the name of `includeArrayIndex` and this field will contain the array index /// - preserveNullAndEmptyArrays: this parameter is optional. If it is set to `true`, the aggregation will also include the documents, that don't have an array that can be unwinded. default is `false`, so the `unwind` aggregation will remove all documents, where there is no value or an empty array at `fieldPath` -/// - Returns: returns an `AggregateBuilderStage` +/// - Returns: an `AggregateBuilderStage` public func unwind( fieldPath: String, includeArrayIndex: String? = nil, diff --git a/Sources/MongoKitten/AggregateStage.swift b/Sources/MongoKitten/AggregateStage.swift index 92add050..e3aa80bd 100644 --- a/Sources/MongoKitten/AggregateStage.swift +++ b/Sources/MongoKitten/AggregateStage.swift @@ -84,7 +84,7 @@ public struct AggregateBuilderStage { /// ``` /// /// - Parameter n: the maximum number of documents - /// - Returns: returns an `AggregateBuilderStage` + /// - Returns: an `AggregateBuilderStage` public static func limit(_ n: Int) -> AggregateBuilderStage { assert(n > 0) @@ -117,7 +117,7 @@ public struct AggregateBuilderStage { /// let pipeline = userCollection.aggregate([ /// .lookup(from: "userCategories", "localField": "categoryID", "foreignField": "_id", newName: "userCategory") /// ]) - /// + /// /// pipeline.execute().whenComplete { result in /// ... /// } @@ -138,7 +138,7 @@ public struct AggregateBuilderStage { /// - localField: the name of the field in the input collection that shall match the `foreignField` in the `from` collection /// - foreignField: the name of the field in the `fromCollection` that shall match the `localField` in the input collection /// - newName: the collecting matches will be inserted as an array to the input collection, named as `newName` - /// - Returns: returns an `AggregateBuilderStage` + /// - Returns: an `AggregateBuilderStage` public static func lookup( from: String, localField: String, @@ -187,7 +187,7 @@ public struct AggregateBuilderStage { /// - fieldPath: the field path to an array field. You have to prefix the path with "$" /// - includeArrayIndex: this parameter is optional. If given, the new documents will hold a new field with the name of `includeArrayIndex` and this field will contain the array index /// - preserveNullAndEmptyArrays: this parameter is optional. If it is set to `true`, the aggregation will also include the documents, that don't have an array that can be unwinded. default is `false`, so the `unwind` aggregation will remove all documents, where there is no value or an empty array at `fieldPath` - /// - Returns: returns an `AggregateBuilderStage` + /// - Returns: an `AggregateBuilderStage` public static func unwind( fieldPath: String, includeArrayIndex: String? = nil, From 6a76862dbd6a73cd054370dfd64c70739c460491 Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Fri, 10 Apr 2020 11:43:10 -0400 Subject: [PATCH 5/9] Removed convenience functions. --- .../Collection+FindAndModify.swift | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift index 5a5d9305..5fcccddb 100644 --- a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift +++ b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift @@ -3,67 +3,6 @@ import MongoClient import MongoKittenCore extension MongoCollection { - - // MARK: - Convenience Functions (Simple API) - - /// Modifies and returns a single document. - /// - Parameters: - /// - where: 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) -> EventLoopFuture { - var command = FindAndModifyCommand(collection: self.name, query: query) - command.update = document - command.remove = remove - command.new = returnValue == .modified - return FindAndModifyBuilder(command: command, collection: self).execute() - } - - /// 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) -> EventLoopFuture { - var command = FindAndModifyCommand(collection: self.name, query: query) - command.remove = true - return FindAndModifyBuilder(command: command, collection: self) - .execute() - .map { $0.value } - } - - /// 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) -> EventLoopFuture { - var command = FindAndModifyCommand(collection: self.name, query: query) - command.new = returnValue == .modified - command.update = document - return FindAndModifyBuilder(command: command, collection: self).execute() - } - - /// 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) -> EventLoopFuture { - var command = FindAndModifyCommand(collection: self.name, query: query) - command.new = returnValue == .modified - command.update = document - return FindAndModifyBuilder(command: command, collection: self) - .execute() - .map { $0.value } - } - // MARK: - Builder Functions (Composable/Chained API) /// Modifies and returns a single document. From 46de69d309c51eb6f8d95ab1566d1e22c7c51e78 Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Fri, 10 Apr 2020 11:47:12 -0400 Subject: [PATCH 6/9] Conformed FindAndModifyReply to Error. --- Sources/MongoKittenCore/Commands/FindAndModify.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MongoKittenCore/Commands/FindAndModify.swift b/Sources/MongoKittenCore/Commands/FindAndModify.swift index 1927e3eb..97e703f8 100644 --- a/Sources/MongoKittenCore/Commands/FindAndModify.swift +++ b/Sources/MongoKittenCore/Commands/FindAndModify.swift @@ -79,7 +79,7 @@ public struct FindAndModifyCommand: Codable { } } -public struct FindAndModifyReply: Codable { +public struct FindAndModifyReply: Codable, Error { private enum CodingKeys: String, CodingKey { case ok case value From a429ce016d1d657b74aa5bcb697271a633226864 Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Sat, 11 Apr 2020 21:45:58 -0400 Subject: [PATCH 7/9] Added fam test. --- .../ConnectionSettingsTests.swift | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Tests/MongoKittenTests/ConnectionSettingsTests.swift b/Tests/MongoKittenTests/ConnectionSettingsTests.swift index eceab543..250b39fa 100644 --- a/Tests/MongoKittenTests/ConnectionSettingsTests.swift +++ b/Tests/MongoKittenTests/ConnectionSettingsTests.swift @@ -36,4 +36,52 @@ class ConnectionSettingsTests : XCTestCase { } } } + + func test_findAndModify() throws { + + do { + let mongoSettings = try ConnectionSettings("mongodb://localhost:27017/helixtest") + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let mongodb = try MongoDatabase.lazyConnect(settings: mongoSettings, on: elg) + + let db = mongodb + + // insert dummy data otherwise mongo won't know that transactions aren't supported because it hasn't found a host just yet since no commands were executed. + let r = try db["queue_collection"].insert(["email": "nova@helixbooking.com","phone": "12345678900"]).wait() + + // Start transaction + let transactionDatabase = try db.startTransaction(autoCommitChanges: false) + // Acquire read lock on dummy data for duration of transaction + let readLockedDocument = try transactionDatabase["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], + to: ["$set": ["readLock": ["id": ObjectId()] as Document]], + returnValue: .modified).execute().wait() + + // Attempt to modify the document (should fail because I have a read lock on the document) + let nonTransactionQuery = try db["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], + to: ["$set": ["phone": "1111111111"]], + returnValue: .modified) + .execute() + .wait() + + print(nonTransactionQuery) + + // Update the document that we have a read lock on. + let original = try transactionDatabase["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], + to: ["$set": ["phone": "0000000000"]], + returnValue: .original).execute().wait() + // verify that the document was not mutated outside the transaction + print("Original document after transaction update.") + print(original) + // Commit the transaction + try transactionDatabase.commit().wait() + + // Fetch the newly modified document to verify it was mutated after the transaction was commited + let res = try mongodb["queue_collection"].findOne().wait() + print("Modified document after transaction update.") + print(res) + } catch { + print(error) + } + } + } From ae952af501e2de9a1ceb2c81f6152d7b8bd3b71e Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Wed, 15 Apr 2020 21:59:08 -0400 Subject: [PATCH 8/9] Removed debugging tests. --- .../ConnectionSettingsTests.swift | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/Tests/MongoKittenTests/ConnectionSettingsTests.swift b/Tests/MongoKittenTests/ConnectionSettingsTests.swift index 250b39fa..eceab543 100644 --- a/Tests/MongoKittenTests/ConnectionSettingsTests.swift +++ b/Tests/MongoKittenTests/ConnectionSettingsTests.swift @@ -36,52 +36,4 @@ class ConnectionSettingsTests : XCTestCase { } } } - - func test_findAndModify() throws { - - do { - let mongoSettings = try ConnectionSettings("mongodb://localhost:27017/helixtest") - let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let mongodb = try MongoDatabase.lazyConnect(settings: mongoSettings, on: elg) - - let db = mongodb - - // insert dummy data otherwise mongo won't know that transactions aren't supported because it hasn't found a host just yet since no commands were executed. - let r = try db["queue_collection"].insert(["email": "nova@helixbooking.com","phone": "12345678900"]).wait() - - // Start transaction - let transactionDatabase = try db.startTransaction(autoCommitChanges: false) - // Acquire read lock on dummy data for duration of transaction - let readLockedDocument = try transactionDatabase["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], - to: ["$set": ["readLock": ["id": ObjectId()] as Document]], - returnValue: .modified).execute().wait() - - // Attempt to modify the document (should fail because I have a read lock on the document) - let nonTransactionQuery = try db["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], - to: ["$set": ["phone": "1111111111"]], - returnValue: .modified) - .execute() - .wait() - - print(nonTransactionQuery) - - // Update the document that we have a read lock on. - let original = try transactionDatabase["queue_collection"].findOneAndUpdate(where: ["email": "nova@helixbooking.com"], - to: ["$set": ["phone": "0000000000"]], - returnValue: .original).execute().wait() - // verify that the document was not mutated outside the transaction - print("Original document after transaction update.") - print(original) - // Commit the transaction - try transactionDatabase.commit().wait() - - // Fetch the newly modified document to verify it was mutated after the transaction was commited - let res = try mongodb["queue_collection"].findOne().wait() - print("Modified document after transaction update.") - print(res) - } catch { - print(error) - } - } - } From e552b4e11f894c17ab8f6cb06886c6c1325ce32c Mon Sep 17 00:00:00 2001 From: Andrew Edwards Date: Wed, 15 Apr 2020 23:28:36 -0400 Subject: [PATCH 9/9] Added mongo query helpers. --- .../Collection+FindAndModify.swift | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift index 5fcccddb..7915dc93 100644 --- a/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift +++ b/Sources/MongoKitten/CollectionHelpers/Collection+FindAndModify.swift @@ -7,7 +7,7 @@ extension MongoCollection { /// Modifies and returns a single document. /// - Parameters: - /// - where: The selection criteria for the modification. + /// - 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. @@ -58,6 +58,60 @@ extension MongoCollection { 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 {