├── .swift-version ├── .cfignore ├── .gitignore ├── manifest.yml ├── Tests ├── LinuxMain.swift └── TodoListTests │ └── TestTodoList.swift ├── Sources ├── TodoList │ ├── TodoCollectionError.swift │ ├── DatabaseConfiguration.swift │ ├── TodoItem.swift │ ├── TodoListAPI.swift │ ├── TodoItemExtension.swift │ ├── TodoListController.swift │ └── TodoList.swift └── Server │ └── main.swift ├── Dockerfile ├── .travis.yml ├── Package.swift ├── config.sh └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | .build/* 2 | Packages/* 3 | Database/* 4 | cloud_config.json 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | couchdb.stdout 6 | couchdb.stderr 7 | Package.resolved 8 | /cloud_config.json 9 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | declared-services: 2 | TodoListCloudantDatabase: 3 | label: cloudantNoSQLDB 4 | plan: Lite 5 | applications: 6 | - name: TodoListCloudantApp 7 | memory: 256M 8 | instances: 1 9 | random-route: true 10 | command: Server 11 | services: 12 | - TodoListCloudantDatabase 13 | 14 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2016 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | 19 | @testable import TodoListTests 20 | 21 | XCTMain([ 22 | testCase(TestTodoList.allTests) 23 | ]) 24 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoCollectionError.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | public enum TodoCollectionError: Error { 20 | 21 | case ConnectionRefused 22 | case IDNotFound(String) 23 | case CreationError(String) 24 | case ParseError 25 | case AuthError 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright IBM Corporation 2016 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | # Dockerfile to build a Docker image with the Swift binaries and its dependencies. 18 | 19 | FROM ibmcom/swift-ubuntu:3.1 20 | MAINTAINER IBM Swift Engineering at IBM Cloud 21 | LABEL Description="Linux Ubuntu 14.04 image with the Swift binaries and CouchDB driver" 22 | 23 | EXPOSE 8080 24 | 25 | WORKDIR $HOME 26 | 27 | # Copy the application source code 28 | COPY . $HOME 29 | 30 | # Compile the application 31 | RUN swift build --configuration release 32 | 33 | CMD .build/release/Server 34 | -------------------------------------------------------------------------------- /Sources/TodoList/DatabaseConfiguration.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | public struct DatabaseConfiguration { 20 | 21 | public var host: String? 22 | public var port: UInt16? 23 | public var username: String? 24 | public var password: String? 25 | public var options = [String: Any]() 26 | 27 | public init(host: String?, port: UInt16?, username: String?, password: String?) { 28 | self.host = host 29 | self.port = port 30 | self.username = username 31 | self.password = password 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | - /^issue.*$/ 5 | 6 | matrix: 7 | include: 8 | - os: linux 9 | dist: trusty 10 | sudo: required 11 | - os: osx 12 | osx_image: xcode9 13 | sudo: required 14 | 15 | before_install: 16 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 17 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install curl openssl ; fi 18 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then wget https://dl.bintray.com/apache/couchdb/mac/1.6.1/Apache-CouchDB-1.6.1.zip && unzip Apache-CouchDB-1.6.1.zip ; fi 19 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then open "Apache CouchDB.app"/ && sleep 5 ; fi 20 | 21 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -y ; fi 22 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget https://swift.org/builds/swift-4.0-release/ubuntu1404/swift-4.0-RELEASE/swift-4.0-RELEASE-ubuntu14.04.tar.gz ; fi 23 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then tar xzvf swift-4.0-RELEASE-ubuntu14.04.tar.gz ; fi 24 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export PATH=swift-4.0-RELEASE-ubuntu14.04/usr/bin:$PATH ; fi 25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -y install couchdb clang-3.8 ; fi 26 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo mkdir /var/run/couchdb && sudo couchdb -b && sleep 5 ; fi 27 | 28 | script: 29 | - swift build 30 | - swift test 31 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoItem.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | public struct TodoItem { 18 | 19 | public let documentID: String 20 | 21 | public let userID: String? 22 | 23 | public let rank: Int 24 | 25 | /// Text to display 26 | public let title: String 27 | 28 | /// Whether completed or not 29 | public let completed: Bool 30 | 31 | public init(documentID: String, userID: String? = nil, rank: Int, title: String, completed: Bool) { 32 | self.documentID = documentID 33 | self.userID = userID 34 | self.rank = rank 35 | self.title = title 36 | self.completed = completed 37 | } 38 | 39 | } 40 | 41 | 42 | extension TodoItem : Equatable { } 43 | 44 | public func == (lhs: TodoItem, rhs: TodoItem) -> Bool { 45 | return lhs.documentID == rhs.documentID && lhs.userID == rhs.userID && lhs.rank == rhs.rank && 46 | lhs.title == rhs.title && lhs.completed == rhs.completed 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoListAPI.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | 18 | 19 | /** 20 | TodoListAPI 21 | 22 | TodoCollection defines the basic operations for todo lists 23 | */ 24 | public protocol TodoListAPI { 25 | func count(withUserID: String?, oncompletion: @escaping (Int?, Error?) -> Void) 26 | func clear(withUserID: String?, oncompletion: @escaping (Error?) -> Void) 27 | func clearAll(oncompletion: @escaping (Error?) -> Void) 28 | func get(withUserID: String?, oncompletion: @escaping ([TodoItem]?, Error?) -> Void) 29 | func get(withUserID: String?, withDocumentID: String, oncompletion: @escaping (TodoItem?, Error?) -> Void ) 30 | func add(userID: String?, title: String, rank: Int, completed: Bool, 31 | oncompletion: @escaping (TodoItem?, Error?) -> Void ) 32 | func update(documentID: String, userID: String?, title: String?, rank: Int?, 33 | completed: Bool?, oncompletion: @escaping (TodoItem?, Error?) -> Void ) 34 | func delete(withUserID: String?, withDocumentID: String, oncompletion: @escaping (Error?) -> Void)} 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | /** 5 | * Copyright IBM Corporation 2017 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | **/ 19 | 20 | import PackageDescription 21 | 22 | 23 | let package = Package( 24 | name: "TodoList", 25 | products: [], 26 | 27 | dependencies: [ 28 | .package(url: "https://github.com/IBM-Swift/Kitura.git", from: "1.0.0"), 29 | .package(url: "https://github.com/IBM-Swift/Kitura-CouchDB.git", from: "1.7.2"), 30 | .package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "4.0.5"), 31 | .package(url: "https://github.com/rob-deans/CloudConfiguration.git", from: "2.1.0") 32 | ], 33 | 34 | targets: [ 35 | .target( 36 | name: "TodoList", 37 | dependencies: ["CloudEnvironment", "CouchDB", "Kitura"] 38 | ), 39 | .target( 40 | name: "Server", 41 | dependencies: [.target(name: "TodoList"), "CloudConfiguration"] 42 | ), 43 | .testTarget( 44 | name: "TodoListTests", 45 | dependencies: ["TodoList"] 46 | ) 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /Sources/Server/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | import Kitura 20 | import HeliumLogger 21 | import LoggerAPI 22 | import TodoList 23 | import Configuration 24 | import CloudFoundryConfig 25 | import CloudEnvironment 26 | 27 | HeliumLogger.use() 28 | 29 | let configFile = "cloud_config.json" 30 | let databaseName = "todolist" 31 | extension TodoList { 32 | 33 | public convenience init(config: CloudantService) { 34 | 35 | self.init(host: config.host, port: UInt16(config.port), 36 | username: config.username, password: config.password) 37 | } 38 | } 39 | 40 | let todos: TodoList 41 | 42 | let manager = ConfigurationManager() 43 | 44 | let cloudEnv = CloudEnv() 45 | let cloudantCredentials = cloudEnv.getCloudantCredentials(name: "MyTodoListDB") 46 | 47 | do { 48 | manager.load(.environmentVariables).load(file: configFile) 49 | let cloudantConfig = try manager.getCloudantService(name: "TodoListCloudantDatabase") 50 | todos = TodoList(config: cloudantConfig) 51 | 52 | } catch { 53 | todos = TodoList() 54 | } 55 | 56 | let controller = TodoListController(backend: todos) 57 | 58 | let port = manager.port 59 | Log.verbose("Assigned port is \(port)") 60 | 61 | Kitura.addHTTPServer(onPort: port, with: controller.router) 62 | Kitura.run() 63 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoItemExtension.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import Configuration 19 | 20 | typealias JSONDictionary = [String : Any] 21 | 22 | let localServerURL = "http://localhost:8080" 23 | 24 | protocol DictionaryConvertible { 25 | func toDictionary() -> JSONDictionary 26 | } 27 | 28 | extension TodoItem : DictionaryConvertible { 29 | var url: String { 30 | 31 | let url: String 32 | 33 | let manager = ConfigurationManager() 34 | manager.load(.environmentVariables) 35 | 36 | if let configUrl = manager["VCAP_APPLICATION:uris:0"] as? String { 37 | url = "https://" + configUrl 38 | } 39 | else { 40 | url = manager["url"] as? String ?? localServerURL 41 | } 42 | return url + "/api/todos/" + documentID 43 | } 44 | 45 | func toDictionary() -> JSONDictionary { 46 | var result = JSONDictionary() 47 | result["id"] = self.documentID 48 | result["user"] = self.userID 49 | result["order"] = self.rank 50 | result["title"] = self.title 51 | result["completed"] = self.completed 52 | result["url"] = self.url 53 | return result 54 | } 55 | } 56 | 57 | extension Array where Element : DictionaryConvertible { 58 | func toDictionary() -> [JSONDictionary] { 59 | return self.map { $0.toDictionary() } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------ 3 | # Script: config.sh 4 | # Author: Swift@IBM 5 | # ----------------------------------------------------------- 6 | 7 | VERSION="1.0" 8 | BUILD_DIR=".build-linux" 9 | BRIDGE_APP_NAME="containerbridge" 10 | DATABASE_NAME="TodoListCloudantDatabase" 11 | REGISTRY_URL="registry.ng.bluemix.net" 12 | DATABASE_TYPE="cloudantNoSQLDB" 13 | DATABASE_LEVEL="Lite" 14 | 15 | function help { 16 | cat <<-!!EOF 17 | Usage: $CMD [ build | run | push-docker ] [arguments...] 18 | 19 | Where: 20 | install-tools Installs necessary tools for config, like Cloud Foundry CLI 21 | login Logs into IBM Cloud and Container APIs 22 | build Builds Docker container from Dockerfile 23 | run Runs Docker container, ensuring it was built properly 24 | stop Stops Docker container, if running 25 | push-docker Tags and pushes Docker container to IBM Cloud 26 | create-bridge Creates empty bridge application 27 | create-db Creates database service and binds to bridge 28 | deploy Binds everything together (app, db, container) through container group 29 | populate-db Populates database with initial data 30 | delete Delete the group container and deletes created service if possible 31 | all Combines all necessary commands to deploy an app to IBM Cloud in a Docker container. 32 | !!EOF 33 | } 34 | 35 | install-tools () { 36 | brew tap cloudfoundry/tap 37 | brew install cf-cli 38 | cf install-plugin https://static-ice.ng.bluemix.net/ibm-containers-mac 39 | } 40 | 41 | login () { 42 | echo "Setting api and login tools." 43 | cf api https://api.ng.bluemix.net 44 | cf login 45 | cf ic login 46 | } 47 | 48 | buildDocker () { 49 | if [ -z "$1" ] 50 | then 51 | echo "Error: build failed, docker name not provided." 52 | return 53 | fi 54 | docker build -t $1 --force-rm . 55 | } 56 | 57 | runDocker () { 58 | if [ -z "$1" ] 59 | then 60 | echo "Error: run failed, docker name not provided." 61 | return 62 | fi 63 | docker run --name $1 -d -p 8080:8080 $1 64 | } 65 | 66 | stopDocker () { 67 | if [ -z "$1" ] 68 | then 69 | echo "Error: clean failed, docker name not provided." 70 | return 71 | fi 72 | docker rm -fv $1 || true 73 | } 74 | 75 | pushDocker () { 76 | if [ -z "$1" ] || [ -z $REGISTRY_URL ] 77 | then 78 | echo "Error: Pushing Docker container to IBM Cloud failed, missing variables." 79 | return 80 | fi 81 | echo "Tagging and pushing docker container..." 82 | namespace=$(cf ic namespace get) 83 | docker tag $1 $REGISTRY_URL/$namespace/$1 84 | docker push $REGISTRY_URL/$namespace/$1 85 | } 86 | 87 | createBridge () { 88 | if [ -z $BRIDGE_APP_NAME ] 89 | then 90 | echo "Error: Creating bridge application failed, missing BRIDGE_APP_NAME." 91 | return 92 | fi 93 | mkdir $BRIDGE_APP_NAME 94 | cd $BRIDGE_APP_NAME 95 | touch empty.txt 96 | cf push $BRIDGE_APP_NAME -p . -i 1 -d mybluemix.net -k 1M -m 64M --no-hostname --no-manifest --no-route --no-start 97 | rm empty.txt 98 | cd .. 99 | rm -rf $BRIDGE_APP_NAME 100 | } 101 | 102 | createDatabase () { 103 | if [ -z $DATABASE_TYPE ] || [ -z $DATABASE_LEVEL ] || [ -z $DATABASE_NAME ] || [ -z $BRIDGE_APP_NAME ] 104 | then 105 | echo "Error: Creating bridge application failed, missing variables." 106 | return 107 | fi 108 | cf create-service $DATABASE_TYPE $DATABASE_LEVEL $DATABASE_NAME 109 | cf bind-service $BRIDGE_APP_NAME $DATABASE_NAME 110 | cf restage $BRIDGE_APP_NAME 111 | } 112 | 113 | deployContainer () { 114 | if [ -z "$1" ] || [ -z $REGISTRY_URL ] || [ -z $BRIDGE_APP_NAME ] 115 | then 116 | echo "Error: Could not deploy container to IBM Cloud, missing variables." 117 | return 118 | fi 119 | 120 | namespace=$(cf ic namespace get) 121 | hostname=$1"-app" 122 | 123 | cf ic group create \ 124 | --anti \ 125 | --auto \ 126 | -m 128 \ 127 | --name $1 \ 128 | -p 8080 \ 129 | -n $hostname \ 130 | -e "CCS_BIND_APP="$BRIDGE_APP_NAME \ 131 | -d mybluemix.net $REGISTRY_URL/$namespace/$1 132 | } 133 | 134 | populateDB () { 135 | if [ -z "$1" ] 136 | then 137 | echo "Error: Could not populate db with sample data, missing imageName." 138 | return 139 | fi 140 | 141 | appURL="https://"$1"-app.mybluemix.net" 142 | eval $(curl -X POST -H "Content-Type: application/json" -d '{ "title": "Wash the car", "order": 0, "completed": false }' $appURL) 143 | eval $(curl -X POST -H "Content-Type: application/json" -d '{ "title": "Walk the dog", "order": 2, "completed": true }' $appURL) 144 | eval $(curl -X POST -H "Content-Type: application/json" -d '{ "title": "Clean the gutters", "order": 1, "completed": false }' $appURL) 145 | } 146 | 147 | delete () { 148 | if [ -z "$1" ] || [ -z $DATABASE_NAME ] || [ -z $BRIDGE_APP_NAME ] 149 | then 150 | echo "Error: Could not delete container group and service, missing variables." 151 | return 152 | fi 153 | 154 | cf ic group rm $1 155 | cf unbind-service $BRIDGE_APP_NAME $DATABASE_NAME 156 | cf delete-service $DATABASE_NAME 157 | } 158 | 159 | all () { 160 | if [ -z "$1" ] 161 | then 162 | echo "Error: Could not complete entire deployment process, missing variables." 163 | return 164 | fi 165 | 166 | login 167 | buildDocker $1 168 | pushDocker $1 169 | createBridge 170 | createDatabase 171 | deployContainer $1 172 | } 173 | 174 | #---------------------------------------------------------- 175 | # MAIN 176 | # --------------------------------------------------------- 177 | 178 | ACTION="$1" 179 | 180 | [[ -z $ACTION ]] && help && exit 0 181 | 182 | # Initialize the SwiftEnv project environment 183 | eval "$(swiftenv init -)" 184 | 185 | 186 | case $ACTION in 187 | "install-tools") install-tools;; 188 | "login") login;; 189 | "build") buildDocker "$2";; 190 | "run") runDocker "$2";; 191 | "stop") stopDocker "$2";; 192 | "push-docker") pushDocker "$2";; 193 | "create-bridge") createBridge;; 194 | "create-db") createDatabase;; 195 | "deploy") deployContainer "$2";; 196 | "populate-db") populateDB "$2";; 197 | "delete") delete "$2";; 198 | "all") all "$2";; 199 | *) help;; 200 | esac 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TodoList CouchDB and Cloudant backend 2 | 3 | Todo backend is an example of using the [Kitura](https://github.com/IBM-Swift/Kitura) Swift framework for building a productivity app with a database for storage of tasks. 4 | 5 | [![Build Status](https://travis-ci.org/IBM-Swift/TodoList-CouchDB.svg?branch=master)](https://travis-ci.org/IBM-Swift/TodoList-CouchDB) 6 | ![](https://img.shields.io/badge/Swift-4.0%20RELEASE-orange.svg) 7 | ![](https://img.shields.io/badge/platform-Linux,%20macOS-blue.svg?style=flat) 8 | 9 | ## Quick start for local development: 10 | 11 | You can set up your development environment and use Xcode 9 for editing, building, debugging, and testing your server application. To use Xcode, you must use the command line tools for generating an Xcode project. 12 | 13 | 1. Download [Xcode 9](https://swift.org/download/) 14 | 2. Download [CouchDB](http://couchdb.apache.org/) and install 15 | 16 | ``` 17 | brew install couchdb 18 | ``` 19 | 20 | 3. Clone the TodoList CouchDB repository 21 | 22 | ``` 23 | git clone https://github.com/IBM-Swift/TodoList-CouchDB 24 | ``` 25 | 26 | 4. Generate an Xcode project 27 | 28 | ``` 29 | swift package generate-xcodeproj 30 | ``` 31 | 32 | 5. Start CouchDB 33 | 34 | ``` 35 | couchdb 36 | ``` 37 | 38 | 6. Run the `Server` target in Xcode and access [http://localhost:8080/](http://localhost:8080/) in your browser to see an empty database. 39 | 40 | ## Quick start on Linux 41 | 42 | To build the project in Linux, you need to first install the Swift 4 toolchain. 43 | 44 | 1. Install the [Swift 4 RELEASE toolchain](http://www.swift.org) 45 | 46 | 2. Install CouchDB: 47 | 48 | ``` 49 | sudo apt-get install couchdb 50 | ``` 51 | 52 | 3. Clone the repository: 53 | 54 | ``` 55 | git clone https://github.com/IBM-Swift/TodoList-CouchDB 56 | ``` 57 | 58 | 4. Compile the project 59 | ``` 60 | swift build 61 | ``` 62 | 63 | 5. Run the server: 64 | 65 | ``` 66 | .build/debug/Server 67 | ``` 68 | 69 | Then access [http://localhost:8080/](http://localhost:8080/) in your browser to see an empty database. 70 | 71 | ## Deploying to IBM Cloud 72 | 73 | ### Using the IBM Cloud Tools for Swift 74 | 75 | The TodoList for Cloudant is deployable with a graphical user interface. Download: 76 | 77 | - [IBM Cloud Application Tools for Swift](http://cloudtools.bluemix.net/) 78 | 79 | ### Deploy to IBM Cloud Button 80 | 81 | You can use this button to deploy TodoList to your IBM Cloud account, all from the browser. The button will create the application, create and bind any services specified in the manifest.yml file and deploy. 82 | 83 | [![Deploy to IBM Cloud](https://bluemix.net/deploy/button.png)](https://bluemix.net/deploy?repository=https://github.com/IBM-Swift/TodoList-CouchDB.git) 84 | 85 | ### Deploying Docker to IBM Cloud Container 86 | 87 | For the following instructions, we will be using our [Bash Script](config.sh) located in the root directory. 88 | You can attempt to complete the whole process with the following command: 89 | 90 | ``` 91 | ./config.sh all 92 | ``` 93 | 94 | Or, you can follow the step-by-step instructions below. 95 | 96 | 1. Install the Cloud Foundry CLI tool and the IBM Containers plugin for CF with the following 97 | 98 | ``` 99 | ./config.sh install-tools 100 | ``` 101 | 102 | 2. Ensure you are logged in with 103 | 104 | ``` 105 | ./config.sh login 106 | ``` 107 | 108 | 3. Build and run a Docker container with the following 109 | 110 | ``` 111 | ./config.sh build 112 | ``` 113 | To test out created Docker image, use 114 | 115 | ``` 116 | ./config.sh run 117 | ./config.sh stop 118 | ``` 119 | 120 | 4. Push created Docker container to IBM Cloud 121 | 122 | ``` 123 | ./config.sh push-docker 124 | ``` 125 | 126 | 5. Create a bridge CF application to later bind to your container 127 | 128 | ``` 129 | ./config.sh create-bridge 130 | ``` 131 | 132 | 6. Create the Cloudant service and bind to your bridge CF application. 133 | 134 | ``` 135 | ./config.sh create-db 136 | ``` 137 | 138 | 7. Create a IBM Cloud container group where your app will live, binding it to your bridge CF application in the process 139 | 140 | ``` 141 | ./config.sh deploy 142 | ``` 143 | 144 | Afterwards, you can ensure Cloudant was bound correctly by viewing all credentials for your group 145 | 146 | ``` 147 | cf ic group inspect 148 | ``` 149 | 150 | 8. Optionally, if you want to populate your database with some sample data, run the following command with your image name: 151 | 152 | ``` 153 | ./config.sh populate-db 154 | ``` 155 | 156 | At this point, your app should be deployed! Accessing your apps route should return your todos, which should be `[]` if you did not populate the database. 157 | 158 | ### Manually 159 | 160 | IBM Cloud is a hosting platform from IBM that makes it easy to deploy your app to the cloud. IBM Cloud also provides various popular databases. [Cloudant](https://cloudant.com/) is an offering that is compatible with the CouchDB database, but provides additional features. You can use Cloudant with your deployed TodoList-CouchDB application. 161 | 162 | 1. Get an account for [IBM Cloud](https://console.ng.bluemix.net/registration/) 163 | 164 | 2. Download and install the [Cloud Foundry tools](https://new-console.ng.bluemix.net/docs/starters/install_cli.html): 165 | 166 | ``` 167 | cf api https://api.ng.bluemix.net 168 | cf login 169 | ``` 170 | 171 | Be sure to run this in the directory where the manifest.yml file is located. 172 | 173 | 3. Create your Cloudant Service 174 | 175 | ``` 176 | cf create-service cloudantNoSQLDB Lite TodoListCloudantDatabase 177 | ``` 178 | 179 | 4. Push your app 180 | 181 | ``` 182 | cf push 183 | ``` 184 | 185 | ***Note** This step will take 3-5 minutes 186 | 187 | ``` 188 | 1 of 1 instances running 189 | 190 | App started 191 | ``` 192 | 193 | 5. Get the credential information: 194 | 195 | ``` 196 | cf env TodoListCloudantApp 197 | ``` 198 | 199 | Note you will see something similar to the following, note the hostname, username, and password: 200 | 201 | ```json 202 | "VCAP_SERVICES": { 203 | "cloudantNoSQLDB": [ 204 | { 205 | "credentials": { 206 | "host": "465ed079-35a8-4731-9425-911843621d7c-bluemix.cloudant.com", 207 | "password": "", 208 | "port": 443, 209 | "url": "https://465ed079-35a8-4731-9425-911843621d7c-bluemix:efe561fc02805bcb1e2b013dea4c928942951d31cd74cb2e01df3814751d9f45@465ed079-35a8-4731-9425-911843621d7c-bluemix.cloudant.com", 210 | "username": "" 211 | }, 212 | }]} 213 | ``` 214 | 215 | At this point, your app should be deployed! Accessing your apps route should return your todos, which should be `[]` to start. 216 | 217 | ## License 218 | 219 | Copyright 2017 IBM 220 | 221 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 222 | 223 | http://www.apache.org/licenses/LICENSE-2.0 224 | 225 | Unless required by applicable law or agreed to in writing, software :distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 226 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoListController.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | import Kitura 20 | import LoggerAPI 21 | import SwiftyJSON 22 | 23 | class AllRemoteOriginMiddleware: RouterMiddleware { 24 | func handle(request: RouterRequest, response: RouterResponse, next: @escaping () -> Swift.Void) { 25 | response.headers["Access-Control-Allow-Origin"] = "*" 26 | next() 27 | } 28 | } 29 | 30 | public final class TodoListController { 31 | public let todosPath = "api/todos" 32 | public let todos: TodoListAPI 33 | public let router = Router() 34 | 35 | public init(backend: TodoListAPI) { 36 | self.todos = backend 37 | setupRoutes() 38 | } 39 | 40 | private func setupRoutes() { 41 | let id = "\(todosPath)/:id" 42 | 43 | router.all("/*", middleware: BodyParser()) 44 | router.all("/*", middleware: AllRemoteOriginMiddleware()) 45 | router.get("/", handler: onGetTodos) 46 | router.get(id, handler: onGetByID) 47 | router.options("/*", handler: onGetOptions) 48 | router.post("/", handler: onAddItem ) 49 | router.post(id, handler: onUpdateByID) 50 | router.patch(id, handler: onUpdateByID) 51 | router.delete(id, handler: onDeleteByID) 52 | router.delete("/", handler: onDeleteAll) 53 | } 54 | 55 | private func onGetTodos(request: RouterRequest, response: RouterResponse, next: () -> Void) { 56 | let userID: String = "default" 57 | todos.get(withUserID: userID) { 58 | todos, error in 59 | do { 60 | guard error == nil else { 61 | try response.status(.badRequest).end() 62 | Log.error(error.debugDescription) 63 | return 64 | } 65 | guard let todos = todos else { 66 | try response.status(.internalServerError).end() 67 | return 68 | } 69 | let json = JSON(todos.toDictionary()) 70 | try response.status(.OK).send(json: json).end() 71 | } catch { 72 | Log.error("Communication error") 73 | } 74 | } 75 | } 76 | 77 | private func onGetByID(request: RouterRequest, response: RouterResponse, next: () -> Void) { 78 | guard let id = request.parameters["id"] else { 79 | response.status(.badRequest) 80 | Log.error("Request does not contain ID") 81 | return 82 | } 83 | 84 | let userID: String = "default" 85 | todos.get(withUserID: userID, withDocumentID: id) { 86 | item, error in 87 | do { 88 | guard error == nil else { 89 | try response.status(.badRequest).end() 90 | Log.error(error.debugDescription) 91 | return 92 | } 93 | if let item = item { 94 | let result = JSON(item.toDictionary()) 95 | try response.status(.OK).send(json: result).end() 96 | 97 | } else { 98 | Log.warning("Could not find the item") 99 | response.status(.badRequest) 100 | return 101 | } 102 | } catch { 103 | Log.error("Communication error") 104 | } 105 | } 106 | } 107 | 108 | /** 109 | */ 110 | private func onGetOptions(request: RouterRequest, response: RouterResponse, next: () -> Void) { 111 | response.headers["Access-Control-Allow-Headers"] = "accept, content-type" 112 | response.headers["Access-Control-Allow-Methods"] = "GET,HEAD,POST,DELETE,OPTIONS,PUT,PATCH" 113 | response.status(.OK) 114 | next() 115 | } 116 | 117 | /** 118 | */ 119 | private func onAddItem(request: RouterRequest, response: RouterResponse, next: () -> Void) { 120 | guard let body = request.body else { 121 | response.status(.badRequest) 122 | Log.error("No body found in request") 123 | return 124 | } 125 | 126 | guard case let .json(json) = body else { 127 | response.status(.badRequest) 128 | Log.error("Body contains invalid JSON") 129 | return 130 | } 131 | 132 | let userID: String = "default" 133 | let title = json["title"].stringValue 134 | let rank = json["order"].intValue 135 | let completed = json["completed"].boolValue 136 | 137 | guard title != "" else { 138 | response.status(.badRequest) 139 | Log.error("Request does not contain valid title") 140 | return 141 | } 142 | 143 | todos.add(userID: userID, title: title, rank: rank, completed: completed) { 144 | newItem, error in 145 | do { 146 | guard error == nil else { 147 | try response.status(.badRequest).end() 148 | Log.error(error.debugDescription) 149 | return 150 | } 151 | 152 | guard let newItem = newItem else { 153 | try response.status(.internalServerError).end() 154 | Log.error("Item not found") 155 | return 156 | } 157 | 158 | let result = JSON(newItem.toDictionary()) 159 | Log.info("\(userID) added \(title) to their TodoList") 160 | do { 161 | try response.status(.OK).send(json: result).end() 162 | } catch { 163 | Log.error("Error sending response") 164 | } 165 | } catch { 166 | Log.error("Communication error") 167 | } 168 | } 169 | } 170 | 171 | private func onUpdateByID(request: RouterRequest, response: RouterResponse, next: () -> Void) { 172 | guard let documentID = request.parameters["id"] else { 173 | response.status(.badRequest) 174 | Log.error("id parameter not found in request") 175 | return 176 | } 177 | 178 | guard let body = request.body else { 179 | response.status(.badRequest) 180 | Log.error("No body found in request") 181 | return 182 | } 183 | 184 | guard case let .json(json) = body else { 185 | response.status(.badRequest) 186 | Log.error("Body contains invalid JSON") 187 | return 188 | } 189 | 190 | let userID: String = "default" 191 | let title: String? = json["title"].stringValue == "" ? nil : json["title"].stringValue 192 | let rank = json["order"].intValue 193 | let completed = json["completed"].boolValue 194 | 195 | todos.update(documentID: documentID, userID: userID, title: title, rank: rank, completed: completed) { 196 | newItem, error in 197 | do { 198 | guard error == nil else { 199 | try response.status(.badRequest).end() 200 | Log.error(error.debugDescription) 201 | return 202 | } 203 | if let newItem = newItem { 204 | let result = JSON(newItem.toDictionary()) 205 | try response.status(.OK).send(json: result).end() 206 | } else { 207 | Log.error("Database returned invalid new item") 208 | try response.status(.badRequest).end() 209 | } 210 | } catch { 211 | Log.error("Communication error") 212 | } 213 | } 214 | } 215 | 216 | private func onDeleteByID(request: RouterRequest, response: RouterResponse, next: () -> Void) { 217 | guard let documentID = request.parameters["id"] else { 218 | Log.warning("Could not parse ID") 219 | response.status(.badRequest) 220 | return 221 | } 222 | 223 | let userID: String = "default" 224 | 225 | todos.delete(withUserID: userID, withDocumentID: documentID) { 226 | error in 227 | do { 228 | guard error == nil else { 229 | try response.status(.badRequest).end() 230 | Log.error(error.debugDescription) 231 | return 232 | } 233 | try response.status(.OK).end() 234 | Log.info("\(userID) deleted document \(documentID)") 235 | } catch { 236 | Log.error("Could not produce response") 237 | } 238 | } 239 | } 240 | 241 | private func onDeleteAll(request: RouterRequest, response: RouterResponse, next: () -> Void) { 242 | let userID: String = "default" 243 | todos.clearAll() { 244 | error in 245 | do { 246 | guard error == nil else { 247 | try response.status(.badRequest).end() 248 | Log.error(error.debugDescription) 249 | return 250 | } 251 | try response.status(.OK).end() 252 | Log.info("\(userID) deleted all their documents") 253 | } catch { 254 | Log.error("Could not produce response") 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Tests/TodoListTests/TestTodoList.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import XCTest 19 | 20 | @testable import TodoList 21 | 22 | class TestTodoList: XCTestCase { 23 | 24 | static var allTests: [(String, (TestTodoList) -> () throws -> Void )] { 25 | return [ 26 | ("testAddItem", testAddItem), 27 | ("testRemoveItem", testRemoveItem), 28 | ("testGetAllItems", testGetAllItems), 29 | ("testUpdateAll", testUpdateAll), 30 | ("testUpdateTitle", testUpdateTitle), 31 | ("testUpdateCompleted", testUpdateCompleted), 32 | ("testUpdateOrder", testUpdateOrder), 33 | ("testClear", testClear) 34 | ] 35 | } 36 | 37 | var todos: TodoList? 38 | 39 | override func setUp() { 40 | 41 | todos = TodoList() 42 | 43 | super.setUp() 44 | } 45 | 46 | 47 | func testAddItem() { 48 | 49 | guard let todos = todos else { 50 | XCTFail() 51 | return 52 | } 53 | 54 | let expectation1 = expectation(description: "Add first item") 55 | 56 | 57 | todos.add(userID: "testAdd", title: "Reticulate splines", rank: 0, completed: false) { 58 | firstitem, error in 59 | 60 | if let firstitem = firstitem { 61 | todos.get(withUserID: "testAdd", withDocumentID: firstitem.documentID) { 62 | fetchedtodo, error in 63 | 64 | XCTAssertEqual(firstitem, fetchedtodo) 65 | 66 | expectation1.fulfill() 67 | } 68 | } 69 | 70 | 71 | } 72 | 73 | 74 | 75 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 76 | 77 | } 78 | 79 | func testRemoveItem() { 80 | 81 | guard let todos = todos else { 82 | XCTFail() 83 | return 84 | } 85 | 86 | let expectation1 = expectation(description: "Remove item") 87 | 88 | todos.clearAll() { 89 | error in 90 | } 91 | 92 | todos.add(userID: "testRemove", title: "Reticulate splines", rank: 0, completed: true) { 93 | newitem, error in 94 | //XCTAssertEqual(todos.count, 1, "There must be 1 element in the collection") 95 | if let newitem = newitem { 96 | todos.delete(withUserID: "testRemove", withDocumentID: newitem.documentID, 97 | oncompletion: { 98 | error in 99 | 100 | todos.count(withUserID: "testRemove") { count, error in 101 | 102 | XCTAssertEqual(count, 0, "There must be 0 elements in the" + 103 | "collection after delete") 104 | expectation1.fulfill() 105 | } 106 | 107 | }) 108 | } 109 | } 110 | 111 | 112 | 113 | 114 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 115 | } 116 | 117 | 118 | func testClear() { 119 | 120 | guard let todos = todos else { 121 | XCTFail() 122 | return 123 | } 124 | 125 | let expectation1 = expectation(description: "Clear all items") 126 | 127 | todos.clearAll() { 128 | error in 129 | 130 | } 131 | todos.count(withUserID: nil) { 132 | count, error in 133 | 134 | XCTAssertEqual(count, 0) 135 | 136 | expectation1.fulfill() 137 | } 138 | 139 | 140 | 141 | 142 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 143 | } 144 | 145 | func testGetAllItems() { 146 | 147 | guard let todos = todos else { 148 | XCTFail() 149 | return 150 | } 151 | 152 | // try! todos.clear(){} 153 | 154 | let expectationGetAll = expectation(description: "Get all items") 155 | 156 | todos.clearAll() { 157 | error in 158 | 159 | todos.add(userID: "testGetAll", title: "Reticulate splines", rank: 0, completed: true) { 160 | _, error in 161 | 162 | 163 | todos.get(withUserID: "testGetAll") { 164 | results, error in 165 | 166 | if let results = results { 167 | XCTAssertEqual(results.count, 1, "There must be at least 1 element in the" + 168 | "collection") 169 | } 170 | 171 | expectationGetAll.fulfill() 172 | } 173 | 174 | 175 | } 176 | 177 | } 178 | 179 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 180 | 181 | 182 | } 183 | 184 | func testUpdateAll() { 185 | 186 | guard let todos = todos else { 187 | XCTFail() 188 | return 189 | } 190 | 191 | todos.clearAll() { 192 | error in 193 | } 194 | 195 | let addExpectation = expectation(description: "Add item") 196 | let updateExpectation = expectation(description: "Update item") 197 | 198 | 199 | todos.add(userID: "testUpdateAll", title: "Reticulate splines", rank: 5, completed: false) { 200 | result, error in 201 | 202 | addExpectation.fulfill() 203 | 204 | if let result = result { 205 | todos.update(documentID: result.documentID, userID: "testUpdateAll", 206 | title: "Obfuscate dictionary", rank: 2, completed: true) { 207 | 208 | updatedItem, error in 209 | 210 | let correctItem = TodoItem(documentID: result.documentID, 211 | userID: "testUpdateAll", 212 | rank: 2, 213 | title: "Obfuscate dictionary", 214 | completed: true) 215 | 216 | XCTAssertEqual(updatedItem, correctItem) 217 | 218 | updateExpectation.fulfill() 219 | } 220 | } 221 | } 222 | 223 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 224 | 225 | } 226 | 227 | func testUpdateTitle() { 228 | 229 | guard let todos = todos else { 230 | XCTFail() 231 | return 232 | } 233 | 234 | todos.clearAll() { 235 | error in 236 | } 237 | 238 | let addExpectation = expectation(description: "Add item") 239 | let updateExpectation = expectation(description: "Update item") 240 | 241 | todos.add(userID: "testUpdateTitle", title: "Reticulate splines", rank: 5, 242 | completed: false) { 243 | result, error in 244 | 245 | addExpectation.fulfill() 246 | 247 | if let result = result { 248 | todos.update(documentID: result.documentID, userID: "testUpdateTitle", 249 | title: "Obfuscate dictionary", rank: nil, completed: nil) { 250 | updatedItem, error in 251 | 252 | let correctItem = TodoItem(documentID: result.documentID, 253 | userID: "testUpdateTitle", rank: 5, 254 | title: "Obfuscate dictionary", 255 | completed: false) 256 | let originalItem = TodoItem(documentID: result.documentID, 257 | userID: "testUpdateTitle", 258 | rank: 5, 259 | title: "Reticulate splines", 260 | completed: false) 261 | 262 | XCTAssertEqual(updatedItem, correctItem) 263 | XCTAssertNotEqual(updatedItem, originalItem) 264 | 265 | updateExpectation.fulfill() 266 | } 267 | } 268 | } 269 | 270 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 271 | 272 | } 273 | 274 | func testUpdateCompleted() { 275 | 276 | guard let todos = todos else { 277 | XCTFail() 278 | return 279 | } 280 | 281 | todos.clearAll() { 282 | error in 283 | } 284 | 285 | let addExpectation = expectation(description: "Add item") 286 | let updateExpectation = expectation(description: "Update item") 287 | 288 | todos.add(userID: "testUpdateCompleted", title: "Reticulate splines", rank: 5, 289 | completed: false) { 290 | result, error in 291 | 292 | addExpectation.fulfill() 293 | 294 | if let result = result { 295 | todos.update(documentID: result.documentID, userID: "testUpdateCompleted", 296 | title: nil, rank: nil, completed: true) { 297 | updatedItem, error in 298 | 299 | let correctItem = TodoItem(documentID: result.documentID, 300 | userID: "testUpdateCompleted", 301 | rank: 5, 302 | title: "Reticulate splines", 303 | completed: true) 304 | let originalItem = TodoItem(documentID: result.documentID, 305 | userID: "testUpdateCompleted", 306 | rank: 5, title: "Reticulate splines", 307 | completed: false) 308 | 309 | XCTAssertEqual(updatedItem, correctItem) 310 | XCTAssertNotEqual(updatedItem, originalItem) 311 | 312 | updateExpectation.fulfill() 313 | } 314 | } 315 | } 316 | 317 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 318 | 319 | } 320 | 321 | func testUpdateOrder() { 322 | 323 | guard let todos = todos else { 324 | XCTFail() 325 | return 326 | } 327 | 328 | let addExpectation = expectation(description: "Add item") 329 | let updateExpectation = expectation(description: "Update item") 330 | 331 | todos.clearAll() { 332 | error in 333 | } 334 | 335 | todos.add(userID: "testUpdateOrder", title: "Reticulate splines", rank: 5, 336 | completed: false) { 337 | result, error in 338 | 339 | addExpectation.fulfill() 340 | 341 | if let result = result { 342 | todos.update(documentID: result.documentID, userID: "testUpdateOrder", 343 | title: nil, rank: 12, completed: nil) { 344 | updatedItem, error in 345 | 346 | let correctItem = TodoItem( 347 | documentID: result.documentID, 348 | userID: "testUpdateOrder", 349 | rank: 12, 350 | title: "Reticulate splines", 351 | completed: false) 352 | let originalItem = TodoItem( 353 | documentID: result.documentID, 354 | userID: "testUpdateOrder", 355 | rank: 5, 356 | title: "Reticulate splines", 357 | completed: false) 358 | 359 | XCTAssertEqual(updatedItem, correctItem) 360 | XCTAssertNotEqual(updatedItem, originalItem) 361 | 362 | updateExpectation.fulfill() 363 | } 364 | } 365 | } 366 | 367 | 368 | waitForExpectations(timeout: 5, handler: { error in XCTAssertNil(error, "Timeout") }) 369 | 370 | } 371 | 372 | } 373 | -------------------------------------------------------------------------------- /Sources/TodoList/TodoList.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | import LoggerAPI 20 | import SwiftyJSON 21 | import Dispatch 22 | 23 | import CouchDB 24 | 25 | #if os(Linux) 26 | typealias Valuetype = Any 27 | #else 28 | typealias Valuetype = AnyObject 29 | #endif 30 | 31 | enum TodoListError: LocalizedError { 32 | case databaseAlreadyExists 33 | } 34 | 35 | enum Result { 36 | case success(T) 37 | case error(Error) 38 | } 39 | 40 | /// TodoList for CouchDB 41 | public class TodoList: TodoListAPI { 42 | 43 | public static let defaultCouchHost = "127.0.0.1" 44 | public static let defaultCouchPort = UInt16(5984) 45 | public static let defaultDatabaseName = "todolist" 46 | 47 | let databaseName = "todolist" 48 | let designName = "todosdesign" 49 | let connectionProperties: ConnectionProperties 50 | let queue = DispatchQueue(label: "com.ibm.todolist", qos: .userInitiated, attributes: .concurrent) 51 | 52 | public init(_ dbConfiguration: DatabaseConfiguration) { 53 | connectionProperties = ConnectionProperties(host: dbConfiguration.host!, 54 | port: Int16(dbConfiguration.port!), 55 | secured: true, 56 | username: dbConfiguration.username, 57 | password: dbConfiguration.password) 58 | setupDB() 59 | } 60 | 61 | public init(database: String = TodoList.defaultDatabaseName, 62 | host: String = TodoList.defaultCouchHost, 63 | port: UInt16 = TodoList.defaultCouchPort, 64 | username: String? = nil, password: String? = nil) { 65 | 66 | let secured = (host == TodoList.defaultCouchHost) ? false : true 67 | connectionProperties = ConnectionProperties(host: host, port: Int16(port), secured: secured, 68 | username: username, password: password) 69 | setupDB() 70 | } 71 | 72 | public func count(withUserID: String? = nil, oncompletion: @escaping (Int?, Error?) -> Void) { 73 | 74 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 75 | let database = couchDBClient.database(databaseName) 76 | let userParameter = withUserID ?? "default" 77 | 78 | database.queryByView("user_todos", ofDesign: designName, 79 | usingParameters: [.keys([userParameter as Valuetype])]) { 80 | document, error in 81 | 82 | if let document = document , error == nil { 83 | if let numberOfTodos = document["rows"][0]["value"].int { 84 | oncompletion(numberOfTodos, nil) 85 | } else { 86 | oncompletion(0, nil) 87 | } 88 | } else { 89 | oncompletion(nil, error) 90 | } 91 | } 92 | } 93 | 94 | public func clear(withUserID: String? = nil, oncompletion: @escaping (Error?) -> Void) { 95 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 96 | let database = couchDBClient.database(databaseName) 97 | let userParameter = withUserID ?? "default" 98 | 99 | database.queryByView("user_todos", ofDesign: designName, usingParameters: [ 100 | .descending(true), .includeDocs(true), 101 | .keys([userParameter as Valuetype])]) { 102 | document, error in 103 | 104 | guard let document = document else { 105 | oncompletion(error) 106 | return 107 | } 108 | guard let idRevs = try? parseGetIDandRev(document) else { 109 | oncompletion(error) 110 | return 111 | } 112 | 113 | let count = idRevs.count 114 | if count == 0 { 115 | oncompletion( nil ) 116 | } else { 117 | var numberCompleted = 0 118 | for i in 0...count-1 { 119 | let item = idRevs[i] 120 | database.delete(item.0, rev: item.1) { 121 | error in 122 | if error != nil { 123 | oncompletion(error) 124 | return 125 | } 126 | numberCompleted += 1 127 | if numberCompleted == count { 128 | oncompletion( nil ) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | public func clearAll(oncompletion: @escaping (Error?) -> Void) { 137 | 138 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 139 | let database = couchDBClient.database(databaseName) 140 | 141 | database.queryByView("all_todos", ofDesign: designName, 142 | usingParameters: [.descending(true), .includeDocs(true)]) { 143 | document, error in 144 | 145 | guard let document = document else { 146 | oncompletion(error) 147 | return 148 | } 149 | 150 | guard let idRevs = try? parseGetIDandRev(document) else { 151 | oncompletion(error) 152 | return 153 | } 154 | 155 | let count = idRevs.count 156 | if count == 0 { 157 | oncompletion(nil) 158 | } else { 159 | var numberCompleted = 0 160 | 161 | for i in 0...count-1 { 162 | let item = idRevs[i] 163 | 164 | database.delete(item.0, rev: item.1) { 165 | error in 166 | 167 | if error != nil { 168 | oncompletion(error) 169 | return 170 | } 171 | 172 | numberCompleted += 1 173 | 174 | if numberCompleted == count { 175 | oncompletion(nil) 176 | } 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | public func get(withUserID: String?, oncompletion: @escaping ([TodoItem]?, Error?) -> Void ) { 184 | 185 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 186 | let database = couchDBClient.database(databaseName) 187 | let userParameter = withUserID ?? "default" 188 | 189 | database.queryByView("user_todos", ofDesign: designName, 190 | usingParameters: [.descending(true), .includeDocs(true), 191 | .keys([userParameter as Valuetype])]) { 192 | document, error in 193 | 194 | if let document = document , error == nil { 195 | 196 | do { 197 | let todoItems = try parseTodoItemList(document) 198 | oncompletion(todoItems, nil) 199 | } catch { 200 | oncompletion(nil, error) 201 | 202 | } 203 | 204 | } else { 205 | oncompletion(nil, error) 206 | } 207 | 208 | 209 | } 210 | 211 | } 212 | 213 | public func get(withUserID: String?, withDocumentID: String, 214 | oncompletion: @escaping (TodoItem?, Error?) -> Void ) { 215 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 216 | let database = couchDBClient.database(databaseName) 217 | let withUserID = withUserID ?? "default" 218 | 219 | database.retrieve(withDocumentID) { 220 | document, error in 221 | 222 | guard let document = document else { 223 | oncompletion(nil, error) 224 | return 225 | } 226 | 227 | guard let userID = document["user"].string else { 228 | oncompletion(nil, error) 229 | return 230 | } 231 | 232 | guard withUserID == userID else { 233 | oncompletion(nil, TodoCollectionError.AuthError) 234 | return 235 | } 236 | 237 | guard let documentID = document["_id"].string else { 238 | oncompletion(nil, error) 239 | return 240 | } 241 | 242 | guard let title = document["title"].string else { 243 | oncompletion(nil, error) 244 | return 245 | } 246 | 247 | guard let rank = document["rank"].int else { 248 | oncompletion(nil, error) 249 | return 250 | } 251 | 252 | guard let completed = document["completed"].bool else { 253 | oncompletion(nil, error) 254 | return 255 | } 256 | 257 | //let completedValue = completed == 1 ? true : false 258 | 259 | let todoItem = TodoItem(documentID: documentID, userID: userID, rank: rank, 260 | title: title, completed: completed) 261 | 262 | oncompletion(todoItem, nil) 263 | } 264 | 265 | } 266 | 267 | public func add(userID: String?, title: String, rank: Int = 0, completed: Bool = false, 268 | oncompletion: @escaping (TodoItem?, Error?) -> Void ) { 269 | 270 | let userID = userID ?? "default" 271 | let json: [String: Any] = [ 272 | "type": "todo", 273 | "user": userID, 274 | "title": title, 275 | "rank": rank, 276 | "completed": completed 277 | ] 278 | 279 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 280 | let database = couchDBClient.database(databaseName) 281 | print("String[]: \(json)") 282 | let x = JSON(json) 283 | print("JSON: \(x.rawString() as String?)") 284 | database.create(JSON(json)) { 285 | id, rev, document, error in 286 | 287 | if let id = id { 288 | let todoItem = TodoItem(documentID: id, userID: userID, rank: rank, 289 | title: title, completed: completed) 290 | 291 | oncompletion(todoItem, nil) 292 | } else { 293 | oncompletion(nil, error) 294 | } 295 | 296 | } 297 | } 298 | 299 | public func update(documentID: String, userID: String?, title: String?, 300 | rank: Int?, completed: Bool?, oncompletion: @escaping (TodoItem?, Error?) -> Void ) { 301 | 302 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 303 | let database = couchDBClient.database(databaseName) 304 | 305 | let userID = userID ?? "default" 306 | 307 | database.retrieve(documentID) { 308 | document, error in 309 | 310 | guard let document = document else { 311 | oncompletion(nil, TodoCollectionError.AuthError) 312 | return 313 | } 314 | 315 | guard userID == document["user"].string else { 316 | oncompletion(nil, TodoCollectionError.ParseError) 317 | return 318 | } 319 | 320 | guard let rev = document["_rev"].string else { 321 | oncompletion(nil, TodoCollectionError.ParseError) 322 | return 323 | } 324 | 325 | let type = "todo" 326 | let user = userID 327 | let title = title ?? document["title"].string! 328 | let rank = rank ?? document["rank"].int! 329 | let completed = completed ?? document["completed"].bool! 330 | /*var completedValue : Int 331 | 332 | if let completed = completed { 333 | completedValue = completed ? 1 : 0 334 | } else { 335 | completedValue = document["completed"].int! 336 | } 337 | 338 | let completedBool = completedValue == 1 ? true : false*/ 339 | 340 | let json: [String: Any] = [ 341 | "type": type, 342 | "user": user, 343 | "title": title, 344 | "rank": rank, 345 | "completed": completed 346 | ] 347 | 348 | database.update(documentID, rev: rev, document: JSON(json)) { 349 | rev, document, error in 350 | 351 | guard error == nil else { 352 | oncompletion(nil, error) 353 | return 354 | } 355 | oncompletion (TodoItem(documentID: documentID, userID: user, rank: rank, title: title, completed: completed), nil) 356 | } 357 | } 358 | } 359 | 360 | public func delete(withUserID: String?, withDocumentID: String, oncompletion: @escaping (Error?) -> Void) { 361 | 362 | let couchDBClient = CouchDBClient(connectionProperties: connectionProperties) 363 | let database = couchDBClient.database(databaseName) 364 | let withUserID = withUserID ?? "default" 365 | 366 | database.retrieve(withDocumentID) { 367 | document, error in 368 | 369 | guard let document = document else { 370 | oncompletion(error) 371 | return 372 | } 373 | 374 | let rev = document["_rev"].string! 375 | let user = document["user"].string! 376 | 377 | if withUserID == user { 378 | database.delete( withDocumentID, rev: rev) { 379 | error in 380 | oncompletion(nil) 381 | } 382 | } 383 | } 384 | } 385 | 386 | private func setupDatabaseDesign(db:Database) { 387 | let design : [String:Any] = [ 388 | "_id": "_design/todosdesign", 389 | "views" : [ 390 | "all_todos" : [ 391 | "map" : "function(doc) { if (doc.type == 'todo') { emit(doc._id, [doc._id, doc.user, doc.title, doc.completed, doc.rank]); }}" 392 | ], 393 | "user_todos": [ 394 | "map": "function(doc) { if (doc.type == 'todo') { emit(doc.user, [doc._id, doc.user, doc.title, doc.completed, doc.rank]); }}" 395 | ], 396 | "total_todos": [ 397 | "map" : "function(doc) { if (doc.type == 'todo') { emit(doc.id, 1); }}", 398 | "reduce" : "_count" 399 | ] 400 | ] 401 | ] 402 | 403 | db.createDesign(self.designName, document: JSON(design), callback: { 404 | json, error in 405 | 406 | if (error != nil) { 407 | Log.error("Bad news! Creating design caused a failure: \(error as Error?)") 408 | } else { 409 | Log.info("Good news! We created the design \(json as JSON?)") 410 | } 411 | }) 412 | } 413 | 414 | private func setupDB() { 415 | let couchDBClient = CouchDBClient(connectionProperties: self.connectionProperties) 416 | couchDBClient.dbExists(databaseName, callback: { 417 | exists, error in 418 | 419 | if (exists) { 420 | Log.info("Good news! Database exists") 421 | } else { 422 | Log.error("Bad news! Database does not exist \(error as Error?)") 423 | couchDBClient.createDB(self.databaseName, callback: { 424 | database, error in 425 | 426 | if (database != nil) { 427 | Log.info("Good news! We created the database") 428 | self.setupDatabaseDesign(db: database!) 429 | } else { 430 | Log.error("Bad news! We were not able to create the database \(self.databaseName) due to error: \(error as Error?)") 431 | } 432 | }) 433 | } 434 | }) 435 | } 436 | } 437 | 438 | func parseGetIDandRev(_ document: JSON) throws -> [(String, String)] { 439 | guard let rows = document["rows"].array else { 440 | throw TodoCollectionError.ParseError 441 | } 442 | 443 | return rows.flatMap { 444 | let doc = $0["doc"] 445 | let id = doc["_id"].string! 446 | let rev = doc["_rev"].string! 447 | return (id, rev) 448 | } 449 | } 450 | 451 | func parseTodoItemList(_ document: JSON) throws -> [TodoItem] { 452 | guard let rows = document["rows"].array else { 453 | throw TodoCollectionError.ParseError 454 | } 455 | 456 | let todos: [TodoItem] = rows.flatMap { 457 | let doc = $0["value"] 458 | guard let id = doc[0].string, 459 | let user = doc[1].string, 460 | let title = doc[2].string, 461 | let completed = doc[3].bool, 462 | let rank = doc[4].int else { 463 | return nil 464 | } 465 | return TodoItem(documentID: id, userID: user, rank: rank, title: title, completed: completed) 466 | } 467 | return todos 468 | } 469 | --------------------------------------------------------------------------------