From d00f8c63a30e79bc51ded8be74e00a4cd7532d84 Mon Sep 17 00:00:00 2001 From: Valerio Mazzeo Date: Fri, 28 Dec 2018 12:51:41 +0000 Subject: [PATCH] Initial implementation (#1) --- .gitignore | 69 ++++ .ruby-version | 1 + .swift-version | 1 + .swiftlint.yml | 30 ++ .travis.yml | 57 +++ GEMFILE | 3 + Gemfile.lock | 12 + LICENSE | 21 ++ Package.swift | 7 +- README.md | 10 +- Sources/FluentMongo/BSONCoder.swift | 17 + .../FluentMongo/BSONEncoder+BSONValue.swift | 26 ++ Sources/FluentMongo/Document+Nested.swift | 59 ++++ Sources/FluentMongo/FluentMongoModel.swift | 23 ++ Sources/FluentMongo/FluentMongoProvider.swift | 53 +++ Sources/FluentMongo/FluentMongoQuery.swift | 203 +++++++++++ .../FluentMongo/FluentMongoQueryAction.swift | 49 +++ .../FluentMongoQueryAggregate.swift | 102 ++++++ .../FluentMongo/FluentMongoQueryData.swift | 79 +++++ .../FluentMongo/FluentMongoQueryFilter.swift | 162 +++++++++ .../FluentMongo/FluentMongoQueryJoin.swift | 59 ++++ Sources/FluentMongo/FluentMongoQueryKey.swift | 70 ++++ .../FluentMongo/FluentMongoQuerySort.swift | 52 +++ Sources/FluentMongo/IndexBuilder.swift | 162 +++++++++ Sources/FluentMongo/Model+Index.swift | 16 + .../FluentMongo/MongoConnection+Connect.swift | 21 ++ Sources/FluentMongo/MongoConnection.swift | 199 +++++++++++ .../MongoDatabase+JoinSupporting.swift | 17 + .../MongoDatabase+KeyedCacheSupporting.swift | 12 + .../MongoDatabase+LogSupporting.swift | 17 + .../MongoDatabase+MigrationSupporting.swift | 23 ++ .../MongoDatabase+QuerySupporting.swift | 69 ++++ Sources/FluentMongo/MongoDatabase.swift | 45 +++ Sources/FluentMongo/MongoDatabaseConfig.swift | 90 +++++ .../FluentMongo/QueryBuilder+Distinct.swift | 19 + Sources/FluentMongo/QueryBuilder+Key.swift | 63 ++++ .../FluentMongoProviderTests.swift | 326 ++++++++++++++++++ Tests/FluentMongoTests/Pet.swift | 38 ++ Tests/FluentMongoTests/PetToy.swift | 30 ++ Tests/FluentMongoTests/Toy.swift | 32 ++ Tests/FluentMongoTests/User.swift | 44 +++ Tests/LinuxMain.swift | 18 + 42 files changed, 2401 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 .swift-version create mode 100644 .swiftlint.yml create mode 100644 .travis.yml create mode 100644 GEMFILE create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 Sources/FluentMongo/BSONCoder.swift create mode 100644 Sources/FluentMongo/BSONEncoder+BSONValue.swift create mode 100644 Sources/FluentMongo/Document+Nested.swift create mode 100644 Sources/FluentMongo/FluentMongoModel.swift create mode 100755 Sources/FluentMongo/FluentMongoProvider.swift create mode 100644 Sources/FluentMongo/FluentMongoQuery.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryAction.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryAggregate.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryData.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryFilter.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryJoin.swift create mode 100644 Sources/FluentMongo/FluentMongoQueryKey.swift create mode 100644 Sources/FluentMongo/FluentMongoQuerySort.swift create mode 100644 Sources/FluentMongo/IndexBuilder.swift create mode 100644 Sources/FluentMongo/Model+Index.swift create mode 100644 Sources/FluentMongo/MongoConnection+Connect.swift create mode 100644 Sources/FluentMongo/MongoConnection.swift create mode 100644 Sources/FluentMongo/MongoDatabase+JoinSupporting.swift create mode 100644 Sources/FluentMongo/MongoDatabase+KeyedCacheSupporting.swift create mode 100644 Sources/FluentMongo/MongoDatabase+LogSupporting.swift create mode 100644 Sources/FluentMongo/MongoDatabase+MigrationSupporting.swift create mode 100644 Sources/FluentMongo/MongoDatabase+QuerySupporting.swift create mode 100644 Sources/FluentMongo/MongoDatabase.swift create mode 100644 Sources/FluentMongo/MongoDatabaseConfig.swift create mode 100644 Sources/FluentMongo/QueryBuilder+Distinct.swift create mode 100644 Sources/FluentMongo/QueryBuilder+Key.swift create mode 100755 Tests/FluentMongoTests/FluentMongoProviderTests.swift create mode 100644 Tests/FluentMongoTests/Pet.swift create mode 100644 Tests/FluentMongoTests/PetToy.swift create mode 100644 Tests/FluentMongoTests/Toy.swift create mode 100644 Tests/FluentMongoTests/User.swift create mode 100644 Tests/LinuxMain.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cff6dbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +*.xcodeproj +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +Package.pins +Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..73462a5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.5.1 diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..bf77d54 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +4.2 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..d977e52 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,30 @@ +disabled_rules: # rule identifiers to exclude from running + - line_length + - identifier_name + - type_name + - large_tuple +function_body_length: 50 +type_name: + max_length: 60 +type_body_length: 400 +file_length: 630 +nesting: + type_level: 2 +function_parameter_count: 6 +cyclomatic_complexity: 20 +included: + - Package.swift + - Sources + - Tests +custom_rules: + vertical_whitespace_return: + includes: "*.swift" + name: "Space above Return" + regex: '\S[^{\s:|in]\n[\t ]+return' + message: "Vertical whitespace required above return statement." + vertical_whitespace_closing_braces: + includes: "*.swift" + regex: '\n[ \t]*\n[ \t]*[)}\]]' + name: "Vertical Whitespace before Closing Braces" + message: "Don't include vertical whitespace (empty line) before closing braces." + severity: warning diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e786146 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,57 @@ +os: + - linux + - osx +language: generic +sudo: required +dist: trusty +osx_image: xcode10.1 +services: docker + +before_install: + - if [ $TRAVIS_OS_NAME == "linux" ]; then + SWIFT_VERSION=$(<.swift-version); + docker pull swift:${SWIFT_VERSION}; + docker pull mongo:3.6.8; + docker run -d -p 27017:27017 mongo:3.6.8; + elif [ $TRAVIS_OS_NAME == "osx" ]; then + rvm use 2.5.1 --install --binary --fuzzy; + ruby --version; + gem install bundler; + bundle --version; + brew update; + brew install mongo-c-driver; + brew install mongodb; + brew services start mongodb; + brew outdated swiftlint || brew upgrade swiftlint; + fi + +install: + - if [ $TRAVIS_OS_NAME == "osx" ]; then + bundle install --jobs=3 --retry=3 --deployment; + fi + +script: + - if [ $TRAVIS_OS_NAME == "osx" ]; then + swift --version; + swift build; + swift build -c release; + swift test; + swiftlint; + else + args="apt-get update + && apt-get -y install cmake + && git clone -b r1.13 https://github.com/mongodb/mongo-c-driver /tmp/libmongoc + && pushd /tmp/libmongoc + && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr + && make -j8 install + && popd + && swift build + && swift build -c release + && swift test"; + docker run --rm --net=host -v $(pwd):/app --workdir /app swift:${SWIFT_VERSION} bash -c "${args}"; + fi + +notifications: + email: + on_success: never + on_failure: change diff --git a/GEMFILE b/GEMFILE new file mode 100644 index 0000000..a0de8e0 --- /dev/null +++ b/GEMFILE @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem 'bundler' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..640de36 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,12 @@ +GEM + remote: https://rubygems.org/ + specs: + +PLATFORMS + ruby + +DEPENDENCIES + bundler + +BUNDLED WITH + 1.17.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e37508e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Asensei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift index ef7795d..6e98dda 100644 --- a/Package.swift +++ b/Package.swift @@ -17,10 +17,11 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/vapor/core.git", .upToNextMajor(from: "3.1.0")), - .package(url: "https://github.com/vapor/fluent.git", .upToNextMajor(from: "3.1.2")) + .package(url: "https://github.com/vapor/fluent.git", .upToNextMajor(from: "3.1.2")), + .package(url: "https://github.com/mongodb/mongo-swift-driver.git", .revision("2610f57ac2c937a039b35e331086e225ea5f0cbf")) ], targets: [ - .target(name: "FluentMongo", dependencies: ["Async", "Fluent"]), - .testTarget(name: "FluentMongoTests", dependencies: ["FluentMongo"]) + .target(name: "FluentMongo", dependencies: ["Async", "Fluent", "MongoSwift"]), + .testTarget(name: "FluentMongoTests", dependencies: ["FluentMongo", "FluentBenchmark"]) ] ) diff --git a/README.md b/README.md index 6b85097..58fc304 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ -# vapor-auth-jwt +# vapor-fluent-mongo ![Swift](https://img.shields.io/badge/swift-4.2-orange.svg) [![Build Status](https://travis-ci.com/asensei/vapor-fluent-mongo.svg?token=eSrCssnzja3G3GciyhUB&branch=master)](https://travis-ci.com/asensei/vapor-fluent-mongo) Mongo driver for Fluent `3.x`. +## Environment Variables + +| Name | Required | Default | Value (e.g.) | Description | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------| +| `FLUENT_MONGO_CONNECTION_URL` | ✔ | `-` | `mongodb://127.0.0.1:27017/vapor` | Mongo connection string. | + ## Getting Started ### Install the MongoDB C Driver @@ -16,7 +22,7 @@ On a Mac, you can install both components at once using [Homebrew](https://brew. On Linux: please follow the [instructions](http://mongoc.org/libmongoc/current/installing.html#building-on-unix) from `libmongoc`'s documentation. Note that the versions provided by your package manager may be too old, in which case you can follow the instructions for building and installing from source. -### Install MongoSwift +### Install FluentMongo *Please follow the instructions in the previous section on installing the MongoDB C Driver before proceeding.* diff --git a/Sources/FluentMongo/BSONCoder.swift b/Sources/FluentMongo/BSONCoder.swift new file mode 100644 index 0000000..89c4b22 --- /dev/null +++ b/Sources/FluentMongo/BSONCoder.swift @@ -0,0 +1,17 @@ +// +// BSONCoder.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 28/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import MongoSwift + +public protocol BSONCoder { + + static var encoder: BSONEncoder { get } + + static var decoder: BSONDecoder { get } +} diff --git a/Sources/FluentMongo/BSONEncoder+BSONValue.swift b/Sources/FluentMongo/BSONEncoder+BSONValue.swift new file mode 100644 index 0000000..de3b98d --- /dev/null +++ b/Sources/FluentMongo/BSONEncoder+BSONValue.swift @@ -0,0 +1,26 @@ +// +// BSONEncoder+BSONValue.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import MongoSwift + +public extension BSONEncoder { + + public func encodeBSONValue(_ value: T) throws -> BSONValue { + switch value { + case let value as BSONValue: + return value + default: + // We can only use BSONEncoder to encode top-level data + let wrappedData = ["value": value] + let document: Document = try self.encode(wrappedData) + + return document["value"] ?? BSONNull() + } + } +} diff --git a/Sources/FluentMongo/Document+Nested.swift b/Sources/FluentMongo/Document+Nested.swift new file mode 100644 index 0000000..f6345af --- /dev/null +++ b/Sources/FluentMongo/Document+Nested.swift @@ -0,0 +1,59 @@ +// +// Document+Nested.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 06/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import MongoSwift + +// Review when https://jira.mongodb.org/browse/SWIFT-273 will be fixed. +public extension Document { + public subscript(keys: [String]) -> BSONValue? { + get { + guard !keys.isEmpty else { + return nil + } + + var value: BSONValue? = self + + for key in keys { + value = (value as? Document)?[key] + } + + return value + } + set { + func setNewValue(for keys: [String], in document: inout Document) { + guard !keys.isEmpty else { + return + } + + guard keys.count > 1 else { + document[keys[0]] = newValue + + return + } + + var path = keys + let component = path.removeFirst() + var next = document[component] as? Document ?? Document() + setNewValue(for: path, in: &next) + document[component] = next + } + + setNewValue(for: keys, in: &self) + } + } + + public subscript(keys: String...) -> BSONValue? { + get { + return self[keys] + } + set { + self[keys] = newValue + } + } +} diff --git a/Sources/FluentMongo/FluentMongoModel.swift b/Sources/FluentMongo/FluentMongoModel.swift new file mode 100644 index 0000000..ef59a90 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoModel.swift @@ -0,0 +1,23 @@ +// +// FluentMongoModel.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 06/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +public protocol FluentMongoModel { + + associatedtype ID: Fluent.ID + + var _id: ID? { get set } +} + +public extension FluentMongoModel where Self: Model { + public static var idKey: WritableKeyPath { + return \._id + } +} diff --git a/Sources/FluentMongo/FluentMongoProvider.swift b/Sources/FluentMongo/FluentMongoProvider.swift new file mode 100755 index 0000000..e5cdf55 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoProvider.swift @@ -0,0 +1,53 @@ +// +// FluentMongoProvider.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Fluent +import Service +import MongoSwift + +public final class FluentMongoProvider: Provider { + + public init() {} + + public func register(_ services: inout Services) throws { + MongoSwift.initialize() + try services.register(FluentProvider()) + try services.register(DatabaseKitProvider()) + services.register(MongoDatabaseConfig.self) + services.register(MongoDatabase.self) + var databases = DatabasesConfig() + databases.add(database: MongoDatabase.self, as: .mongo) + services.register(databases) + } + + public func didBoot(_ worker: Container) throws -> EventLoopFuture { + return .done(on: worker) + } + + /* + deinit { + MongoSwift.cleanup() + } + */ +} + +/// MARK: Services + +extension MongoDatabaseConfig: ServiceType { + /// See `ServiceType.makeService(for:)` + public static func makeService(for worker: Container) throws -> MongoDatabaseConfig { + return try .init() + } +} + +extension MongoDatabase: ServiceType { + /// See `ServiceType.makeService(for:)` + public static func makeService(for worker: Container) throws -> MongoDatabase { + return try .init(config: worker.make()) + } +} diff --git a/Sources/FluentMongo/FluentMongoQuery.swift b/Sources/FluentMongo/FluentMongoQuery.swift new file mode 100644 index 0000000..7970c7c --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQuery.swift @@ -0,0 +1,203 @@ +// +// FluentMongoQuery.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 04/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +// MARK: - Query + +public struct FluentMongoQuery { + + public static let defaultAggregateField: String = "fluentAggregate" + + public var collection: String + public var action: FluentMongoQueryAction + public var keys: [FluentMongoQueryKey] + public var isDistinct: Bool + public var joins: FluentMongoQueryJoin + public var filter: FluentMongoQueryFilter? + public var defaultFilterRelation: FluentMongoQueryFilterRelation + public var data: FluentMongoQueryData? + public var partialData: FluentMongoQueryData? + public var skip: Int64? + public var limit: Int64? + public var sort: FluentMongoQuerySort? + + public init( + collection: String, + action: FluentMongoQueryAction = .find, + keys: [FluentMongoQueryKey] = [], + isDistinct: Bool = false, + joins: FluentMongoQueryJoin = [], + filter: FluentMongoQueryFilter? = nil, + defaultFilterRelation: FluentMongoQueryFilterRelation = .and, + data: FluentMongoQueryData? = nil, + partialData: FluentMongoQueryData? = nil, + skip: Int64? = nil, + limit: Int64? = nil, + sort: FluentMongoQuerySort? = nil + ) { + self.collection = collection + self.action = action + self.keys = keys + self.isDistinct = isDistinct + self.joins = joins + self.filter = filter + self.defaultFilterRelation = defaultFilterRelation + self.data = data + self.partialData = partialData + self.skip = skip + self.limit = limit + self.sort = sort + } + + func projection() -> Document? { + var projection = Document() + for key in self.keys { + guard case .raw(let field) = key else { + continue + } + + projection[field.pathWithNamespace.joined(separator: ".")] = true + } + + guard !projection.isEmpty else { + return nil + } + + projection[self.collection + "._id"] = true + + return projection + } + + func distinct() -> [Document] { + var stages = [Document]() + var group = Document() + var id = Document() + for key in self.keys { + guard case .raw(let field) = key else { + continue + } + // It is not possible to use dots when specifying an id field for $group + id[field.pathWithNamespace.joined(separator: ":")] = "$" + field.pathWithNamespace.joined(separator: ".") + } + + if id.isEmpty { + id[self.collection + ":_id"] = "$" + self.collection + "._id" + } + + group["_id"] = id + group["doc"] = ["$first": "$" + self.collection] as Document + + stages.append(["$group": group]) + stages.append(["$project": [self.collection: "$doc"] as Document]) + + return stages + } + + func aggregates() -> [Document]? { + var aggregates = [Document]() + + for key in self.keys { + guard case .computed(let aggregate, let keys) = key else { + continue + } + + switch aggregate { + case .count: + aggregates.append([aggregate.value: FluentMongoQuery.defaultAggregateField]) + case .group(let accumulator): + var group: Document = ["_id": BSONNull()] + for key in keys { + guard case .raw(let field) = key else { + continue + } + // It seems that fluent only support one aggregated field + group[FluentMongoQuery.defaultAggregateField] = [accumulator.value: "$" + field.pathWithNamespace.joined(separator: ".")] as Document + break + } + + aggregates.append([aggregate.value: group]) + } + } + + return aggregates.isEmpty ? nil : aggregates + } + + func aggregationPipeline() -> [Document] { + var pipeline = [Document]() + + // Namespace root document + pipeline.append(["$project": [self.collection: "$$ROOT"] as Document]) + + // Joins + if !self.joins.isEmpty { + pipeline.append(contentsOf: self.joins) + } + + // Filters + if let filter = self.filter { + pipeline.append(["$match": filter]) + } + + // Projection + if let projection = self.projection() { + pipeline.append(["$project": projection]) + } + + // Distinct + if self.isDistinct { + pipeline.append(contentsOf: self.distinct()) + } + + // Sort + if let sort = self.sort { + pipeline.append(["$sort": sort]) + } + + // Skip + if let skip = self.skip { + pipeline.append(["$skip": skip]) + } + + // Limit + if let limit = self.limit { + pipeline.append(["$limit": limit]) + } + + // Aggregates + if let aggregates = self.aggregates() { + pipeline.append(contentsOf: aggregates) + } else { + // Remove namespace + pipeline.append(["$replaceRoot": ["newRoot": "$" + self.collection] as Document]) + } + + return pipeline + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery { + + public static func query(_ entity: String) -> FluentMongoQuery { + return FluentMongoQuery(collection: entity) + } + + public static func queryEntity(for query: FluentMongoQuery) -> String { + return query.collection + } + + public static func queryRangeApply(lower: Int, upper: Int?, to query: inout Query) { + query.skip = Int64(lower) + + if let upper = upper { + query.limit = Int64(upper - lower) + } + } +} diff --git a/Sources/FluentMongo/FluentMongoQueryAction.swift b/Sources/FluentMongo/FluentMongoQueryAction.swift new file mode 100644 index 0000000..bc7eb04 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryAction.swift @@ -0,0 +1,49 @@ +// +// FluentMongoQueryAction.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +// MARK: - QueryAction + +public enum FluentMongoQueryAction { + case insert + case find + case update + case delete +} + +extension Database where Self: QuerySupporting, Self.QueryAction == FluentMongoQueryAction { + + public static var queryActionCreate: QueryAction { + return .insert + } + + public static var queryActionRead: QueryAction { + return .find + } + + public static var queryActionUpdate: QueryAction { + return .update + } + + public static var queryActionDelete: QueryAction { + return .delete + } + + public static func queryActionIsCreate(_ action: QueryAction) -> Bool { + return action == .insert + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryAction == FluentMongoQueryAction { + + public static func queryActionApply(_ action: QueryAction, to query: inout Query) { + query.action = action + } +} diff --git a/Sources/FluentMongo/FluentMongoQueryAggregate.swift b/Sources/FluentMongo/FluentMongoQueryAggregate.swift new file mode 100644 index 0000000..06169a9 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryAggregate.swift @@ -0,0 +1,102 @@ +// +// FluentMongoQueryAggregate.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +// MARK: - QueryAggregate + +public enum FluentMongoQueryAggregate { + case count + case group(AccumulatorOperator) + + var value: String { + switch self { + case .count: + return "$count" + case .group: + return "$group" + } + } + + public var isCount: Bool { + switch self { + case .count: + return true + default: + return false + } + } + + public var isGroup: Bool { + switch self { + case .group: + return true + default: + return false + } + } + + public enum AccumulatorOperator: String { + /// Returns an array of unique expression values for each group. Order of the array elements is undefined. + case addToSet + /// Returns an average of numerical values. Ignores non-numeric values. + case avg + /// Returns a value from the first document for each group. Order is only defined if the documents are in a defined order. + case first + /// Returns a value from the last document for each group. Order is only defined if the documents are in a defined order. + case last + /// Returns the highest expression value for each group. + case max + /// Returns a document created by combining the input documents for each group. + case mergeObjects + /// Returns the lowest expression value for each group. + case min + /// Returns an array of expression values for each group. + case push + /// Returns the population standard deviation of the input values. + case stdDevPop + /// Returns the sample standard deviation of the input values. + case stdDevSamp + /// Returns a sum of numerical values. Ignores non-numeric values. + case sum + + var value: String { + return "$" + self.rawValue + } + } +} + +extension Database where Self: QuerySupporting, Self.QueryAggregate == FluentMongoQueryAggregate { + public static var queryAggregateCount: FluentMongoQueryAggregate { + return .count + } + + public static var queryAggregateSum: FluentMongoQueryAggregate { + return .group(.sum) + } + + public static var queryAggregateAverage: FluentMongoQueryAggregate { + return .group(.avg) + } + + public static var queryAggregateMinimum: FluentMongoQueryAggregate { + return .group(.min) + } + + public static var queryAggregateMaximum: FluentMongoQueryAggregate { + return .group(.max) + } +} + +extension Database where Self: QuerySupporting, Self.QueryAggregate == FluentMongoQueryAggregate, Self.QueryKey == FluentMongoQueryKey { + + public static func queryAggregate(_ aggregate: QueryAggregate, _ fields: [QueryKey]) -> QueryKey { + return .computed(aggregate, fields) + } +} diff --git a/Sources/FluentMongo/FluentMongoQueryData.swift b/Sources/FluentMongo/FluentMongoQueryData.swift new file mode 100644 index 0000000..0552efe --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryData.swift @@ -0,0 +1,79 @@ +// +// FluentMongoQueryData.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +// MARK: - QueryData + +public typealias FluentMongoQueryData = Document + +extension Database where Self: BSONCoder, Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryData == FluentMongoQueryData, Self.QueryField == FluentMongoQueryField { + + public static func queryDataSet(_ field: QueryField, to data: E, on query: inout Query) { + guard let value: BSONValue = try? Self.encoder.encodeBSONValue(data) else { + return + } + + var document = query.partialData ?? Document() + document[field.path] = value + query.partialData = document + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryData == FluentMongoQueryData { + + public static func queryDataApply(_ data: QueryData, to query: inout Query) { + query.data = data + } +} + +extension Database where Self: BSONCoder, Self: QuerySupporting, Self.Output == FluentMongoOutput { + + public static func queryDecode(_ output: Output, entity: String, as decodable: D.Type, on conn: Connection) -> Future { + do { + return conn.future(try Self.decoder.decode(D.self, from: output)) + } catch { + return conn.future(error: error) + } + } +} + +extension Database where Self: BSONCoder, Self: QuerySupporting, Self.QueryData == FluentMongoQueryData { + + public static func queryEncode(_ encodable: E, entity: String) throws -> QueryData { + return try Self.encoder.encode(encodable) + } +} + +// MARK: - QueryField + +public typealias FluentMongoQueryField = FluentProperty + +extension FluentProperty { + + var pathWithNamespace: [String] { + guard let entity = self.entity else { + return self.path + } + + return [entity] + self.path + } +} + +extension Database where Self: QuerySupporting, Self.QueryField == FluentProperty { + + public static func queryField(_ property: FluentProperty) -> QueryField { + return property + } +} + +// MARL: - Output + +public typealias FluentMongoOutput = Document diff --git a/Sources/FluentMongo/FluentMongoQueryFilter.swift b/Sources/FluentMongo/FluentMongoQueryFilter.swift new file mode 100644 index 0000000..67cd470 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryFilter.swift @@ -0,0 +1,162 @@ +// +// FluentMongoQueryFilter.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +// MARK: - QueryFilter + +public typealias FluentMongoQueryFilter = Document + +extension Database where Self: QuerySupporting, Self.QueryFilter == FluentMongoQueryFilter, Self.QueryField == FluentMongoQueryField, Self.QueryFilterMethod == FluentMongoQueryFilterMethod, Self.QueryFilterValue == FluentMongoQueryFilterValue? { + + public static func queryFilter(_ field: QueryField, _ method: QueryFilterMethod, _ value: QueryFilterValue) -> QueryFilter { + var document = Document() + + let unwrappedValue: BSONValue + + switch method { + case .equal, + .greaterThan, + .greaterThanOrEqual, + .lessThan, + .lessThanOrEqual, + .notEqual: + unwrappedValue = value?.first ?? BSONNull() + case .inSubset, .notInSubset: + unwrappedValue = value ?? [] + } + + document[field.pathWithNamespace.joined(separator: ".")] = [method.rawValue: unwrappedValue] as Document + + return document + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryFilter == FluentMongoQueryFilter { + + public static func queryFilters(for query: Query) -> [QueryFilter] { + guard let filter = query.filter else { + return [] + } + + return [filter] + } + + public static func queryFilterApply(_ filter: QueryFilter, to query: inout Query) { + switch query.filter { + case .some(let document): + query.filter = [query.defaultFilterRelation.rawValue: [document, filter]] + case .none: + query.filter = filter + } + } +} + +// MARK: - QueryFilterMethod + +public enum FluentMongoQueryFilterMethod: String { + case equal = "$eq" + case notEqual = "$ne" + case greaterThan = "$gt" + case lessThan = "$lt" + case greaterThanOrEqual = "$gte" + case lessThanOrEqual = "$lte" + case inSubset = "$in" + case notInSubset = "$nin" +} + +extension Database where Self: QuerySupporting, Self.QueryFilterMethod == FluentMongoQueryFilterMethod { + + public static var queryFilterMethodEqual: QueryFilterMethod { + return .equal + } + + public static var queryFilterMethodNotEqual: QueryFilterMethod { + return .notEqual + } + + public static var queryFilterMethodGreaterThan: QueryFilterMethod { + return .greaterThan + } + + public static var queryFilterMethodLessThan: QueryFilterMethod { + return .lessThan + } + + public static var queryFilterMethodGreaterThanOrEqual: QueryFilterMethod { + return .greaterThanOrEqual + } + + public static var queryFilterMethodLessThanOrEqual: QueryFilterMethod { + return .lessThanOrEqual + } + + public static var queryFilterMethodInSubset: QueryFilterMethod { + return .inSubset + } + + public static var queryFilterMethodNotInSubset: QueryFilterMethod { + return .notInSubset + } +} + +// MARK: - QueryFilterValue + +public typealias FluentMongoQueryFilterValue = [BSONValue] + +extension Database where Self: BSONCoder, Self: QuerySupporting, Self.QueryFilterValue == FluentMongoQueryFilterValue? { + + public static func queryFilterValue(_ encodables: [E]) -> QueryFilterValue { + let encoder = Self.encoder + let value: [BSONValue] = encodables.compactMap { try? encoder.encodeBSONValue($0) } + + return value + } + + public static var queryFilterValueNil: QueryFilterValue { + return nil + } +} + +// MARK: - QueryFilterRelation + +public enum FluentMongoQueryFilterRelation: String { + case and = "$and" + case or = "$or" +} + +extension Database where Self: QuerySupporting, Self.QueryFilterRelation == FluentMongoQueryFilterRelation { + + public static var queryFilterRelationAnd: QueryFilterRelation { + return .and + } + + public static var queryFilterRelationOr: QueryFilterRelation { + return .or + } +} + +extension Database where Self: QuerySupporting, Self.QueryFilter == FluentMongoQueryFilter, Self.QueryFilterRelation == FluentMongoQueryFilterRelation { + + public static func queryFilterGroup(_ relation: QueryFilterRelation, _ filters: [QueryFilter]) -> QueryFilter { + guard filters.count >= 2 else { + return filters.first ?? [:] + } + + return [relation.rawValue: filters] + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryFilterRelation == FluentMongoQueryFilterRelation { + + public static func queryDefaultFilterRelation(_ relation: QueryFilterRelation, on: inout Query) { + on.defaultFilterRelation = relation + } +} diff --git a/Sources/FluentMongo/FluentMongoQueryJoin.swift b/Sources/FluentMongo/FluentMongoQueryJoin.swift new file mode 100644 index 0000000..10bfde1 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryJoin.swift @@ -0,0 +1,59 @@ +// +// FluentMongoQueryJoin.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 19/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +public typealias FluentMongoQueryJoin = [Document] + +public enum FluentMongoQueryJoinMethod { + case inner + case outer +} + +extension Database where Self: JoinSupporting, Self.QueryJoin == FluentMongoQueryJoin, Self.Query == FluentMongoQuery { + + public static func queryJoinApply(_ join: QueryJoin, to query: inout Query) { + query.joins.append(contentsOf: join) + } +} + +extension Database where Self: JoinSupporting, Self.QueryJoin == FluentMongoQueryJoin, Self.QueryJoinMethod == FluentMongoQueryJoinMethod, Self.QueryField == FluentMongoQueryField { + + public static func queryJoin(_ method: QueryJoinMethod, base: QueryField, joined: QueryField) -> QueryJoin { + guard let collection = joined.entity else { + return [] + } + + let lookup: Document = [ + "$lookup": [ + "from": collection, + "localField": base.pathWithNamespace.joined(separator: "."), + "foreignField": joined.path.joined(separator: "."), + "as": collection + ] as Document + ] + + let unwind: Document = [ + "$unwind": [ + "path": "$" + collection, + "preserveNullAndEmptyArrays": method == .outer + ] as Document + ] + + return [lookup, unwind] + } +} + +extension Database where Self: JoinSupporting, Self.QueryJoinMethod == FluentMongoQueryJoinMethod { + + public static var queryJoinMethodDefault: QueryJoinMethod { + return .inner + } +} diff --git a/Sources/FluentMongo/FluentMongoQueryKey.swift b/Sources/FluentMongo/FluentMongoQueryKey.swift new file mode 100644 index 0000000..7f195e1 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQueryKey.swift @@ -0,0 +1,70 @@ +// +// FluentMongoQueryKey.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 11/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +// MARK: - QueryKey + +public indirect enum FluentMongoQueryKey { + case all + case raw(FluentMongoQueryField) + case computed(FluentMongoQueryAggregate, [FluentMongoQueryKey]) + + public typealias Computed = (aggregate: FluentMongoQueryAggregate, keys: [FluentMongoQueryKey]) + + public var raw: FluentMongoQueryField? { + switch self { + case .raw(let value): + return value + default: + return nil + } + } + + public var computed: Computed? { + switch self { + case .computed(let aggregate, let keys): + return (aggregate: aggregate, keys: keys) + default: + return nil + } + } +} + +extension Array where Element == FluentMongoQueryKey { + + public var raw: [FluentMongoQueryField] { + return self.compactMap { $0.raw } + } + + public var computed: [FluentMongoQueryKey.Computed] { + return self.compactMap { $0.computed } + } +} + +extension Database where Self: QuerySupporting, Self.QueryKey == FluentMongoQueryKey { + + public static var queryKeyAll: QueryKey { + return .all + } +} + +extension Database where Self: QuerySupporting, Self.QueryKey == FluentMongoQueryKey, Self.QueryField == FluentMongoQueryField { + + public static func queryKey(_ field: QueryField) -> QueryKey { + return .raw(field) + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QueryKey == FluentMongoQueryKey { + + public static func queryKeyApply(_ key: QueryKey, to query: inout Query) { + query.keys.append(key) + } +} diff --git a/Sources/FluentMongo/FluentMongoQuerySort.swift b/Sources/FluentMongo/FluentMongoQuerySort.swift new file mode 100644 index 0000000..c1982e4 --- /dev/null +++ b/Sources/FluentMongo/FluentMongoQuerySort.swift @@ -0,0 +1,52 @@ +// +// FluentMongoQuerySort.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 18/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +// MARK: - QuerySort + +public typealias FluentMongoQuerySort = Document + +public enum FluentMongoQuerySortDirection: Int { + case ascending = 1 + case descending = -1 +} + +extension Database where Self: QuerySupporting, Self.QuerySort == FluentMongoQuerySort, Self.QuerySortDirection == FluentMongoQuerySortDirection, Self.QueryField == FluentMongoQueryField { + + public static func querySort(_ field: QueryField, _ direction: QuerySortDirection) -> QuerySort { + var document = Document() + document[field.pathWithNamespace.joined(separator: ".")] = direction.rawValue + + return document + } +} + +extension Database where Self: QuerySupporting, Self.Query == FluentMongoQuery, Self.QuerySort == FluentMongoQuerySort { + + public static func querySortApply(_ sort: QuerySort, to query: inout Query) { + var document = query.sort ?? Document() + for field in sort { + document[field.key] = field.value + } + query.sort = document + } +} + +extension Database where Self: QuerySupporting, Self.QuerySortDirection == FluentMongoQuerySortDirection { + + public static var querySortDirectionAscending: QuerySortDirection { + return .ascending + } + + public static var querySortDirectionDescending: QuerySortDirection { + return .descending + } +} diff --git a/Sources/FluentMongo/IndexBuilder.swift b/Sources/FluentMongo/IndexBuilder.swift new file mode 100644 index 0000000..5c5ee0e --- /dev/null +++ b/Sources/FluentMongo/IndexBuilder.swift @@ -0,0 +1,162 @@ +// +// IndexBuilder.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 21/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +public final class IndexBuilder { + + private var index: IndexModel + + public let connection: Future + + internal init(on connection: Future) { + self.index = IndexModel(keys: [:]) + self.connection = connection + } +} + +public extension IndexBuilder { + + public func create() -> Future { + return self.connection.flatMap { conn in + return conn.createIndex(self.index, in: T.entity) + } + } + + public func drop() -> Future { + return self.connection.flatMap { conn in + return conn.dropIndex(self.index, in: T.entity) + } + } +} + +public extension IndexBuilder { + + public func key(_ key: KeyPath, _ direction: FluentMongoQuerySortDirection = .ascending) -> IndexBuilder { + let property: FluentProperty = .keyPath(key) + var keys = self.index.keys + keys[property.path] = direction.rawValue + self.index = IndexModel(keys: keys, options: self.index.options) + + return self + } + + public func background(_ value: Bool) -> IndexBuilder { + let previous = self.index.options + let options = IndexOptions( + background: value, + expireAfter: previous?.expireAfter, + name: previous?.name, + sparse: previous?.sparse, + storageEngine: previous?.storageEngine, + unique: previous?.unique, + version: previous?.version, + defaultLanguage: previous?.defaultLanguage, + languageOverride: previous?.defaultLanguage, + textVersion: previous?.textVersion, + weights: previous?.weights, + sphereVersion: previous?.sphereVersion, + bits: previous?.bits, + max: previous?.max, + min: previous?.min, + bucketSize: previous?.bucketSize, + partialFilterExpression: previous?.partialFilterExpression, + collation: previous?.collation + ) + self.index = IndexModel(keys: self.index.keys, options: options) + + return self + } + + public func expireAfter(_ value: Int32) -> IndexBuilder { + let previous = self.index.options + let options = IndexOptions( + background: previous?.background, + expireAfter: value, + name: previous?.name, + sparse: previous?.sparse, + storageEngine: previous?.storageEngine, + unique: previous?.unique, + version: previous?.version, + defaultLanguage: previous?.defaultLanguage, + languageOverride: previous?.defaultLanguage, + textVersion: previous?.textVersion, + weights: previous?.weights, + sphereVersion: previous?.sphereVersion, + bits: previous?.bits, + max: previous?.max, + min: previous?.min, + bucketSize: previous?.bucketSize, + partialFilterExpression: previous?.partialFilterExpression, + collation: previous?.collation + ) + self.index = IndexModel(keys: self.index.keys, options: options) + + return self + } + + public func name(_ value: String) -> IndexBuilder { + let previous = self.index.options + let options = IndexOptions( + background: previous?.background, + expireAfter: previous?.expireAfter, + name: value, + sparse: previous?.sparse, + storageEngine: previous?.storageEngine, + unique: previous?.unique, + version: previous?.version, + defaultLanguage: previous?.defaultLanguage, + languageOverride: previous?.defaultLanguage, + textVersion: previous?.textVersion, + weights: previous?.weights, + sphereVersion: previous?.sphereVersion, + bits: previous?.bits, + max: previous?.max, + min: previous?.min, + bucketSize: previous?.bucketSize, + partialFilterExpression: previous?.partialFilterExpression, + collation: previous?.collation + ) + self.index = IndexModel(keys: self.index.keys, options: options) + + return self + } + + public func unique(_ value: Bool) -> IndexBuilder { + let previous = self.index.options + let options = IndexOptions( + background: previous?.background, + expireAfter: previous?.expireAfter, + name: previous?.name, + sparse: previous?.sparse, + storageEngine: previous?.storageEngine, + unique: value, + version: previous?.version, + defaultLanguage: previous?.defaultLanguage, + languageOverride: previous?.defaultLanguage, + textVersion: previous?.textVersion, + weights: previous?.weights, + sphereVersion: previous?.sphereVersion, + bits: previous?.bits, + max: previous?.max, + min: previous?.min, + bucketSize: previous?.bucketSize, + partialFilterExpression: previous?.partialFilterExpression, + collation: previous?.collation + ) + self.index = IndexModel(keys: self.index.keys, options: options) + + return self + } +} + +public enum IndexBuilderError: Swift.Error { + case invalidKeys +} diff --git a/Sources/FluentMongo/Model+Index.swift b/Sources/FluentMongo/Model+Index.swift new file mode 100644 index 0000000..5d9742d --- /dev/null +++ b/Sources/FluentMongo/Model+Index.swift @@ -0,0 +1,16 @@ +// +// Model+Index.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 21/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension Model where Database == MongoDatabase { + public static func index(on conn: Database.Connection) -> IndexBuilder { + return IndexBuilder(on: conn.databaseConnection(to: Self.defaultDatabase)) + } +} diff --git a/Sources/FluentMongo/MongoConnection+Connect.swift b/Sources/FluentMongo/MongoConnection+Connect.swift new file mode 100644 index 0000000..8ca6eae --- /dev/null +++ b/Sources/FluentMongo/MongoConnection+Connect.swift @@ -0,0 +1,21 @@ +// +// MongoConnection+Connect.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Async + +public extension MongoConnection { + + public static func connect(config: MongoDatabaseConfig, on worker: Worker) -> Future { + do { + return try worker.future(MongoConnection(config: config, on: worker)) + } catch { + return worker.future(error: error) + } + } +} diff --git a/Sources/FluentMongo/MongoConnection.swift b/Sources/FluentMongo/MongoConnection.swift new file mode 100644 index 0000000..296bc5d --- /dev/null +++ b/Sources/FluentMongo/MongoConnection.swift @@ -0,0 +1,199 @@ +// +// MongoConnection.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import MongoSwift +import Fluent + +/// A Mongo frontend client. +public final class MongoConnection: BasicWorker, DatabaseConnection, DatabaseQueryable { + + public required init(config: MongoDatabaseConfig, on worker: Worker) throws { + self.config = config + self.worker = worker + self.client = try MongoClient(connectionString: config.connectionURL.absoluteString, options: config.options) + self.isClosed = false + self.logger = nil + } + + private let config: MongoDatabaseConfig + + private let worker: Worker + + let client: MongoClient + + /// See `Worker`. + public var eventLoop: EventLoop { + return self.worker.eventLoop + } + + /// If non-nil, will log queries. + public var logger: DatabaseLogger? + + /// See `Extendable`. + public var extend: Extend = [:] + + /// See `DatabaseConnection`. + public typealias Database = MongoDatabase + + public private(set)var isClosed: Bool + + /// Closes the `DatabaseConnection`. + public func close() { + self.client.close() + self.isClosed = true + } + + /// See `DatabaseQueryable`. + + public func query(_ query: Database.Query, _ handler: @escaping (Database.Output) throws -> Void) -> Future { + do { + self.logger?.record(query: String(describing: query)) + let database = try self.client.db(config.database) + let collection = try database.collection(query.collection) + + switch query.action { + case .insert: + guard let document = query.data else { + throw Error.invalidQuery(query) + } + if let result = try collection.insertOne(document) { + self.logger?.record(query: String(describing: result)) + } + case .find: + let cursor = try collection.aggregate(query.aggregationPipeline()) + var count = 0 + try cursor.forEach { + count += 1 + try handler($0) + } + // Running `count` in an aggregation pipeline produce a `nil` document when the provided filter does not match any. Therefore we have to manually set the count to `0`. + if let aggregate = query.keys.computed.first?.aggregate, count == 0 { + switch aggregate { + case .count: + try handler([FluentMongoQuery.defaultAggregateField: 0]) + case .group: + try handler([FluentMongoQuery.defaultAggregateField: BSONNull()]) + } + } + case .update: + switch (query.data, query.partialData) { + case (.none, .some(let document)): + if let result = try collection.updateMany(filter: self.filter(query, collection), update: ["$set": document]) { + self.logger?.record(query: String(describing: result)) + } + case (.some(let document), .none): + if let result = try collection.replaceOne(filter: self.filter(query, collection), replacement: document) { + self.logger?.record(query: String(describing: result)) + } + default: + throw Error.invalidQuery(query) + } + case .delete: + if let result = try collection.deleteMany(self.filter(query, collection)) { + self.logger?.record(query: String(describing: result)) + } + } + + return self.worker.future() + } catch { + return self.worker.future(error: error) + } + } + + private func filter(_ query: Database.Query, _ collection: MongoCollection) throws -> FluentMongoQueryFilter { + + guard let filter = query.filter, !filter.isEmpty else { + return [:] + } + + var pipeline = query.aggregationPipeline() + pipeline.append(["$project": ["_id": true] as Document]) + let cursor = try collection.aggregate(pipeline) + let identifiers = cursor.compactMap { $0["_id"] } + + return ["_id": ["$in": identifiers] as Document] + } +} + +// MARK: - Internal Indexing Helpers + +extension MongoConnection { + + func createIndex(_ index: IndexModel, in collection: String) -> Future { + do { + guard !index.keys.isEmpty else { + throw IndexBuilderError.invalidKeys + } + self.logger?.record(query: "MongoConnection.createIndex") + let database = try self.client.db(config.database) + self.logger?.record(query: "Create index on \(collection)") + _ = try database.collection(collection).createIndex(index) + + return self.worker.future() + } catch { + return self.worker.future(error: error) + } + } + + func dropIndex(_ index: IndexModel, in collection: String) -> Future { + do { + guard !index.keys.isEmpty else { + throw IndexBuilderError.invalidKeys + } + + self.logger?.record(query: "MongoConnection.dropIndex") + let database = try self.client.db(config.database) + self.logger?.record(query: "Drop index on \(collection)") + _ = try database.collection(collection).dropIndex(index) + + return self.worker.future() + } catch { + return self.worker.future(error: error) + } + } +} + +// MARK: - Internal MigrationSupporting Helpers + +extension MongoConnection { + + func prepareMigrationMetadata() -> Future { + do { + self.logger?.record(query: "MongoConnection.prepareMigrationMetadata") + let database = try self.client.db(config.database) + let collection = MigrationLog.entity + self.logger?.record(query: "Create collection: \(collection)") + _ = try database.createCollection(collection) + + return self.worker.future() + } catch { + return self.worker.future(error: error) + } + } + + func revertMigrationMetadata() -> Future { + do { + self.logger?.record(query: "MongoConnection.revertMigrationMetadata") + let database = try self.client.db(config.database) + let collection = MigrationLog.entity + self.logger?.record(query: "Drop collection: \(collection)") + _ = try database.collection(collection).drop() + + return self.worker.future() + } catch { + return self.worker.future(error: error) + } + } +} + +public extension MongoConnection { + public enum Error: Swift.Error { + case invalidQuery(Database.Query) + } +} diff --git a/Sources/FluentMongo/MongoDatabase+JoinSupporting.swift b/Sources/FluentMongo/MongoDatabase+JoinSupporting.swift new file mode 100644 index 0000000..1b48dd7 --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase+JoinSupporting.swift @@ -0,0 +1,17 @@ +// +// MongoDatabase+JoinSupporting.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 19/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension MongoDatabase: JoinSupporting { + + public typealias QueryJoin = FluentMongoQueryJoin + + public typealias QueryJoinMethod = FluentMongoQueryJoinMethod +} diff --git a/Sources/FluentMongo/MongoDatabase+KeyedCacheSupporting.swift b/Sources/FluentMongo/MongoDatabase+KeyedCacheSupporting.swift new file mode 100644 index 0000000..61e2cdb --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase+KeyedCacheSupporting.swift @@ -0,0 +1,12 @@ +// +// MongoDatabase+KeyedCacheSupporting.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 28/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension MongoDatabase: KeyedCacheSupporting { } diff --git a/Sources/FluentMongo/MongoDatabase+LogSupporting.swift b/Sources/FluentMongo/MongoDatabase+LogSupporting.swift new file mode 100644 index 0000000..b3452a5 --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase+LogSupporting.swift @@ -0,0 +1,17 @@ +// +// MongoDatabase+LogSupporting.swift +// Fluent +// +// Created by Valerio Mazzeo on 03/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension MongoDatabase: LogSupporting { + /// See `LogSupporting`. + public static func enableLogging(_ logger: DatabaseLogger, on conn: MongoConnection) { + conn.logger = logger + } +} diff --git a/Sources/FluentMongo/MongoDatabase+MigrationSupporting.swift b/Sources/FluentMongo/MongoDatabase+MigrationSupporting.swift new file mode 100644 index 0000000..2dccb4e --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase+MigrationSupporting.swift @@ -0,0 +1,23 @@ +// +// MongoDatabase+MigrationSupporting.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 21/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension MongoDatabase: MigrationSupporting { + + /// See `MigrationSupporting`. + public static func prepareMigrationMetadata(on conn: Connection) -> Future { + return conn.prepareMigrationMetadata() + } + + /// See `MigrationSupporting`. + public static func revertMigrationMetadata(on conn: Connection) -> Future { + return conn.revertMigrationMetadata() + } +} diff --git a/Sources/FluentMongo/MongoDatabase+QuerySupporting.swift b/Sources/FluentMongo/MongoDatabase+QuerySupporting.swift new file mode 100644 index 0000000..dfc0273 --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase+QuerySupporting.swift @@ -0,0 +1,69 @@ +// +// MongoDatabase+QuerySupporting.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 03/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +extension MongoDatabase: QuerySupporting { + + public typealias Query = FluentMongoQuery + + public typealias Output = FluentMongoOutput + + public typealias QueryAction = FluentMongoQueryAction + + public typealias QueryAggregate = FluentMongoQueryAggregate + + public typealias QueryData = FluentMongoQueryData + + public typealias QueryField = FluentMongoQueryField + + public typealias QueryFilter = FluentMongoQueryFilter + + public typealias QueryFilterMethod = FluentMongoQueryFilterMethod + + public typealias QueryFilterValue = FluentMongoQueryFilterValue? + + public typealias QueryFilterRelation = FluentMongoQueryFilterRelation + + public typealias QueryKey = FluentMongoQueryKey + + public typealias QuerySort = FluentMongoQuerySort + + public typealias QuerySortDirection = FluentMongoQuerySortDirection + + public static func queryExecute(_ query: Query, on conn: Connection, into handler: @escaping (Output, Connection) throws -> Void) -> Future { + return conn.query(query) { try handler($0, conn) } + } + + public static func modelEvent(event: ModelEvent, model: M, on conn: Connection) -> Future where M.Database == MongoDatabase { + var copy = model + switch event { + case .willCreate where M.ID.self is UUID.Type && copy.fluentID == nil: + copy.fluentID = UUID() as? M.ID + case .willCreate where M.ID.self is Int.Type && copy.fluentID == nil: + return M.query(on: conn).max(M.idKey).map { id in + switch id { + case .some(let value as Int): + copy.fluentID = (value + 1) as? M.ID + case .none: + copy.fluentID = 0 as? M.ID + default: + break + } + + return copy + } + default: + break + } + + return conn.future(copy) + } +} diff --git a/Sources/FluentMongo/MongoDatabase.swift b/Sources/FluentMongo/MongoDatabase.swift new file mode 100644 index 0000000..e703726 --- /dev/null +++ b/Sources/FluentMongo/MongoDatabase.swift @@ -0,0 +1,45 @@ +// +// MongoDatabase.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import MongoSwift + +/// Creates connections to an identified Mongo database. +public final class MongoDatabase: Database { + /// This database's configuration. + public let config: MongoDatabaseConfig + + /// Creates a new `MongoDatabase`. + public init(config: MongoDatabaseConfig) { + self.config = config + } + + /// See `Database` + public func newConnection(on worker: Worker) -> Future { + return MongoConnection.connect(config: self.config, on: worker) + } +} + +extension MongoDatabase: BSONCoder { + + public static var encoder: BSONEncoder = { + return BSONEncoder() + }() + + public static var decoder: BSONDecoder = { + return BSONDecoder() + }() +} + +extension DatabaseIdentifier { + /// Default identifier for `MongoDatabase`. + public static var mongo: DatabaseIdentifier { + return .init("mongo") + } +} diff --git a/Sources/FluentMongo/MongoDatabaseConfig.swift b/Sources/FluentMongo/MongoDatabaseConfig.swift new file mode 100644 index 0000000..1defc99 --- /dev/null +++ b/Sources/FluentMongo/MongoDatabaseConfig.swift @@ -0,0 +1,90 @@ +// +// MongoDatabaseConfig.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import MongoSwift + +/// Config options for a `MongoDatabase` +public struct MongoDatabaseConfig { + + /// Creates a `MongoDatabaseConfig` with default settings. + public static func `default`(database: String, options: ClientOptions? = nil) throws -> MongoDatabaseConfig { + return try .init(database: database, options: options) + } + + /// Connection string. + public let connectionURL: URL + + public let database: String + + public let options: ClientOptions? + + /// Creates a new `MongoDatabaseConfig`. + public init(connectionString: String, options: ClientOptions? = nil) throws { + guard let url = URL(string: connectionString) else { + throw URLError(.badURL) + } + + try self.init(connectionURL: url, options: options) + } + + public init(connectionURL: URL, options: ClientOptions? = nil) throws { + guard let database = connectionURL.databaseName else { + throw URLError(.badURL) + } + + self.connectionURL = connectionURL + self.database = database + self.options = options + } + + public init( + scheme: String = "mongodb", + user: String? = nil, + password: String? = nil, + host: String = "127.0.0.1", + port: Int = 27017, + database: String, + options: ClientOptions? = nil + ) throws { + + var components = URLComponents() + components.scheme = scheme + components.user = user + components.password = password + components.host = host + components.port = port + components.path = "/" + database + + guard let url = components.url else { + throw URLError(.badURL) + } + + try self.init(connectionURL: url, options: options) + } + + public init(environment: [String: String] = ProcessInfo.processInfo.environment) throws { + + guard let connectionString = environment[EnvironmentKey.connectionURL.rawValue] else { + throw Error.missingEnvironmentKey(.connectionURL) + } + + try self.init(connectionString: connectionString) + } +} + +public extension MongoDatabaseConfig { + + public enum EnvironmentKey: String { + case connectionURL = "FLUENT_MONGO_CONNECTION_URL" + } + + public enum Error: Swift.Error { + case missingEnvironmentKey(EnvironmentKey) + } +} diff --git a/Sources/FluentMongo/QueryBuilder+Distinct.swift b/Sources/FluentMongo/QueryBuilder+Distinct.swift new file mode 100644 index 0000000..8ef41c9 --- /dev/null +++ b/Sources/FluentMongo/QueryBuilder+Distinct.swift @@ -0,0 +1,19 @@ +// +// QueryBuilder+Distinct.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 18/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +extension QueryBuilder where Database.Query == FluentMongoQuery { + + public func distinct(_ value: Bool = true) -> Self { + self.query.isDistinct = value + + return self + } +} diff --git a/Sources/FluentMongo/QueryBuilder+Key.swift b/Sources/FluentMongo/QueryBuilder+Key.swift new file mode 100644 index 0000000..836a8cf --- /dev/null +++ b/Sources/FluentMongo/QueryBuilder+Key.swift @@ -0,0 +1,63 @@ +// +// QueryBuilder+Key.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 12/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent + +// Duplicates https://github.com/vapor/fluent/pull/601 +public extension QueryBuilder { + // MARK: Key + + /// Applies a key to this query specifying a field to fetch from the database. + /// + /// let users = try User.query(on: conn) + /// .key(\.name) + /// .all() + /// + /// - parameters: + /// - key: Swift `KeyPath` to a field on the model to retrieve. + /// - returns: Query builder for chaining. + @discardableResult + public func key(_ field: KeyPath) -> Self where T: Decodable { + Database.queryKeyApply( + Database.queryKey(Database.queryField(.keyPath(field))), + to: &self.query + ) + + return self + } + + /// Applies all the keys reflected from `type` to this query specifying the fields to fetch from the database. This also sets the query to decode `Decodable` type `T` when run. This subtype can represents a subset of the data to retrieve and it is not necessarily a `Model`. The data will be decoded from the original `Model` query entity. + /// + /// struct Person { + /// let name: String + /// } + /// + /// let people = try User.query(on: conn) + /// .keys(for: Person.self) + /// .all() + /// + /// - parameters: + /// - type: New decodable type `T` to decode. + /// - depth: The level of nesting to use. + /// If `0`, the top-most properties will be added as keys. + /// If `1`, the first layer of nested properties, and so-on. + /// - returns: `QueryBuilder` decoding type `T`. + public func keys(for type: T.Type, depth: Int = 0) throws -> QueryBuilder where T: Decodable { + let properties = try type.decodeProperties(depth: depth) + + for property in properties { + Database.queryKeyApply( + Database.queryKey(Database.queryField(.reflected(property, rootType: Result.self))), + to: &self.query + ) + } + + return self.decode(data: T.self, Database.queryEntity(for: self.query)) + } +} diff --git a/Tests/FluentMongoTests/FluentMongoProviderTests.swift b/Tests/FluentMongoTests/FluentMongoProviderTests.swift new file mode 100755 index 0000000..a35c1e8 --- /dev/null +++ b/Tests/FluentMongoTests/FluentMongoProviderTests.swift @@ -0,0 +1,326 @@ +// +// FluentMongoProviderTests.swift +// FluentMongoTests +// +// Created by Valerio Mazzeo on 30/11/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import XCTest +import Fluent +import FluentBenchmark +import MongoSwift +@testable import FluentMongo + +class FluentMongoProviderTests: XCTestCase { + + static let allTests = [ + ("testIndex", testIndex), + ("testModels", testModels), + ("testJoin", testJoin), + ("testDistinct", testDistinct), + ("testMigration", testMigration), + //("testBenchmarkModels", testBenchmarkModels), + ("testBenchmarkUpdate", testBenchmarkUpdate), + ("testBenchmarkBugs", testBenchmarkBugs), + ("testBenchmarkSort", testBenchmarkSort), + ("testBenchmarkRange", testBenchmarkRange), + ("testBenchmarkSubset", testBenchmarkSubset), + ("testBenchmarkChunking", testBenchmarkChunking), + ("testBenchmarkAggregate", testBenchmarkAggregate), + ("testBenchmarkLifecycle", testBenchmarkLifecycle), + ("testBenchmarkAutoincrement", testBenchmarkAutoincrement), + ("testBenchmarkTimestampable", testBenchmarkTimestampable), + ("testBenchmarkJoins", testBenchmarkJoins) + ] + + var benchmarker: Benchmarker! + + var database: FluentMongo.MongoDatabase! + + override func setUp() { + let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1) + do { + let config = try MongoDatabaseConfig( + host: "localhost", + port: 27017, + database: "vapor_database" + ) + + try MongoClient(connectionString: config.connectionURL.absoluteString).db(config.database).drop() + self.database = MongoDatabase(config: config) + self.benchmarker = try Benchmarker(self.database, on: eventLoop, onFail: XCTFail) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testIndex() { + do { + let conn = try self.database.newConnection(on: MultiThreadedEventLoopGroup(numberOfThreads: 1)).wait() + try User.index(on: conn).key(\.name, .descending).unique(true).create().wait() + XCTAssertNoThrow(try User(name: "asdf", age: 42).save(on: conn).wait()) + XCTAssertThrowsError(try User(name: "asdf", age: 58).save(on: conn).wait()) + try User.index(on: conn).key(\.name, .descending).drop().wait() + XCTAssertNoThrow(try User(name: "asdf", age: 58).save(on: conn).wait()) + } catch { + XCTFail(error.localizedDescription) + } + } + + // Can be replaced with testBenchmarkModels once https://github.com/vapor/fluent/pull/603 is fixed + func testModels() { + do { + let conn = try self.database.newConnection(on: MultiThreadedEventLoopGroup(numberOfThreads: 1)).wait() + + // create + let a = try User(name: "asdf", age: 42).save(on: conn).wait() + let b = try User(name: "asdf", age: 42).save(on: conn).wait() + + XCTAssertEqual(try User.query(on: conn).count().wait(), 2) + + // update + b.name = "fdsa" + _ = try b.save(on: conn).wait() + _ = try User.query(on: conn).filter(\User._id == a._id).update(\.age, to: 314).run().wait() + + // read + XCTAssertEqual(try User.find(b.requireID(), on: conn).wait()?.name, "fdsa") + + // make sure that AND queries work as expected - this query should return exactly one result + XCTAssertEqual(try User.query(on: conn) + .group(.and) { and in + and.filter(\User.name == "asdf") + and.filter(\User.age == 314) + } + .all().wait().count, 1) + + // make sure that OR queries work as expected - this query should return exactly two results + XCTAssertEqual(try User.query(on: conn) + .group(.or) { or in + or.filter(\User.name == "asdf") + or.filter(\User.name == "fdsa") + } + .all().wait().count, 2) + + // delete + XCTAssertNoThrow(try b.delete(on: conn).wait()) + XCTAssertEqual(try User.query(on: conn).count().wait(), 1) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testJoin() { + do { + let conn = try self.database.newConnection(on: MultiThreadedEventLoopGroup(numberOfThreads: 1)).wait() + + let ball = try Toy(name: "ball").save(on: conn).wait() + let bone = try Toy(name: "bone").save(on: conn).wait() + let puppet = try Toy(name: "puppet").save(on: conn).wait() + + let molly = try Pet(name: "Molly", age: 2, favoriteToyId: ball.requireID()) + .save(on: conn) + .wait() + let rex = try Pet(name: "Rex", age: 1).save(on: conn).wait() + + // Relationships + XCTAssertNotNil(try molly.favoriteToy?.get(on: conn).wait()) + XCTAssertNil(try rex.favoriteToy?.get(on: conn).wait()) + + // Inner Join + let toysFavoritedByPets = try Toy + .query(on: conn) + .key(\.name) + .join(\Pet.favoriteToyId, to: Toy.idKey, method: .inner) + .all() + .wait() + + XCTAssertEqual(toysFavoritedByPets.count, 1) + XCTAssertEqual(toysFavoritedByPets.first?._id, ball._id) + + // Outer Join + let toysNotFavoritedByPets = try Toy + .query(on: conn) + .key(\.name) + .join(\Pet.favoriteToyId, to: Toy.idKey, method: .outer) + .filter(Pet.idKey == nil) + .all() + .wait() + //.filter(Pet.self, Pet.idKey, .equals, nil) + //.all([.raw("name", [])]) + + XCTAssertEqual(toysNotFavoritedByPets.count, 2) + XCTAssertTrue(toysNotFavoritedByPets.contains(where: { $0._id == bone._id })) + XCTAssertTrue(toysNotFavoritedByPets.contains(where: { $0._id == puppet._id })) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testDistinct() { + do { + let conn = try self.database.newConnection(on: MultiThreadedEventLoopGroup(numberOfThreads: 1)).wait() + _ = try User(name: "Alice", age: 20).save(on: conn).wait() + _ = try User(name: "Bob", age: 20).save(on: conn).wait() + _ = try User(name: "Charlie", age: 20).save(on: conn).wait() + _ = try User(name: "Bob", age: 19).save(on: conn).wait() + _ = try User(name: "Charlie", age: 20).save(on: conn).wait() + + XCTAssertEqual(try User.query(on: conn).count().wait(), 5) + XCTAssertEqual(try User.query(on: conn).distinct().count().wait(), 5) + XCTAssertEqual(try User.query(on: conn).distinct().key(\.name).count().wait(), 3) + XCTAssertEqual(try User.query(on: conn).distinct().key(\.name).key(\.age).count().wait(), 4) + let users = try User.query(on: conn).distinct().key(\.name).all().wait().map { $0.name } + XCTAssertEqual(users.count, 3) + XCTAssertTrue(users.contains("Alice")) + XCTAssertTrue(users.contains("Bob")) + XCTAssertTrue(users.contains("Charlie")) + let usersNameAge = try User.query(on: conn).distinct().key(\.name).key(\.age).all().wait() + XCTAssertEqual(usersNameAge.count, 4) + XCTAssertTrue(usersNameAge.contains(where: { $0.name == "Alice" && $0.age == 20 })) + XCTAssertTrue(usersNameAge.contains(where: { $0.name == "Bob" && $0.age == 20 })) + XCTAssertTrue(usersNameAge.contains(where: { $0.name == "Bob" && $0.age == 19 })) + XCTAssertTrue(usersNameAge.contains(where: { $0.name == "Charlie" && $0.age == 20 })) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testMigration() { + do { + let conn = try self.database.newConnection(on: MultiThreadedEventLoopGroup(numberOfThreads: 1)).wait() + try MongoDatabase.prepareMigrationMetadata(on: conn).wait() + _ = try User(name: "Alice", age: 20).save(on: conn).wait() + _ = try User(name: "Bob", age: 20).save(on: conn).wait() + _ = try User(name: "Charlie", age: 20).save(on: conn).wait() + _ = try User(name: "Bob", age: 19).save(on: conn).wait() + _ = try User(name: "Charlie", age: 20).save(on: conn).wait() + + try User.SetAgeMigration.prepare(on: conn).wait() + XCTAssertEqual(try User.query(on: conn).filter(\.age == 99).count().wait(), 5) + try User.SetAgeMigration.revert(on: conn).wait() + XCTAssertEqual(try User.query(on: conn).filter(\.age == nil).count().wait(), 5) + try MongoDatabase.revertMigrationMetadata(on: conn).wait() + } catch { + XCTFail(error.localizedDescription) + } + } + + /* + func testBenchmarkModels() { + do { + https://github.com/vapor/fluent/pull/603 + try self.benchmarker.benchmarkModels() + } catch { + XCTFail(error.localizedDescription) + } + }*/ + + func testBenchmarkUpdate() { + do { + try self.benchmarker.benchmarkUpdate() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkBugs() { + do { + try self.benchmarker.benchmarkBugs() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkSort() { + do { + try self.benchmarker.benchmarkSort() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkRange() { + do { + try self.benchmarker.benchmarkRange() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkSubset() { + do { + try self.benchmarker.benchmarkSubset() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkChunking() { + do { + try self.benchmarker.benchmarkChunking() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkAggregate() { + do { + try self.benchmarker.benchmarkAggregate() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkLifecycle() { + do { + try self.benchmarker.benchmarkLifecycle() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkAutoincrement() { + do { + try self.benchmarker.benchmarkAutoincrement() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkTimestampable() { + do { + try self.benchmarker.benchmarkTimestampable() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkJoins() { + do { + try self.benchmarker.benchmarkJoins() + } catch { + XCTFail(error.localizedDescription) + } + } + + /** Implement when we implementing + + func testBenchmarkTransaction() { + do { + try self.benchmarker.benchmarkTransaction() + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBenchmarkSoftDeletable() { + do { + try self.benchmarker.benchmarkSoftDeletable() + } catch { + XCTFail(error.localizedDescription) + } + } + */ +} diff --git a/Tests/FluentMongoTests/Pet.swift b/Tests/FluentMongoTests/Pet.swift new file mode 100644 index 0000000..3757d2f --- /dev/null +++ b/Tests/FluentMongoTests/Pet.swift @@ -0,0 +1,38 @@ +// +// Pet.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 20/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import FluentMongo + +final class Pet: FluentMongoModel, Model { + + typealias Database = MongoDatabase + + typealias ID = UUID + + var _id: UUID? + var name: String + var age: Int? + var favoriteToyId: Toy.ID? + + init(_id: UUID? = nil, name: String, age: Int? = nil, favoriteToyId: Toy.ID? = nil) { + self._id = _id + self.name = name + self.age = age + self.favoriteToyId = favoriteToyId + } + + var favoriteToy: Parent? { + return self.parent(\.favoriteToyId) + } + + var toys: Siblings { + return self.siblings() + } +} diff --git a/Tests/FluentMongoTests/PetToy.swift b/Tests/FluentMongoTests/PetToy.swift new file mode 100644 index 0000000..b4e3643 --- /dev/null +++ b/Tests/FluentMongoTests/PetToy.swift @@ -0,0 +1,30 @@ +// +// PetToy.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 20/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import FluentMongo + +final class PetToy: FluentMongoModel, Pivot { + + typealias Database = MongoDatabase + + typealias ID = UUID + + typealias Left = Pet + + typealias Right = Toy + + static var leftIDKey: LeftIDKey = \.petId + + static var rightIDKey: RightIDKey = \.toyId + + var _id: UUID? + var petId: Pet.ID + var toyId: Toy.ID +} diff --git a/Tests/FluentMongoTests/Toy.swift b/Tests/FluentMongoTests/Toy.swift new file mode 100644 index 0000000..ba0b09e --- /dev/null +++ b/Tests/FluentMongoTests/Toy.swift @@ -0,0 +1,32 @@ +// +// Toy.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 20/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import FluentMongo + +final class Toy: FluentMongoModel, Model { + + typealias Database = MongoDatabase + + typealias ID = UUID + + var _id: UUID? + var name: String + var material: String? + + init(_id: UUID? = nil, name: String, material: String? = nil) { + self._id = _id + self.name = name + self.material = material + } + + var pets: Siblings { + return self.siblings() + } +} diff --git a/Tests/FluentMongoTests/User.swift b/Tests/FluentMongoTests/User.swift new file mode 100644 index 0000000..87f3dce --- /dev/null +++ b/Tests/FluentMongoTests/User.swift @@ -0,0 +1,44 @@ +// +// User.swift +// FluentMongo +// +// Created by Valerio Mazzeo on 18/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +import Foundation +import Fluent +import FluentMongo + +final class User: FluentMongoModel, Model { + + typealias Database = MongoDatabase + + typealias ID = UUID + + var _id: UUID? + var name: String + var age: Int? + + init(_id: UUID? = nil, name: String, age: Int? = nil) { + self._id = _id + self.name = name + self.age = age + } +} + +extension User { + + class SetAgeMigration: Migration { + + typealias Database = MongoDatabase + + static func prepare(on conn: Database.Connection) -> Future { + return User.query(on: conn).update(\.age, to: 99).run() + } + + static func revert(on conn: Database.Connection) -> Future { + return User.query(on: conn).update(\.age, to: nil).run() + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..dce78ce --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,18 @@ +// +// LinuxMain.swift +// FluentMongoTests +// +// Created by Valerio Mazzeo on 18/12/2018. +// Copyright © 2018 Asensei Inc. All rights reserved. +// + +#if os(Linux) + +import XCTest +@testable import FluentMongoTests + +XCTMain([ + testCase(FluentMongoProviderTests.allTests) +]) + +#endif