├── Configuration ├── .codebeatignore ├── .codecov.yml ├── .gitignore ├── .swiftlint.yml └── .circleci │ ├── vapor1 │ └── config.yml │ ├── vapor3 │ └── config.yml │ └── vapor2 │ └── config.yml ├── Documentation ├── how-to-deploy-and-host.md ├── tips-and-tricks.md ├── how-to-create-an-erd.md ├── how-to-release-a-version.md ├── how-to-work-with-vapor-2-projects.md ├── how-to-create-postman-documentation.md ├── how-to-support-emojis.md ├── checklist-before-going-live.md ├── how-to-build-a-package.md ├── how-to-report-to-bugsnag.md ├── how-to-use-a-proxy.md ├── how-to-upload-files.md ├── how-to-setup-environment-variables.md ├── how-to-start-a-project.md ├── how-to-create-postman-collections.md ├── how-to-urban-airship-push.md ├── how-to-create-postman-tests.md ├── guide-how-to-write-swift.md ├── how-to-firebase-cloud-message.md └── how-to-write-apis.md ├── Assets ├── nodeslogo.png └── vaporlogo.png ├── .gitignore └── README.md /Configuration/.codebeatignore: -------------------------------------------------------------------------------- 1 | Public/** 2 | Resources/Assets/** 3 | -------------------------------------------------------------------------------- /Documentation/how-to-deploy-and-host.md: -------------------------------------------------------------------------------- 1 | # How to deploy and host Vapor apps 2 | 3 | TODO -------------------------------------------------------------------------------- /Assets/nodeslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/readme/HEAD/Assets/nodeslogo.png -------------------------------------------------------------------------------- /Assets/vaporlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/readme/HEAD/Assets/vaporlogo.png -------------------------------------------------------------------------------- /Configuration/.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "0...100" 3 | ignore: 4 | - "Tests" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .idea 4 | xcuserdata 5 | *.xcodeproj 6 | Config/secrets/ 7 | .DS_Store 8 | node_modules/ 9 | bower_components/ 10 | .swift-version 11 | CMakeLists.txt 12 | -------------------------------------------------------------------------------- /Configuration/.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .idea 4 | xcuserdata 5 | *.xcodeproj 6 | Config/secrets/ 7 | .DS_Store 8 | node_modules/ 9 | bower_components/ 10 | .swift-version 11 | CMakeLists.txt 12 | -------------------------------------------------------------------------------- /Configuration/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | function_body_length: 4 | warning: 60 5 | variable_name: 6 | min_length: 7 | warning: 2 8 | line_length: 100 9 | disabled_rules: 10 | - opening_brace 11 | - nesting 12 | colon: 13 | flexible_right_spacing: true 14 | -------------------------------------------------------------------------------- /Documentation/tips-and-tricks.md: -------------------------------------------------------------------------------- 1 | # Tips and tricks 2 | 3 | ## Kill Vapor process, e.g. after Xcode crash 4 | Asking for processes on port `8080` and then kill the `PID`: 5 | ``` 6 | -> lsof -i :8080 7 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 8 | App 38017 casper_r 4u IPv4 0xb334c0f620b8c201 0t0 TCP *:http-alt (LISTEN) 9 | 10 | -> kill 38017 11 | ``` 12 | 13 | ## Retrieve client IP 14 | ```swift 15 | try request.peerAddress?.address() 16 | ``` 17 | 18 | ## URLs in `Package.swift` 19 | Make sure that all URLs for packages in `Package.swift` uses `https` and ends with `.git`. 20 | -------------------------------------------------------------------------------- /Documentation/how-to-create-an-erd.md: -------------------------------------------------------------------------------- 1 | # How to create an entity relationship diagram 2 | 3 | To get a better overview of our database structures we use ERDs. This small guide explains how to easily and automatically generate it from the command line. 4 | 5 | ## Install Graphviz 6 | 7 | ##### source: https://www.norbauer.com/rails-consulting/notes/erd-diagrams-from-sequel-pro.html 8 | ```bash 9 | brew install graphviz 10 | ``` 11 | 12 | ## Export database structure from Sequel Pro 13 | `File > Export > Dot` 14 | 15 | **Note:** uncheck "Use case-insentive [sic] links" 16 | 17 | ## Create a diagram as PNG 18 | ``` 19 | dot -Tpng myDatabase.dot > erd.png 20 | ``` 21 | -------------------------------------------------------------------------------- /Configuration/.circleci/vapor1/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | Linux: 4 | docker: 5 | - image: brettrtoomey/vapor1-ci:0.0.2 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v2-spm-deps-{{ checksum "Package.pins" }} 11 | - run: 12 | name: Copy Pins Package 13 | command: cp Package.pins res 14 | - run: 15 | name: Build and Run Tests 16 | no_output_timeout: 1800 17 | command: | 18 | swift test -Xswiftc -DNOJSON 19 | - run: 20 | name: Restoring Pins Package 21 | command: mv res Package.pins 22 | - save_cache: 23 | key: v2-spm-deps-{{ checksum "Package.pins" }} 24 | paths: 25 | - .build 26 | workflows: 27 | version: 2 28 | build-and-test: 29 | jobs: 30 | - Linux 31 | experimental: 32 | notify: 33 | branches: 34 | only: 35 | - master 36 | - develop 37 | -------------------------------------------------------------------------------- /Documentation/how-to-release-a-version.md: -------------------------------------------------------------------------------- 1 | # How to release a version 2 | 3 | When we release a package, we try to align the format and naming of these releases to make it easier to work and use our different packages. This document describes how we create a release on GitHub. 4 | 5 | ## Tagging a version 6 | 7 | We use semantic versioning for tagging our releases. See the [official docs](https://semver.org/) for more information. Unless we have branches that point to different versions of e.g. Vapor, we usually tag from the `master` branch. If the version is not stable yet (e.g. a beta release) we mark it as a "Pre-release". 8 | 9 | ## Creating release notes 10 | 11 | ### Title 12 | 13 | Our release title follows our version tag. Some examples: 14 | 15 | - `0.0.1` becomes "Version 0.0.1" 16 | - `1.1.0-beta.1` becomes "Version 1.1.0 Beta 1" 17 | - `2.0.0` could become "Version 2.0.0" 18 | 19 | ### Body 20 | 21 | We use the headlines and the order from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for describing our release. We use `h3` tags for the different headlines and we use bullets for describing the different changes within each headline. E.g: 22 | 23 | ``` 24 | ### Fixed 25 | - Some bug which causes this and this 26 | ``` 27 | -------------------------------------------------------------------------------- /Documentation/how-to-work-with-vapor-2-projects.md: -------------------------------------------------------------------------------- 1 | # How to work with Vapor 2 projects 2 | 3 | ## MySQL 4 | 5 | MySQL's current version is 8 but for Vapor 2 projects you need to have mysql 5.7 installed: 6 | 7 | ``` 8 | brew install mysql@5.7 9 | ``` 10 | 11 | If you have MySQL 8 installed as well you will get CMySQL related build errors. To solve this you will first have to tell Homebrew to use 5.7 by default: 12 | 13 | ``` 14 | brew link mysql@5.7 --force 15 | ``` 16 | 17 | But this is not quite enough because there is a file `/usr/local/opt/mysql` which is a symlink to MySQL 8 and this is where Swift looks for the MySQL header files. You can remove this file and create a link to 5.7 like so: 18 | 19 | ``` 20 | cd /usr/local/opt 21 | rm mysql 22 | ln -s mysql@5.7 mysql 23 | ``` 24 | 25 | After this clean and rebuild your project. 26 | 27 | ## Xcode 28 | Vapor 2 projects cannot be built in Xcode 10.2 and later. You should therefore install [Xcode 10.1](https://developer.apple.com/download/more/) under `/Application/Xcode10.1.app` (or to a name of your choosing). No need to add any addition toolchains, just open your project in the newly installed Xcode. 29 | 30 | Once you have the older version of Xcode installed, you should set the local Swift version to 4.1.2. This can be done in Xcode through Settings -> Locations -> Command Line tools (Choose Xcode 10.1) or in the command line using `xcode-select -s /Applications/Xcode10.1.app` (or adjusted to the location where you installed Xcode 10.1). 31 | -------------------------------------------------------------------------------- /Documentation/how-to-create-postman-documentation.md: -------------------------------------------------------------------------------- 1 | # How to create Postman documentation 2 | 3 | Below is an example for how to write Postman documentation. Looking at the raw representation of this file, one should be able to copy/paste this into the documentation for an endpoint. If a section is not relevant for a particular endpoint, please fill in n/a (to make it more clear that it was not just forgotten). 4 | 5 | _Please note that each section in this example might not make sense for a real endpoint, but its purpose is just to give guidance on how to fill out each section._ 6 | 7 | --- 8 | 9 | Getting a list of users optionally filtered by type. 10 | 11 | ### Request details 12 | 13 | Method: `GET` 14 | 15 | URL: `/api/users/:id` 16 | 17 | ### Headers 18 | 19 | `Authorization` (`String`) **required**: Access token (bearer format). 20 | 21 | `N-Meta` (`String`) **required**: N-Meta header. 22 | 23 | ### URL Parameters 24 | 25 | `id` (`Int`) **required**: ID for user. 26 | 27 | ### Query parameters 28 | 29 | `lastId` (`Int`): Last ID in the collection of entries. 30 | 31 | `limit` (`Int`): Number of entries to return (default: 10). 32 | 33 | `typeIds` (`[Int]`): Filtering of types (default: all). 34 | 35 | ### Parameters 36 | 37 | `email` (`String`) **required**: Email for user. 38 | 39 | `password` (`String`) **required**: Password for user. 40 | 41 | `name` (`String`): User's name. 42 | 43 | ### Response codes 44 | 45 | `200` OK 46 | 47 | `401` Unauthorized 48 | 49 | `412` Precondition failed 50 | -------------------------------------------------------------------------------- /Configuration/.circleci/vapor3/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | MacOS: 4 | macos: 5 | xcode: "10.0.0" 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-spm-deps-{{ checksum "Package.resolved" }} 11 | - run: 12 | name: Install dependencies 13 | command: | 14 | export HOMEBREW_NO_AUTO_UPDATE=1 15 | brew tap vapor/homebrew-tap 16 | brew install cmysql 17 | brew install ctls 18 | brew install libressl 19 | brew install cstack 20 | - run: 21 | name: Build and Run Tests 22 | no_output_timeout: 1800 23 | command: | 24 | swift test -Xswiftc -DNOJSON 25 | - save_cache: 26 | key: v1-spm-deps-{{ checksum "Package.resolved" }} 27 | paths: 28 | - .build 29 | Linux: 30 | docker: 31 | - image: nodesvapor/vapor-ci:swift-4.2 32 | steps: 33 | - checkout 34 | - restore_cache: 35 | keys: 36 | - v2-spm-deps-{{ checksum "Package.resolved" }} 37 | - run: 38 | name: Copy Resolved Package 39 | command: cp Package.resolved res 40 | - run: 41 | name: Build and Run Tests 42 | no_output_timeout: 1800 43 | command: | 44 | swift test -Xswiftc -DNOJSON 45 | - run: 46 | name: Restoring Resolved Package 47 | command: mv res Package.resolved 48 | - save_cache: 49 | key: v2-spm-deps-{{ checksum "Package.resolved" }} 50 | paths: 51 | - .build 52 | workflows: 53 | version: 2 54 | build-and-test: 55 | jobs: 56 | - MacOS 57 | - Linux 58 | experimental: 59 | notify: 60 | branches: 61 | only: 62 | - master 63 | - develop 64 | -------------------------------------------------------------------------------- /Documentation/how-to-support-emojis.md: -------------------------------------------------------------------------------- 1 | 2 | # How to support emojis (utf8mb4) 3 | 4 | ### A little background 5 | 6 | On MySQL, the max key length is 767 bytes. We need pay attention to all the keys on the database that are based specially on the **VARCHAR** type. 7 | 8 | For example: 9 | 10 | - *utf8*, each character is 3 bytes. So a varchar(255) = 255 * 3 = 765 bytes (**OK**, under the limit). 11 | 12 | - *utf8mb4*, each character is 4 bytes. So a varchar(255) = 255 * 4 = 1020 bytes (**NOT OK**) 13 | 14 | While running a migration you won’t see MySQL 5.7 complaining about this, and everything will be created as you would expect. Except for the keys that break the limit. (MySQL fails that silently?) 15 | 16 | ### Options 17 | 18 | 2 options may be considered: 19 | 20 | - Option 1: Do you really need 255 characters on that column? 21 | 22 | A good example of this is the PHP nodes/backend package. We have backend_roles table with the column slug varchar(255) and a reference to this column on the backend_users table, on the column user_role varchar(255). Do we really need a role slug to be 255 characters? We can reduce it to 191 characters and everything would be ok (191 * 4 = 764 bytes). 23 | 24 | This would represent a slug like this: 25 | 35qh8PXbQeYxtfv5k3ZtaZChucgHm4GuWSFCum80oa4JQYBSFfEqn9ffEK378MIbmhVpGbhpVnLx5mk9MlLfVK05f3yrydwVBddMKoecA4rzFiaqWcrzrgf2yCH8GnmbEqC4Dk7ZZkVV7VEci32n0X1DqtmhDluuOjwkPrIxXeYsbotvgtkZ1bW6SEp0leB 26 | 27 | Enough? :) 28 | 29 | - Option2: We really need 255 characters! 30 | 31 | If we really need all 255 characters, we can consider using this innodb setting: 32 | http://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html#sysvar_innodb_large_prefix 33 | 34 | ### How to enable utf8mb4 support on Vapor? 35 | 36 | If your project is deployed to Vapor Cloud, then `utf8mb4` will be the default encoding on `VARCHAR` fields. 37 | -------------------------------------------------------------------------------- /Configuration/.circleci/vapor2/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | MacOS: 4 | macos: 5 | xcode: "9.4.0" 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-spm-deps-{{ checksum "Package.resolved" }} 11 | - run: 12 | name: Install dependencies 13 | # A specific version of MySQL (5.7.22) is being installed due to MySQL 8 issues with Vapor 2 14 | command: | 15 | export HOMEBREW_NO_AUTO_UPDATE=1 16 | brew tap vapor/homebrew-tap 17 | brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/a340bfac3b7abff408a6b6fe6fdc39a38ab94871/Formula/mysql.rb 18 | brew install cmysql 19 | brew install ctls 20 | brew install cstack 21 | brew switch mysql 5.7.22 22 | - run: 23 | name: Build and Run Tests 24 | no_output_timeout: 1800 25 | command: | 26 | swift test -Xswiftc -DNOJSON 27 | - save_cache: 28 | key: v1-spm-deps-{{ checksum "Package.resolved" }} 29 | paths: 30 | - .build 31 | Linux: 32 | docker: 33 | - image: nodesvapor/vapor-ci:swift-4.1 34 | steps: 35 | - checkout 36 | - restore_cache: 37 | keys: 38 | - v2-spm-deps-{{ checksum "Package.resolved" }} 39 | - run: 40 | name: Copy Resolved Package 41 | command: cp Package.resolved res 42 | - run: 43 | name: Build and Run Tests 44 | no_output_timeout: 1800 45 | command: | 46 | swift test -Xswiftc -DNOJSON 47 | - run: 48 | name: Restoring Resolved Package 49 | command: mv res Package.resolved 50 | - save_cache: 51 | key: v2-spm-deps-{{ checksum "Package.resolved" }} 52 | paths: 53 | - .build 54 | workflows: 55 | version: 2 56 | build-and-test: 57 | jobs: 58 | - MacOS 59 | - Linux 60 | experimental: 61 | notify: 62 | branches: 63 | only: 64 | - master 65 | - develop 66 | -------------------------------------------------------------------------------- /Documentation/checklist-before-going-live.md: -------------------------------------------------------------------------------- 1 | # Checklist before going live with a project 2 | 3 | When a project is ready to be deployed and go live, there's a couple of things one needs to remember to have in place - this is good place to start, although it might not cover everything for your project. 4 | 5 | ## What to check before going live 6 | 7 | - **Redis**: Check that the provider has been added and Redis is being used for sessions in `droplet.json`. Also make sure that the sessions middleware has been added. 8 | - **Bugsnag**: Make sure that the project has been created (on Bugsnag) and the correct credentials has been added to all environments using environment variables. Make sure that the Bugsnag middleware has been added and consider doing a simple test to see if errors are being reported correctly. 9 | - **Meta**: Check that the middleware has been added and it is being enforced when doing requests. 10 | - **Crypto**: Check that the keys for hashers and ciphers has been generated correctly. Consider if it is needed to move the keys to environment variables to ensure that generated tokens cannot be used across different environments. 11 | - **SSO**: If your project includes an admin panel, then make sure to add Nodes SSO as well. Remember to create credentials for the project and make sure that they are added to all environments as environment variables. 12 | - **Storage**: If file uploads are supported, make sure that the Storage credentials has been correctly added to all environments as environment variables. 13 | - **Mail**: If your project supports sending mails, make sure all mail related credentials has been setup correctly as environment variables on all environments. 14 | - **CORS**: The default Nodes template comes with the CORS middleware installed and configured. Consider whether or not you need to limit the domains and/or remove from or add to the list of allowed headers. 15 | - **Continuous Integration**: Is your repo on GitHub running correctly with Circle CI? Is your master/develop branches protected? 16 | - **Debug logs**: If you have debug logs turned on (e.g. in `fluent.json`) while developing the project, consider turning them off before deploying. 17 | - **Signers**: Are you using JWT and will be running the project on multiple environments? If so, remember to generate keys for each environment. Consider adding the keys as different config files (opposed to environment variables). 18 | -------------------------------------------------------------------------------- /Documentation/how-to-build-a-package.md: -------------------------------------------------------------------------------- 1 | # How to build a package 2 | 3 | ## Automatically with Swift Package Manager (SPM) 4 | This is by far the easiest way to get setup. First, create a directory: 5 | ```bash 6 | mkdir MyPackage 7 | cd MyPackage 8 | ``` 9 | 10 | Now, use SPM to init your package: 11 | ```bash 12 | swift package init --type library 13 | ``` 14 | 15 | All of your files will be generated automatically and you're now ready to setup [Travis and Codecov](#travis-and-codecov) 16 | 17 | ## Manually 18 | 19 | ### Package manifest 20 | Package.swift file in the root 21 | ```swift 22 | import PackageDescription 23 | 24 | let package = Package( 25 | name: "MyPackage", 26 | dependencies: [ 27 | // Add dependencies 28 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1), 29 | ] 30 | ) 31 | ``` 32 | 33 | ### Source 34 | Create and put your sources in the directory 35 | `Sources/MyPackage/` 36 | 37 | ### Tests 38 | 39 | Create and put your tests in the directory 40 | `Tests/MyPackageTests/` 41 | 42 | In order to support Linux, add `Tests/LinuxMain.swift` the following file with all of your `XCTestCase`s. 43 | ```swift 44 | import XCTest 45 | @testable import MyPackageTests 46 | 47 | XCTMain([ 48 | testCase(MyPackageTests.allTests) 49 | ]) 50 | 51 | ``` 52 | 53 | #### Example of TestCase 54 | 55 | ```swift 56 | import XCTest 57 | @testable import NeededImport 58 | 59 | class MyPackageTests: XCTestCase { 60 | static var allTests = [ 61 | ("test", test) 62 | ] 63 | 64 | func test() { 65 | XCTAssertEqual("abc", "abc") 66 | } 67 | } 68 | ``` 69 | 70 | ## Travis and Codecov 71 | For `Travis` add the following file to your project root: 72 | 73 | ### .travis.yml 74 | ```yml 75 | os: 76 | - linux 77 | - osx 78 | language: generic 79 | sudo: required 80 | dist: trusty 81 | osx_image: xcode8 82 | script: 83 | - eval "$(curl -sL https://swift.vapor.sh/ci)" 84 | - eval "$(curl -sL https://swift.vapor.sh/codecov)" 85 | ``` 86 | 87 | For `Codecov` add the following file to your project root: 88 | 89 | ### .codecov.yml 90 | ```yml 91 | coverage: 92 | ignore: 93 | - "Whatever folder" 94 | 95 | ``` 96 | 97 | ## XCode 98 | Generate an Xcode project: 99 | ```bash 100 | vapor xcode -y 101 | ``` 102 | The hotkey to build and run unit tests is `cmd+u`. 103 | -------------------------------------------------------------------------------- /Documentation/how-to-report-to-bugsnag.md: -------------------------------------------------------------------------------- 1 | # How to report to Bugsnag 2 | 3 | We use [Bugsnag](https://www.bugsnag.com/) as our tool of choice for reporting and monitoring errors. It can be a really powerful tool to get some insights on how our projects are performing. However, poorly made reports can also cause confusion and frustration since it can be hard to decipher what went wrong and why. 4 | 5 | To make sure our reports are aligned and contains the neccessary information, please have a look below. 6 | 7 | ## When to report 8 | 9 | There's a lot of different scenarios where it can make sense to report errors: 10 | 11 | - When a request is not as expected 12 | - When something went wrong while handling the request 13 | - When the response could not be created correctly 14 | - When commands didn't run as expected 15 | 16 | As a rule of thumb, one can report when the logic is not following the happy path of the project. However, consider marking errors that are a trivial (e.g. a request with validation errors) to not be reported to Bugsnag. 17 | 18 | ## How to report 19 | 20 | Using our [Bugsnag package](https://github.com/nodes-vapor/bugsnag), there's basically two ways of reporting errors to Bugsnag: 21 | 22 | - Using the Bugsnag middleware which reports all thrown `Abort` errors unless they hold `metadata` that says that they shouldn't be reported. 23 | - Using the reporter directly. This is convenient when you are not working with a request (e.g. in a command) or when you want to report an error but without throwing the error as a response to the request. 24 | 25 | ## What to report 26 | 27 | What you decide to include in the report is crucial to be able to debug later if errors starts to get reported. Here's some central things to consider when constructing your `Abort` error: 28 | 29 | - `status`: Have a look at [our API guide](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-write-apis.md#response-codes) to see a list of our most commonly used status codes. 30 | - `reson`: Make sure to have a descriptive error message. Consider making the message general for the type of error and then use the `metadata` part for any dynamic values. 31 | - `metadata`: Any non-sensitive information should be added to this dictionary (do not include any passwords etc.). A common approach is to include id's for each model item that is relevant for the error that occurred. If an error occurred bassed on a request, that request will automatically be included in the report. Remember to [configure Bugsnag](https://github.com/nodes-vapor/bugsnag#metadata) to filter out any sensitive information from the request. 32 | -------------------------------------------------------------------------------- /Documentation/how-to-use-a-proxy.md: -------------------------------------------------------------------------------- 1 | # How to use a proxy 2 | 3 | In some cases you might need to use a proxy for performing requests with your Vapor client, for example if a certain IP is whitelisted when reaching a third party integration. It's not straightforward to set this up and hopefully in the future, this will be a bit simpler and more aligned. 4 | 5 | ## Without TLS (using `http`) 6 | 7 | If there are no requirements for using `https`, a proxy can be defined in Vapor 2 as described in the [official docs](https://docs.vapor.codes/2.0/http/client/#proxy). 8 | 9 | ## With TLS (using `https`) 10 | 11 | Using a proxy with requirements for using `https` becomes a bit more challenging. First, we suggest that you set your client to `foundation` in your `droplet.json` since this client supports TLS. 12 | 13 | ### macOS 14 | 15 | Since we're using the `FoundationClient` we can use the provided support for proxying: 16 | 17 | ```swift 18 | let configuration = URLSessionConfiguration.default 19 | 20 | let hostname = "00.000.000.000" // my proxy IP 21 | let port = 8888 // my proxy port 22 | 23 | configuration.connectionProxyDictionary = [ 24 | kCFNetworkProxiesHTTPEnable: true, 25 | kCFNetworkProxiesHTTPProxy: hostname, 26 | kCFNetworkProxiesHTTPPort: port, 27 | kCFNetworkProxiesHTTPSEnable: true, 28 | kCFNetworkProxiesHTTPSProxy: hostname, 29 | kCFNetworkProxiesHTTPSPort: port 30 | ] 31 | 32 | let client = HTTP.FoundationClient( 33 | scheme: "https", 34 | hostname: "www.google.com", 35 | port: 443, 36 | session: URLSession(configuration: configuration) 37 | ) 38 | ``` 39 | 40 | The above code will init a `FoundationClient` that will proxy all reguests through `00.000.000.000:8888`. 41 | 42 | ### Linux 43 | 44 | Since we cannot define the `connectionProxyDictionary` on Linux, we unfortunately cannot use the above approach. Hopefully this will be ported in the future, but until then, we discovered a minor hack we can use in the meantime. 45 | 46 | If you set the `HTTPS_PROXY` environment variable to your hostname and port, then `FoundationClient` on Linux, which depends on `cURL`, will use that variable for proxying your requests. Following the macOS example you could set it to: `00.000.000.000:8888` to use that IP and port for proxying your requests. 47 | 48 | This might break in the future, but at this moment, this is the only way we've discovered where you can proxy using TLS without having to roll out your own client on top of `cURL`. 49 | 50 | Consider using the OS conditionals (e.g. `if os(macOS)`) for using both approaches, so that the proxy works when developing locally on macOS and when deploying to e.g. Vapor Cloud. -------------------------------------------------------------------------------- /Documentation/how-to-upload-files.md: -------------------------------------------------------------------------------- 1 | # How to upload files 2 | 3 | Uploading files, either through an API or through our Admin Panel is common functionality in our projects. Files can be everything from CSVs to images and videos. Here is some general guidelines on how to support this functionality. 4 | 5 | ## File sizes 6 | 7 | Our general rule of thumb on how big files we're allowing in a "traditional" file upload (a client submitting the file to the backend) using a JSON payload through the API or using a form in the Admin Panel are as follows: 8 | 9 | - Mobile: 10 mb 10 | - Web: 25 mb 11 | 12 | ## How to upload a file 13 | 14 | ### API 15 | 16 | Our preferred way of uploading files using our APIs is to send the file in a base64 encoded format in a JSON payload. This allows us to have a consistent API and since the size of the files our apps should be sending using the API are relatively small, then we can accept the overhead in using this approach. Once received on the backend, the file should be uploaded to S3. 17 | 18 | Instead of base64 json, Multipart can also be used for file upload to webserver. 19 | 20 | ### Web (e.g. Admin Panel) 21 | 22 | Our preferred way of upload files on the web, e.g. in our Admin Panel or through a SPA, is to use a multipart form. Once received on the backend, the file should be uploaded to S3. 23 | 24 | ## Handling large files 25 | 26 | Often we have to deal with files that are larger than the suggested file sizes for "traditional" file uploads, and therefore the following approach is our recommendation. 27 | 28 | Get a project-specific s3 bucket setup. 29 | 30 | ### Mobile 31 | 32 | If the mobile apps have to upload large files, then our preferred way of doing it is to have the apps upload directly to S3 and afterwards submitting the path to the file to the backend. The apps can use native SDKs for assisting in uploading the files. 33 | 34 | ### Web (e.g. Admin Panel) 35 | 36 | Similar to handling large files in mobile apps, our preferred way of uploading large files on the web is to use JavaScript to upload the file directly to S3. We can use the AWS SDK for this and once the upload is done, we can save the path to the file. 37 | 38 | ## Serving files 39 | 40 | ### Public files 41 | 42 | Every `public` file uploaded to S3 should be served using a CDN. The CDN will allow frontends to do cropping and resizing using query parameters. 43 | 44 | Either the backend can move the files into the project S3 with CDN, or a new CDN can be set up for the bucket (eg: imgix). 45 | 46 | ### Private files 47 | 48 | It's important `private` files are never public or cached on CDNs. We have few approaches to this: 49 | 50 | 1) NStack, if the files needs auth or password protection. This could be a very simple solution. 51 | 2) Private S3 bucket and files are served by backend. It is possible to stream files from S3 as a response. 52 | 53 | ## Processing files 54 | 55 | Processing that takes longer time than a request to respond should be offloaded into a queue. If a user needs to be notified, an email can be sent out. Remember to think about the amount of memory that is being used in the queue and consider if chunking is possible. 56 | -------------------------------------------------------------------------------- /Documentation/how-to-setup-environment-variables.md: -------------------------------------------------------------------------------- 1 | # How to setup environment variables 2 | 3 | We use environment variables heavily in our projects. It allows us to have personalized configurations, but it also makes it easy to deploy our projects without having to store sensitive values in our codebase. 4 | 5 | 6 | 7 | ## A short introduction to the config file 8 | 9 | A config file in Vapor (`Config/someconfig.json`) is basically a JSON file with keys and values, like this: 10 | 11 | 12 | 13 | ```json 14 | { 15 | "name": "my project", 16 | "mySecretApiKey": "topsecretapikey" 17 | } 18 | ``` 19 | 20 | 21 | 22 | 23 | 24 | To set the values of your config file, you have a couple of options: 25 | 26 | 27 | 28 | ### Hardcode the values into the config 29 | 30 | ```json 31 | { 32 | "name": "my project", 33 | "mySecretApiKey": "topsecretapikey" 34 | } 35 | ``` 36 | 37 | 38 | 39 | Hardcoding the values directly into the config means that the values goes hand in hand with the codebase. This can be convenient for values that doesn't change across environments, and for values that are not secret nor personal. In other words, if a fellow developer pulls down the codebase, it would make sense for that developer to keep those values in the config. 40 | 41 | 42 | 43 | ### Use environment variables 44 | 45 | ```json 46 | { 47 | "name": "my project", 48 | "mySecretApiKey": "$MY_API_KEY" 49 | } 50 | ``` 51 | 52 | 53 | 54 | Setting the values in a config using environment values can sometimes be preferred. It is useful when the values are somewhat secret, such as API keys for third party integrations or passwords to databases, since they will not be readable even though an outsider should get access to the codebase. Using environment variables is also useful for values that might be dependent on the machine that are running the project. E.g. connection credentials for a database might be different from one developer to another. Also, when the project is later deployed to be released, the credentials will most likely be different as well. Environment variables allows us to swap out values when running the project, so that we can run the project on multiple machines, but with the different values for some of our configurations. 55 | 56 | 57 | 58 | ### Use environment variables with a default value 59 | 60 | ```json 61 | { 62 | "name": "my project", 63 | "mySecretApiKey": "$MY_API_KEY:myfallback" 64 | } 65 | ``` 66 | 67 | 68 | 69 | Sometimes it's convenient to have the option to supply a hardcoded value in a configuration file, but still being able to override that value using an environment value. This can be done by referring to the environment variable, followed by a `:` and then the hardcoded value. A good example of this could be the port of the webserver you're running or the name of the database. The name of the database will most likely be the same on each machine, and it can be annoying to have to change the values when switching projects. Having a fallback value for the database name can solve this, if each developer makes sure to name their databases accordingly. 70 | 71 | 72 | 73 | ### Set the values on the scheme 74 | 75 | ```json 76 | { 77 | "name": "my project", 78 | "mySecretApiKey": "$MY_API_KEY" 79 | } 80 | ``` 81 | 82 | 83 | 84 | There's one last option to set the values of the environment variables, which can be convenient in cases where you need to add some values for a specific project, but without changing the config files or changing your global set of environment variables on your machine. In this case, the config will look the same and will refer to your environment variables. To set the values, you edit the scheme of your project (using the api-template, the scheme is named `Run`), and you set any key values you might need under the section called "Environment Variables". To add a value to the environment variable used in the example above, one would simply add a row for the name `MY_API_KEY` with whatever value is needed. 85 | 86 | 87 | 88 | ## How to setup your own environment variables 89 | 90 | You could export your variables in your `~/.bash_profile` and then let Xcode load these by adding a build phase, but there's a more convenient way which doesn't require adding build phases each time to your project. If you download and install [EnvPane](https://github.com/hschmidt/EnvPane) then you will get an extra preference pane in your System Preferences which will allow you to add environment variables. However, please note that after adding/changing a variable, Xcode needs to be restarted. 91 | 92 | When a project is ready to be deployed, remember that environment variables also needs to be setup on the deployment environment. Have a look at Vapor Cloud's [documentation](https://docs.vapor.cloud/toolbox/managing-your-apps/#custom-environment-variables) on how to add this. Remember that some environment variables (e.g. MySQL variables) will automatically be created and set by Vapor Cloud. We have some shared credentials which can be found [here](https://github.com/nodes-projects/readme/blob/master/server-side/vapor/environment-variables.md) (please note that the link points to a private repo). 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Vapor at Nodes 4 | 5 | Welcome to the Vapor team at Nodes' space on GitHub. We ❤️ Vapor. Look below to see what we are working on. 6 | 7 | Feel free to get in touch or to submit a PR to any of our repos. 8 | 9 | ## Documentation 10 | 11 | - **Creating a project** 12 | - [How to start a project](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-start-a-project.md) 13 | - [How to build a package](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-build-a-package.md) 14 | - **Syntax, semantics and structure** 15 | - [How to write Swift](https://github.com/nodes-vapor/readme/blob/master/Documentation/guide-how-to-write-swift.md) 16 | - [How to write APIs](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-write-apis.md) 17 | - **Releasing** 18 | - [How to release a version](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-release-a-version.md) 19 | - [Checklist before going live with a project](https://github.com/nodes-vapor/readme/blob/master/Documentation/checklist-before-going-live.md) 20 | - **Database** 21 | - [How to create an entity relationship diagram](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-an-erd.md) 22 | - [How to support emojis (utf8mb4)](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-support-emojis.md) 23 | - **Hosting** 24 | - [How to setup environment variables](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-setup-environment-variables.md) 25 | - [How to deploy and host Vapor apps](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-deploy-and-host.md) 26 | - **Third party integrations** 27 | - [Push notifications with Urban Airship](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-urban-airship-push.md) 28 | - [How to report errors to Bugsnag](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-report-to-bugsnag.md) 29 | - **Postman** 30 | - [How to create Postman collections](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-postman-collections.md) 31 | - [How to create Postman documentation](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-postman-documentation.md) 32 | - [How to create Postman API acceptance tests](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-postman-tests.md) 33 | - **Misc** 34 | - [Tips and tricks](https://github.com/nodes-vapor/readme/blob/master/Documentation/tips-and-tricks.md) 35 | - [How to use a proxy](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-use-a-proxy.md) 36 | - [How to upload files](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-upload-files.md) 37 | - [How to work with Vapor 2 projects](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-work-with-vapor-2-projects.md) 38 | 39 | ## Configuration 40 | 41 | - [Git ignore](https://github.com/nodes-vapor/readme/blob/master/Configuration/.gitignore) 42 | - [CircleCI](https://github.com/nodes-vapor/readme/tree/master/Configuration/.circleci) (please note the folder structure) 43 | - [Travis](https://github.com/nodes-vapor/readme/blob/master/Configuration/.travis.yml) 44 | - [Codecov](https://github.com/nodes-vapor/readme/blob/master/Configuration/.codecov.yml) 45 | - [SwiftLint](https://github.com/nodes-vapor/readme/blob/master/Configuration/.swiftlint.yml) 46 | - [Codebeat](https://github.com/nodes-vapor/readme/blob/master/Configuration/.codebeatignore) 47 | 48 | ## Blogging 49 | 50 | - [Our blog](https://engineering.nodesagency.com/categories/Vapor/) 51 | - [Ideas for future posts](https://github.com/nodes-vapor/blog-post-ideas/issues) (please see the readme for more information) 52 | 53 | ## Packages 54 | 55 | ### Made at Nodes 56 | 57 | - [Admin Panel](https://github.com/nodes-vapor/admin-panel-provider) ✍️ (note: the old one can be found [here](https://github.com/nodes-vapor/admin-panel)) 58 | - [Admin Panel Nodes SSO](https://github.com/nodes-vapor/admin-panel-nodes-sso) 🔑 59 | - [JWT Keychain](https://github.com/nodes-vapor/jwt-keychain) ⛓ 60 | - [Template](https://github.com/nodes-vapor/template) 🏎 (note: our old Vapor 1 template can be found [here](https://github.com/nodes-vapor/template-old)) 61 | - [Storage](https://github.com/nodes-vapor/storage) 🗄 62 | - [Sourcery Templates](https://github.com/nodes-vapor/sourcery-templates) ✨ 63 | - [Sanitized](https://github.com/nodes-vapor/sanitized) 64 | - [Meta](https://github.com/nodes-vapor/meta) 65 | - [Audit](https://github.com/nodes-vapor/audit-provider) 🕵️ 66 | - [Bugsnag](https://github.com/nodes-vapor/bugsnag) 🐛 67 | - [Slugify](https://github.com/nodes-vapor/slugify) 68 | - [AWS](https://github.com/nodes-vapor/aws) 69 | - [Sugar](https://github.com/nodes-vapor/sugar) 🍬 70 | - [Stacked](https://github.com/nodes-vapor/stacked) 📚 71 | - [Paginator](https://github.com/nodes-vapor/paginator) 📄 72 | - [NStack](https://github.com/nodes-vapor/nstack) 73 | - [UAPusher](https://github.com/nodes-vapor/push-urban-airship) ✉️ 74 | - [Error Extended](https://github.com/nodes-vapor/error-extended) 75 | - [Gatekeeper](https://github.com/nodes-vapor/gatekeeper) 👮 76 | - [DataURI](https://github.com/nodes-vapor/data-uri) 77 | - [Flash](https://github.com/nodes-vapor/flash) ⚡️ 78 | - [Dockerfiles](https://github.com/nodes-vapor/dockerfiles) 🐳 79 | 80 | ### Made by others 81 | 82 | - [Jobs](https://github.com/BrettRToomey/Jobs) 83 | - [SwiftyBeaver](https://github.com/SwiftyBeaver/SwiftyBeaver-Vapor) 84 | -------------------------------------------------------------------------------- /Documentation/how-to-start-a-project.md: -------------------------------------------------------------------------------- 1 | # How to start a project 2 | 3 | ## Prerequisites 4 | 5 | Before it makes sense to start a project, please make sure you have the following installed on your machine: 6 | 7 | ### Vapor 8 | 9 | See [Vapor docs](https://docs.vapor.codes) for information on how to set it up. During the beta of Vapor Cloud, you might want to install the beta version of the CLI instead - for more information about this, see the [Vapor Cloud docs](https://docs.vapor.cloud). 10 | 11 | ### MySQL 12 | 13 | For projects using a database, MySQL needs to be installed. This can be done through brew, e.g. `brew install mysql`. See this [guide](https://blog.joefallon.net/2013/10/install-mysql-on-mac-osx-using-homebrew/) for more information. 14 | 15 | ### Redis 16 | 17 | For projects using Redis as a cache, make sure to install this using brew by calling `brew install redis`. See this [guide](https://gist.github.com/nrollr/eb24336b8fb8e7ba5630) for more information. 18 | 19 | 20 | ## Getting started 21 | 22 | ### Vapor 23 | 24 | Our Vapor template comes with some convenient packages which are used in most of our projects. Remember to look at the [documentation](https://github.com/nodes-vapor/template/blob/master/README.md) for the template for further instructions on how to get it up and running. 25 | 26 | ``` 27 | vapor new my-project-name --template=https://github.com/nodes-vapor/template 28 | ``` 29 | 30 | ### GitHub 31 | 32 | #### Creating the repo 33 | 34 | After you've aligned the project name with your project team, you can create the repo on the nodes-projects organization. Check this [guide](https://github.com/nodes-projects/readme/blob/master/general/new-repository.md) (internal) for information on naming of repos. 35 | 36 | #### `LICENSE` 37 | 38 | If you're setting up a customer project, then go ahead and delete the `LICENSE` file. If it's an open source project, then keep the MIT license file but make sure to update any year references if needed. 39 | 40 | #### `README` 41 | 42 | Have a look at this [template](https://github.com/nodes-projects/readme/blob/master/general/readme-template.md) (internal) for setting up your `README` file for customer projects. Towards the top of the file, make sure to add the relevant badges. It should look something like: 43 | 44 | ``` 45 | [Swift version] [Vapor version] 46 | 47 | `master`: [Circle CI] [Codebeat] 48 | `develop`: [Circle CI] [Codebeat] 49 | ``` 50 | 51 | See some of the other projects on the nodes-projects organization for comparison and check out the Circle CI and Codebeat sections below for information on how to generate the badges. 52 | 53 | #### Branches 54 | 55 | After the repository has been created and Circle CI (see [Circle CI](#circle-ci) section) has been set up, go to the "Settings" part of the repository on Github and hit "Branches". In here you want to make sure that `develop` is set as the default branch and that `master` and `develop` are added as protected branches. For each protected branch make sure that the following options are checked: 56 | 57 | - [x] "Protect this branch" 58 | - [x] "Require status checks to pass before merging" 59 | - [x] "Require branches to be up to date before merging" 60 | - [x] "ci/circleci: Linux" 61 | - [x] "ci/circleci: MacOS" (if running Vapor 2 or newer) 62 | 63 | ### Circle CI 64 | 65 | After the repository has been created on GitHub, the project can be added to Circle CI. Make sure you're on the nodes-projects organization (on Circle CI), hit "Add Projects". On the following page search for your repository name and hit "Set Up Project". On the next page choose "Linux" under "Operating "System" and hit "Start building" at the bottom of the site. 66 | 67 | Last step is to generate a token for the Circle CI badge for the readme. Select your newly created project on Circle CI and hit settings (the gear icon), go to "API Permissions" and hit "Create Token". Choose "Status" as scope and write "Badge" as the token label. This generated token can now then be used for the Circle CI badge for the readme by inserting it into the url at the right spot: 68 | 69 | ``` 70 | [![Circle CI](https://circleci.com/gh/nodes-projects/my-project/tree/master.svg?style=shield&circle-token=my-token)](https://circleci.com/gh/nodes-projects/my-project) 71 | ``` 72 | 73 | > Note that in order to create the badge for the develop environment you first have to trigger a Circle CI build on develop by pushing a commit. 74 | 75 | ### Codebeat 76 | 77 | We use Codebeat for static code analysis. Although the reports and the GPA score should be taken with a grain of salt, the reports are still useful for getting recommendations for areas to improve in the codebase. Each repository and branch needs to be set up manually. After logging into Codebeat (please reach out to a fellow developer for credentials) you can add the `master` and `develop` branches for the repository. When Codebeat is done analyzing, the badges for the readme will be available under "Settings" within the different reports. 78 | 79 | ### Deployment 80 | 81 | Using the Vapor Cloud CLI, your Vapor project can be deployed to the cloud. See the [Vapor Cloud docs](https://docs.vapor.cloud/) for more information on how to do this. 82 | 83 | ## Optional packages 84 | 85 | ### CORS 86 | If the API is going to be used in a web app, consider adding the [CORS middleware](https://docs.vapor.codes/2.0/http/cors/#cors). 87 | 88 | ``` 89 | let corsConfiguration = CORSConfiguration( 90 | allowedOrigin: .all, 91 | allowedMethods: [.get, .post, .put, .options, .delete, .patch], 92 | allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin", "X-Requested-With", "N-Meta"] 93 | ) 94 | drop.middleware.insert(CORSMiddleware(configuration: corsConfiguration), at: 0) 95 | ``` 96 | Read more: https://github.com/vapor/documentation/blob/master/http/cors.md 97 | 98 | Test: http://codepen.io/dennishn/pen/BLbYyJ 99 | 100 | 101 | ### Storage 102 | 103 | If you need to store files, e.g. images, the [Storage](https://github.com/nodes-vapor/storage) package is here to help you. 104 | -------------------------------------------------------------------------------- /Documentation/how-to-create-postman-collections.md: -------------------------------------------------------------------------------- 1 | # How to create Postman workspaces & collections 2 | 3 | We use [Postman](https://www.getpostman.com/) heavily at Nodes for tasks like: 4 | 5 | - Requesting our internal and external APIs to see if they behave as expected 6 | - Documenting our APIs 7 | - Writing tests for our APIs 8 | - Mocking our APIs 9 | - Setting up monitoring for our APIs 10 | 11 | To align how we work with Postman, this document was created. 12 | 13 | ## How to set up a new workspace 14 | 15 | Each project at Nodes that contains an API should have a workspace. A workspace can hold internal API collections, external API collections and any test collections. The workspace should have the same name as the project on GitHub but without the framework suffix. See [this document](https://github.com/nodes-projects/readme/blob/master/general/new-repository.md) for more info on our repo naming convention (internal link). 16 | 17 | Remember when creating a workspace that it's created on the Nodes Team instead of your personal account. 18 | 19 | ### Structure of a workspace 20 | 21 | Use collections within the workspace to group related endpoints. Normally we would have one collection for the API that the mobile applications will consume and then a number of extra collections based on the specifics of the project. These could be collections for external API's that the backend uses or test collections. 22 | 23 | ## How to setup a new collection 24 | 25 | ### Creating a collection 26 | 27 | Collections are groups of endpoints and using them is a great way to keep a structure within your API project. Remember to create the relevant collections within the correct project-specific workspace (instead of the fallback "Team Workspace"). 28 | 29 | ### Sharing a collection 30 | 31 | With your new collection created, the next step is to make sure the rest of your team is able to see it. To do this, you're going to click the "…" icon on your new collection and click "Share Collection". This will bring up a new dialogue where you're able to specify how to share the collection: 32 | 33 | - Team: Can View 34 | - Individual permissions for each backend developer at Nodes: Can Edit 35 | - QA Lead: Can Edit 36 | 37 | This will allow consumers of the API to read about it as well as trying it out. It will also allow any other backend developer who might work on the project in the future to edit the collection if needed. Remember that you need to be invited to the Nodes team on Postman before you can publish it to the team. 38 | 39 | ### Structure of a collection 40 | 41 | At the top level, the folders created should follow the different domains of the available endpoints. These domains will most likely follow the different models and controllers in your project. As an example, let's consider an API for interacting with a blog. At the top level, the collection could have the following folders: 42 | 43 | - Posts 44 | - Authors 45 | - Categories 46 | 47 | Sometimes, it makes sense to have nested folders to group related endpoints within a specific domain. In the above example, that could be categories for a specific post (e..g `/posts/:id/categories`). In that case, it's fine to have another folder inside Posts that group the endpoints related to categories for a specific post. Remember to keep the top level folder for Categories for any endpoints that are not tied to a specific post. 48 | 49 | Endpoints for the different domains should then be created within each top level domain folder. The endpoints should be named to describe their action. Having a reference to the domain in the name of the endpoint can be convenient when having multiple tabs open in the Postman application. 50 | 51 | Expanding the Posts folder could then have endpoints like: 52 | 53 | - All posts 54 | - Specific post 55 | - Create post 56 | - Update post 57 | - Delete post 58 | 59 | In some cases, it might be convenient to setup endpoints for dealing with a third party API. To keep things separate and to not confuse any consumers of our APIs, a new collection should be created using the same collection name but with a "-backend" suffix to indicate that this collection is to be used by the backend team only. 60 | 61 | ## How to setup variables 62 | 63 | To make it easier to move between different environments when testing our APIs we use Postman variables. It's important to know the difference between **environment variables** and **collection variables**: 64 | 65 | ### Environment variables 66 | 67 | Environment variables are the ones that specify environment-specific values, e.g. how we are hosting our projects. Since we use Vapor Cloud for all of our projects and since we always use a development, staging and production environment, three templates for these three setups has been made: 68 | 69 | - Vapor Cloud Development 70 | 71 | - Vapor Cloud Staging 72 | 73 | - Vapor Cloud Production 74 | 75 | Each template will come with the same set of variables but with different values. Here's the list of variables the templates will contain: 76 | 77 | - `scheme` (e.g. `https://`) 78 | 79 | - `host` (e.g. `.vapor.cloud`) 80 | 81 | - `baseURL` (`{{scheme}}://{{appName}}{{host}}:{{port}}`) 82 | 83 | - `port` (e.g. `443`) 84 | 85 | - `accessToken` (`{{{{appName}}-accessToken}}`) 86 | 87 | - `refreshToken` (`{{{{appName}}-refreshToken}}`) 88 | 89 | Note how the `baseURL` is being constructed using other environment variables and using a collection variable. `accessToken` and `refreshToken` are also being constructed using a collection variable called `appName` which will be explained below. 90 | 91 | These environments can be found under the "Team Library" and by looking at the "Environment Templates". We suggest that you add these three templates to your setup and if relevant, feel free to go ahead and add your own local environment if you're running the project on your machine. The only requirement for your local environment is to have a variable called `baseURL` (e.g. `http://0.0.0.0:8080`), one called `refreshToken` and one called `accessToken`. `accessToken` and `refreshToken` should just have the same values as the ones coming from any of the templates. 92 | 93 | ### Collection variables 94 | 95 | Collection variables should be all the values that is relevant for your collection and which are not depending on the selected environment. Currently there's one required variable which needs to be set for each collection: 96 | 97 | - `appName` (e.g. `my-blog`) 98 | 99 | This variable should hold the name of the application name on Vapor Cloud. If the URL for your application on Vapor Cloud is `https://my-blog.vapor.cloud` then `my-blog` is your `appName` value. Feel free to add any other relevant collection variables when needed. 100 | 101 | ### Working with access tokens 102 | 103 | Most of our APIs will have endpoints which requires a token (either a access token or a refresh token). This could be protected endpoints where a user is required to authenticate before giving access. To make it easier to move between projects and environments, the following code should be added under "Tests" for the relevant endpoints where tokens are being returned: 104 | 105 | ```javascript 106 | let json = pm.response.json(); 107 | let accessTokenKey = pm.variables.get("appName") + "-accessToken" 108 | let refreshTokenKey = pm.variables.get("appName") + "-refreshToken" 109 | 110 | pm.environment.set(accessTokenKey, json.accessToken) 111 | pm.environment.set(refreshTokenKey, json.refreshToken) 112 | ``` 113 | 114 | This code snippet expects an `accessToken` and a `refreshToken` at the top level of the JSON response of the endpoint. Endpoints which logins a user or creates a user are good examples of endpoints where this might be relevant. Remember to align the code to with what tokens you are expecting (some projects might not be using refresh tokens). 115 | 116 | ## How to create endpoint documentation 117 | 118 | Postman documentation is covered in its own file [here](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-postman-documentation.md). 119 | 120 | ## How to create API acceptance tests 121 | 122 | Postman API acceptance testing is covered in its own file [here](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-create-postman-tests.md). 123 | -------------------------------------------------------------------------------- /Documentation/how-to-urban-airship-push.md: -------------------------------------------------------------------------------- 1 | # Sending push notifications with Urban Airship 2 | ❗️Note, using Firebase directly is now a valid option for ios / android / web and free ([Read more](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-firebase-cloud-message.md)) 3 | 4 | Before digging into details it's important to understand the architectural idea 5 | ![image](https://cloud.githubusercontent.com/assets/1279756/25579133/383ca46e-2e75-11e7-8001-21d7e7d34f5a.png) 6 | 7 | # Diagram 8 | 1) Through the UA-SDK (or the native integration) the app will request a push-token at it's platform push-network. This will trigger a popup to allow push notification on most platforms. This push-token is unique for the App, device & certification (release, debug etc) and will expire after x days. Luckily the UA SDK will deal with refreshing it. 9 | 10 | 2) Through UA-SDK / UA-API the push-token will be sent to UA and stored. UA can now send push notifications to that device & app & certificate. Since we do not want to deal with push-tokens vs users references. It's important to register to channels as well. [Read more](https://github.com/nodes-vapor/readme/blob/master/Documentation/how-to-urban-airship-push.md#channels) 11 | 12 | 3) Sending a push notification from a backend project happens via the API. It's normally triggered by 13 | - Admin panel as a view, send to X, Y & Z with a custom alert. 14 | - Triggered by an event, fx some else liked your post. The alert would be from NStack / Localization 15 | 16 | 4) UA will look up the namedUser or tag, loop through each push-tokens registered to this channel. And sent 1-by-1 to each push-network. This is done Async in queue. 17 | 18 | 5) The push-network will queue the request from UA and sent the push notification through a socket connection to the device. If the device is not online, it will save it for x hours (depending on push-network) 19 | 20 | # Apps 21 | 22 | In UA you create applications for each application & environment. 23 | 24 | For application ABC there should be 3 UA apps 25 | 26 | - ABC - Development (development) 27 | - ABC - Staging (production) 28 | - ABC - Production (production) 29 | 30 | The mode in () is the UA "Production status". The reason why we need "production" mode on staging is to have the option to do enterprise builds 31 | 32 | The API keys can always be found at 33 | https://go.urbanairship.com/apps/[APP-ID]/api/ 34 | 35 | If the certificates are not setup, this view cannot be found from navigation, so use the URL :) 36 | 37 | The App Master Secret is only suppose to be used for sending push via the API. Never store this key in apps / frontend. 38 | 39 | Note: All keys should be setup as server variables and never be hardcoded 40 | 41 | # Alert 42 | A push message has a "alert" which is the string showed in the notification center. It has a limit of 255 chars. So limit it to less. 43 | 44 | Always put the alert message in NStack / Localization 45 | 46 | Note: iOS already truncate them earlier (around 110 chars) 47 | 48 | # Payloads / Extra 49 | 50 | Payload is a way to pass more information, fx for deeplinking 51 | 52 | Example of a payload could be 53 | 54 | ```json 55 | "notification": { 56 | "ios": { 57 | "extra": { 58 | "type": "friendRequest", 59 | "userId": 1 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | This will let the app know where to deeplink 66 | 67 | UA does not support nested data. If you need to send a full model, consider json-encoding it to 1 key 68 | 69 | Note: there is limits on iOS for maximum 2kb of data 70 | 71 | # Channels / Tags / Named user 72 | There is currently 3 types of channels 73 | - named user (Always register with your userId) 74 | - alias (deprecated) 75 | - tags (Push send to group of users, fx users who have push setting1 on, joins setting1 tag) 76 | 77 | We always register userId as namedUser (this used to be alias, but is now deprecated by UA) 78 | 79 | Tags can be used very smart, and should avoid every situation of having to store push settings in the DB. 80 | 81 | Example 1) 82 | 83 | Feature: Push on breaking news which is optional for users (is a setting) 84 | 85 | The app will register to a tag "breakingNews" if the push setting is on. 86 | It's important to code this as a way where local storage is master -> @johnny agree? 87 | 88 | The backend will now just push to the tag "breakingNews" 89 | 90 | Example 2) 91 | 92 | Feature: Recieving push notifications about athlete with id: 12345, after favoriting him/her 93 | 94 | The app will register to tag "athelete_12345" 95 | 96 | The backend will now just push to the tag "athlete\_[ID]" instead of looping users which have favorited it (maybe the state is even local) 97 | 98 | Example 3) 99 | 100 | The app have options to only receive news push notification during race or always 101 | 102 | The app will register to tag "newsAlways" or "newsDuringRace" or both 103 | 104 | Backend will push to those tags, but will figure out if the the current time is durring race and add that tag also. 105 | 106 | Note: Users who registered to both tags will not get the notification twice. 107 | 108 | # Sound 109 | 110 | There is options to change the notification sound, either to other native sounds or custom 111 | 112 | - Setting sound to empty string "" will set no push sound 113 | - Setting the sound to "default" will set the sound to platforms default tone 114 | 115 | On top of that, 116 | There is some differences between the different platforms 117 | 118 | ### iOS 119 | 120 | Add a wav file in the build and add the sound key in the ios object via the API when sending a push message 121 | 122 | ex 123 | ``` 124 | "notification": { 125 | "alert": "test", 126 | "ios": { 127 | "sound": "arrivedsound.wav" 128 | } 129 | } 130 | ``` 131 | ### Android 132 | 133 | Add it to payload. The Android app will handle this manually 134 | 135 | ```json 136 | "notification": { 137 | "android": { 138 | "extra": { 139 | "sound":"arrivedsound" 140 | } 141 | } 142 | } 143 | ``` 144 | 145 | ### Windows 146 | 147 | Right now it's only possible to use native sounds, 148 | And can be set same as iOS, just add the sound key in the wns object 149 | 150 | # Silent push notifications 151 | 152 | It's possible to sent a push notification without it appears in the notification center. This can be very handy to trigger updates in the app, update badge counts etc. 153 | 154 | ### iOS 155 | 156 | This mode is called content-available for iOS 157 | Inside the ios object, set the key "content-available": true 158 | and make sure to not have badge, sound & alert set in the ios object 159 | 160 | ```json 161 | { 162 | "notification": { 163 | "ios": { 164 | "content-available":true 165 | } 166 | } 167 | } 168 | 169 | ``` 170 | ### Android 171 | 172 | Android is building the notification them self in runtime. That means just agree on some kind of indication on silent and 173 | Again the type could be enough, else create a payload key "silent": true fx 174 | 175 | # Badge count 176 | This is originally an iOS feature 177 | ![image](https://cloud.githubusercontent.com/assets/1279756/25580264/c21753e6-2e7f-11e7-9499-265f73ea79e9.png) 178 | 179 | But there is an option for doing something simular on android also 180 | 181 | ![image](https://cloud.githubusercontent.com/assets/1279756/25580290/084cdfe8-2e80-11e7-9f09-e13c0c282866.png) 182 | 183 | There is 2 ways of doing badge count 184 | 185 | ## The big solution 186 | 187 | Like FB, Google etc, let the server keep track of unread count. And send silent push notification when this updates, to keep all you divices / platforms aligned 188 | 189 | This can be very time consuming and often require a full activity / notification system before hand 190 | 191 | ## The simple solution 192 | 193 | Use the +1 value, which will increase the counter by one in ios. And when you open to the app / specific view, you clear the count 194 | 195 | ### iOS 196 | 197 | Inside the ios object, set the key "badge": count or +1 198 | 199 | ```json 200 | { 201 | "notification": { 202 | "ios": { 203 | "badge":"+1" 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | ### Android 210 | 211 | Add it to payload. 212 | Note: this is not native Android! 213 | 214 | ```json 215 | "notification": { 216 | "android": { 217 | "extra": { 218 | "badge":"+1" 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | ### Windows 225 | 226 | Not possible 227 | 228 | # Localization 229 | 230 | First you need to setup a system to keep track of language for each user. 231 | 232 | Setup a field on the user model "locale" and store the Accept-Language header here. 233 | Now when sending a push notification you can look up the user's locale before hand, and thereby translate it. 234 | 235 | This solution requires you to loop each user. It is possible to send arrays of namedUsers/aliases to minimize the network to UA (which is the slow part) 236 | 237 | Note: Also a creative solutions with tags can be used 238 | 239 | # Other 240 | 241 | - Android has a "delivery_priority" field which should be set to high if the notification is a must. Else low-battery modes etc will ignore it ("normal" or "high") 242 | - Android has a Visibility option also. Which controls how much of the push message is showed in the lock screen (Android 5 feature) 243 | -------------------------------------------------------------------------------- /Documentation/how-to-create-postman-tests.md: -------------------------------------------------------------------------------- 1 | # How to create Postman API acceptance tests 2 | 3 | Writing API acceptance tests is a great way to write system tests while hitting real environments. These tests can be run on your local machine as well as in Vapor Cloud (please consider what environments you're using and why). If it's your first time looking into testing with Postman, then consider reading through some of these resources: 4 | 5 | - [Writing tests in Postman](http://blog.getpostman.com/2017/10/25/writing-tests-in-postman/) 6 | - [API testing tips from a Postman professional](http://blog.getpostman.com/2017/07/28/api-testing-tips-from-a-postman-professional/) 7 | - [JSON Schema](https://spacetelescope.github.io/understanding-json-schema/about.html) 8 | - [Tiny Validator (for v4 JSON Schema)](https://geraintluff.github.io/tv4/) 9 | 10 | ## Creating a test collection 11 | 12 | To keep things separate, the tests for your API collection should be created in a different collection. If your API collection is named `my-project` then the corresponding test collection should be named `my-project-tests`. Remember to share this collection the same way you would share a normal API collection. 13 | 14 | Inside your test collection you will have endpoints for the endpoints you want to test. The order in which you arrange your folders and endpoints (from top to bottom) is **important** since this will be the order in which the Postman Runner will run your tests. Postman Runner is a tool for running all of your tests in a collection. 15 | 16 | As with your normal API collection, top level folders should follow the domains of your project: 17 | 18 | - Posts 19 | - Authors 20 | - Categories 21 | 22 | Depending on the complexity of your project, you might want to create folders inside of these top level folders to describe flows. 23 | 24 | Expanding the Posts folder might then reveal the following endpoints in an arranged order: 25 | 26 | - Get all posts 27 | - Add post 28 | - Get single post 29 | - Get all posts 30 | - Delete post 31 | - Get single post 32 | 33 | ## Setting up variables to use for testing 34 | 35 | Usually you will want to setup variables that can be used across your tests. These variables could hold the user that's going be used for the tests, the json schemas to use for validation (see "Writing tests" for more information) or something else that might be useful for your tests. We save these variables as "globals" since we can only choose between environment and global variables and since we don't want to clutter our environment variables too much, we'll stick with globals. The variables can be set like this: 36 | 37 | ```javascript 38 | // Simple variable 39 | pm.globals.set("my-project_tests_email", "postman-test-user@nodes.dk"); 40 | 41 | // Variable with json 42 | pm.globals.set("my-project_tests_json", JSON.stringify({ 43 | "foo": "bar" 44 | })); 45 | ``` 46 | 47 | To retrieve these variables you can do the following: 48 | 49 | ```javascript 50 | const email = globals.my-project_tests_email 51 | const json = JSON.parse(globals.my-project_tests_json) 52 | ``` 53 | 54 | Note how the variables are being prefixed with the project name followed by `_tests_` to make it clear that the variables are used for testing. 55 | 56 | Remember that we're using JavaScript, so we can use JavaScript libraries to help us generate our data. Here's an example of creating a random email for testing an endpoint for creating a user (this is for the purpose of the example since Postman also has a random function): 57 | 58 | ```javascript 59 | const int = Math.floor(Math.random() * 1000); 60 | const randomEmail = "postman-test-" + int + "@nodes.dk"; 61 | ``` 62 | 63 | Since most of these variables will be used throughout the tests, it makes sense to set them up on the first endpoint. Further, to have them available when testing the first endpoint, we can set these variables up in the "Pre-request Script" phase. This means that the variables we set up for testing purposes can be accessed in all endpoints in the body, url and test part of our endpoints. 64 | 65 | ## Writing tests 66 | 67 | ### Global tests 68 | 69 | Some tests are relevant for all endpoints in our test collection and to avoid having to add them to every single endpoint, these can be set up once and then run automatically for each endpoint in the collection. To do this, edit the collection and hit the "Tests" pane. Here's two examples of tests that could go in here: 70 | 71 | ```javascript 72 | // Response time tests 73 | pm.test("Response time is less than 2 seconds", function () { 74 | pm.expect(pm.response.responseTime).to.be.below(2000); 75 | }); 76 | 77 | // Header tests 78 | pm.test("Content-Type is set correctly", function () { 79 | pm.response.to.have.header("Content-Type", "application/json; charset=utf-8") 80 | }); 81 | ``` 82 | 83 | Our tests will be using a BDD-inspired syntax which you can read more about [here](http://blog.getpostman.com/2017/10/25/writing-tests-in-postman/) (also mentioned at the top). 84 | 85 | ### Endpoint-specific tests 86 | 87 | Coming back to our example on endpoint tests, the first test might be "Get all posts". The tests for this endpoint could look something like: 88 | 89 | ```Javascripts 90 | // Header tests 91 | pm.test("Response code is set correctly", function () { 92 | pm.response.to.have.status(200) 93 | }); 94 | 95 | // Body tests 96 | const paginationSchema = JSON.parse(globals.my-project_tests_schema_pagination); 97 | const postListSchema = JSON.parse(globals.my-project_tests_schema_post_list); 98 | const json = pm.response.json() 99 | 100 | pm.test('Pagination schema is valid', function() { 101 | pm.expect(tv4.validate(json, paginationSchema)).to.be.true; 102 | }); 103 | 104 | pm.test('Post schema is valid', function() { 105 | pm.expect(tv4.validate(json.data, postListSchema)).to.be.true; 106 | }); 107 | ``` 108 | 109 | There's a couple of things going on here, so let's try and break it down: 110 | 111 | #### Testing that the response code is correct 112 | 113 | The first test ensures that the response code we're giving back from the endpoint is as we expect. To do this, we can simply fetch the status code from the `pm.response` object. 114 | 115 | #### Testing that the response body is correct 116 | 117 | Most of the time, we're interested in the structure of the returned JSON and not each individual value. Instead of writing tests that check to see if each expected key has the expected value, we can use something called [JSON schemas](https://spacetelescope.github.io/understanding-json-schema/about.html) to define how we expect the structure of a response to be and [Tiny Validator](https://geraintluff.github.io/tv4/) to actually validate our schemas. 118 | 119 | In order to test using JSON schemas, we first need to define our schemas: 120 | 121 | ```javascript 122 | const post = { 123 | "type": "object", 124 | "properties": { 125 | "id": { 126 | "type": "number" 127 | }, 128 | "userId": { 129 | "type": "number" 130 | }, 131 | "title": { 132 | "type": "string" 133 | }, 134 | "body": { 135 | "type": ["string", "null"] 136 | }, 137 | "createdAt": { 138 | "type": "string" 139 | }, 140 | "updatedAt": { 141 | "type": "string" 142 | } 143 | } 144 | }; 145 | ``` 146 | 147 | In the above schema, we focus on the properties we expect and the types they should hold. We could do the same to define a schema for how we structure pagination: 148 | 149 | ```javascript 150 | pm.globals.set("my-project_tests_schema_pagination", JSON.stringify({ 151 | "title": "Pagination", 152 | "type": "object", 153 | "properties": { 154 | "meta": { 155 | "type": "object", 156 | "properties": { 157 | "paginator": { 158 | "type": "object", 159 | "properties": { 160 | "total": { 161 | "type": "number" 162 | }, 163 | "perPage": { 164 | "type": "number" 165 | }, 166 | "totalPages": { 167 | "type": "number" 168 | }, 169 | "currentPage": { 170 | "type": "number" 171 | }, 172 | "queries": { 173 | "type": ["string", "null"] 174 | }, 175 | "links": { 176 | "type": "object", 177 | "properties": { 178 | "next": { 179 | "type": ["string", "null"] 180 | }, 181 | "previous": { 182 | "type": ["string", "null"] 183 | } 184 | }, 185 | "minProperties": 2 186 | } 187 | }, 188 | "minProperties": 6 189 | } 190 | }, 191 | "minProperties": 1 192 | }, 193 | "data": { 194 | "type": "array" 195 | } 196 | }, 197 | "minProperties": 2 198 | })); 199 | ``` 200 | 201 | Note how this pagination schema is saved directly as a global variable. We can then use these two pieces to define a schema for a single post and a list of posts (using pagination): 202 | 203 | ```Javascript 204 | pm.globals.set("my-project_tests_schema_post", JSON.stringify({ 205 | "title": "Post", 206 | post, 207 | "minProperties": 12 208 | })); 209 | 210 | pm.globals.set("my-project_tests_schema_post_list", JSON.stringify({ 211 | "title": "List of posts", 212 | "type": "array", 213 | "items": { 214 | post 215 | } 216 | })); 217 | ``` 218 | 219 | It is recommended that schemas are being defined in one place, which should be the "Pre-request Script" phase of your first endpoint test. This will make it easier to find any relevant schema for a test. 220 | 221 | ## Running your tests 222 | 223 | With the tests in place, it's time to actually run them. To run the tests while writing them, we recommend simply requesting the endpoint through Postman. As soon as endpoints should be run all together, the Postman Runner is more convenient. Inside Postman, click "Runner" at the top left corner to bring up a new window with the runner. To start your tests, here are the settings you should consider: 224 | 225 | - Collection: This should be your test collection (e.g. `my-project-tests`). 226 | - Environment: The environment you want to run your tests against. 227 | - Iterations: The number of iterations you want to run. 1 should be fine for most projects. 228 | - Delay: The delay between each request. 0 should fine for most projects. 229 | - Log Responses: This tells whether or not Postman Runner should output the request/response of each endpoint in the console. 230 | - Data: If you have a set of data you want to use as variables for your tests, this can be selected. 231 | - Persist Variables: If we were using environment variables in our tests, then this setting would persist them. 232 | 233 | With that configured, go ahead and click the "Run my-project-tests". This will start the runner which will go through each endpoint in the collection in the order they are arranged from top to bottom. For each test, the runner will output whether or it succeeded or failed. 234 | 235 | One thing to keep in mind when running your tests is that some tests might add data to your database which might not be deleted automatically afterwards (depending on your tests). This might be fine in some cases on test environments and in other cases it might be necessary to do some manual deletion afterwards. 236 | 237 | Last thing is to be pragmatic. Consider what code you're testing and if it makes sense to have API acceptance tests for certain endpoints. There might be endpoints which are difficult or impossible to test, for example endpoints that requires acting on an email before proceeding during a sign up. -------------------------------------------------------------------------------- /Documentation/guide-how-to-write-swift.md: -------------------------------------------------------------------------------- 1 | # How to write Swift 2 | 3 | The intention of this document is to guide how we should write Swift at Nodes. The document should be considered as work-in-progress and PR's with corrections etc. are much appreciated. 4 | 5 | ## Table of Contents 6 | 7 | - [Language](#language) 8 | - [Code Organization](#code-organization) 9 | - [Naming](#naming) 10 | - [Spacing](#spacing) 11 | - [Comments](#comments) 12 | - [Documentation](#documentation) 13 | - [Classes and Structures](#classes-and-structures) 14 | - [Type Inference](#type-inference) 15 | - [Access Control](#access-control) 16 | - [Generics](#generics) 17 | - [Extensions](#extensions) 18 | - [Syntactic Sugar](#syntactic-sugar) 19 | - [Parentheses](#parentheses) 20 | - [Functions vs Methods](#functions-vs-methods) 21 | - [Use of Self](#use-of-self) 22 | - [Golden Path](#golden-path) 23 | - [Xcode Warnings](#xcode-warnings) 24 | - [Outcommented Code](#outcommented-code) 25 | - [SwiftLint](#swiftlint) 26 | - [Miscellaneous](#miscellaneous) 27 | - [License](#license) 28 | 29 | 30 | 31 | ## Language 32 | 33 | Use US English spelling to match Apple's API. 34 | 35 | ## Code Organization 36 | 37 | Use marks ( `// MARK: -` and `// MARK:`) and comments throughout your code to keep things separated and well-organized. 38 | 39 | ## Naming 40 | 41 | Descriptive and consistent naming makes software easier to read and understand. Use the Swift naming conventions described in the [API Design Guidelines]( https://swift.org/documentation/api-design-guidelines/). Some key takeaways: 42 | 43 | - Method names and variables must start with a lower case letter. 44 | - Classes, structs, and type names should be capitalized. 45 | - The name of a variable should be enough to tell another programmer what that variable does. Don't use variable names such as `number`, `a`, `b`, `x`, `button`, `label`, etc. 46 | - To adhere to the Swift 3 syntax, all enum cases will be lowerCamelCase and not UpperCamelCase as before. 47 | - An [initialism](https://en.wiktionary.org/wiki/initialism) should be kept uppercase consistently (e.g.: `class AvatarURLTag`, `var avatarURL`.) unless the name starts with it, then we keep it lowercase (e.g. `var url: URL`) 48 | 49 | ## Spacing 50 | 51 | - Indent using 4 spaces. (Xcode pref. -> Text Editing -> Indentation and then make sure it is set to "Spaces" and width = 4.) 52 | - Long lines should be wrapped at **100** characters. (Xcode pref -> Text Editing -> Page guide at column 100.) 53 | - Avoid trailing whitespaces at the ends of lines. (Xcode pref. -> Text Editing and make sure "Automatically trim trail…" and "Including whitespace-only lines" checked.) 54 | - Add a single newline character at the end of each file. (Xcode will do this initially for you.) 55 | 56 | Tip: Setup Xcode to help you follow these guidelines. 57 | 58 | To achieve a column width of max 100 consider how you call and declare functions, how you use `guard` etc. For example: 59 | 60 | ```swift 61 | guard let a = test, let b = test2 else { 62 | 63 | } 64 | ``` 65 | 66 | Could be written: 67 | 68 | ```swift 69 | guard 70 | let a = test, 71 | let b = test2 72 | else { 73 | 74 | } 75 | ``` 76 | 77 | Similarly a function like: 78 | 79 | ```swift 80 | func products(for category: Category, on worker: Worker) throws -> Future<[Product]> { 81 | 82 | } 83 | ``` 84 | 85 | Could be written: 86 | 87 | ```swift 88 | func products( 89 | for category: Category, 90 | on worker: Worker 91 | ) throws -> Future<[Product]> { 92 | 93 | } 94 | ``` 95 | 96 | ## Comments 97 | 98 | When they are needed, use comments to explain why a particular piece of code does something. Comments must be kept up-to-date or deleted. 99 | 100 | Avoid block comments inline with code, as the code should be as self-documenting as possible. _Exception: This does not apply to those comments used to generate documentation._ 101 | 102 | ## Documentation 103 | 104 | At least all public interfaces should be documented, but it is also preferred that internal (and private) interfaces gets documented as well. The preferred style of documentation blocks follows the Swift API's and are as follows (both for one-line and multi-line documentation): 105 | 106 | ```Swift 107 | /// Shows all users. 108 | /// 109 | /// - Parameter request: The performed request. 110 | /// - Returns: A list of users. 111 | /// - Throws: Error if db is not setup correctly. 112 | ``` 113 | 114 | This documentation can be auto-generated by Xcode by placing the cursor on the declaration (being function, object etc.) and by clicking Editor -> Structure -> Add Documentation or `⌥⌘/`. 115 | 116 | ## Classes and Structures 117 | 118 | Remember, structs have [value semantics](https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_144). Use structs for things that do not have an identity. An array that contains `[a, b, c]` is really the same as another array that contains `[a, b, c]` and they are completely interchangeable. It doesn't matter whether you use the first array or the second, because they represent the exact same thing. That's why arrays are structs. 119 | 120 | Classes have [reference semantics](https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_145). Use classes for things that do have an identity or a specific life cycle. You would model a person as a class because two person objects are two different things. Just because two people have the same name and birthdate, doesn't mean they are the same person. But the person's birthdate would be a struct because a date of 3 March 1950 is the same as any other date object for 3 March 1950. The date itself doesn't have an identity. 121 | 122 | ## Type Inference 123 | 124 | Prefer compact code and let the compiler infer the type for constants or variables of single instances. Type inference is also appropriate for small (non-empty) arrays and dictionaries. When required, specify the specific type. 125 | 126 | In situations where type inference is not possible and there's an option to pass in the type in a function call, having the type on the constant/variable declaration is preferred. 127 | 128 | **Preferred**: 129 | ```swift 130 | let foo: MyType = try someFunction() 131 | ``` 132 | 133 | **Not preferred:** 134 | ```swift 135 | let foo = try someFunction(MyType.self) 136 | ``` 137 | 138 | ## Access Control 139 | 140 | It is preferred to limit scope as much as possible by using access modifiers. That said, redundant access modifiers should be avoided. 141 | 142 | ## Generics 143 | 144 | Generic type parameters should be descriptive, upper camel case names. When a type name doesn't have a meaningful relationship or role, use a traditional single uppercase letter such as `T`, `U`, or `V`. 145 | 146 | ## Extensions 147 | 148 | When adding a protocol implementation to a class or struct, add a separate extension for the protocol methods and use the `//MARK: -` comment. This increases the readability of the code. 149 | 150 | ```Swift 151 | final class Post: Model { 152 | // ... 153 | } 154 | 155 | // MARK: - Preparation 156 | extension Post: Preparation { 157 | // Preparation methods ... 158 | } 159 | ``` 160 | ## Syntactic Sugar 161 | 162 | Prefer the shortcut versions of type declarations over the full generics syntax. 163 | 164 | **Preferred:** 165 | 166 | ```swift 167 | var deviceModels: [String] 168 | var employees: [Int: String] 169 | var faxNumber: Int? 170 | ``` 171 | 172 | **Not Preferred:** 173 | 174 | ```swift 175 | var deviceModels: Array 176 | var employees: Dictionary 177 | var faxNumber: Optional 178 | ``` 179 | 180 | ## Parentheses 181 | 182 | Parentheses around conditionals are not required and should be omitted. 183 | 184 | ## Functions vs Methods 185 | 186 | Free functions, which aren't attached to a class or type, should be used sparingly. When possible, prefer to use a method instead of a free function. This aids in readability and discoverability. 187 | 188 | Free functions are most appropriate when they aren't associated with any particular type or instance. 189 | 190 | ## Use of Self 191 | 192 | For conciseness, avoid using `self` since Swift does not require it to access an object's properties or invoke its methods. 193 | 194 | Use self only when required by the compiler (in `@escaping` closures, or in initializers to disambiguate properties from arguments). In other words, if it compiles without `self` then omit it. 195 | 196 | ## Golden Path 197 | 198 | When coding with conditionals, the left-hand margin of the code should be the "golden" or "happy" path. That is, don't nest `if` statements. Multiple return statements are OK. The `guard` statement is built for this. 199 | 200 | **Preferred:** 201 | 202 | ```swift 203 | func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies { 204 | 205 | guard let context = context else { 206 | throw FFTError.noContext 207 | } 208 | guard let inputData = inputData else { 209 | throw FFTError.noInputData 210 | } 211 | 212 | // use context and input to compute the frequencies 213 | 214 | return frequencies 215 | } 216 | ``` 217 | 218 | **Not Preferred:** 219 | 220 | ```swift 221 | func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies { 222 | 223 | if let context = context { 224 | if let inputData = inputData { 225 | // use context and input to compute the frequencies 226 | 227 | return frequencies 228 | } else { 229 | throw FFTError.noInputData 230 | } 231 | } else { 232 | throw FFTError.noContext 233 | } 234 | } 235 | ``` 236 | 237 | ## Xcode Warnings 238 | 239 | Try to keep the number of warnings in a project to 0. If there is a really good reason for a warning to be there, consider adding a comment explaining why it is acceptabel to be there (temporarily). 240 | 241 | ## Outcommented Code 242 | 243 | Don't leave out commented code. Or if you feel you really need to, also leave a comment saying why that code is there and why it might be needed again. But in general, delete commented out code. We use git, so you can always get old code from there. 244 | 245 | ## SwiftLint 246 | 247 | [SwiftLint](https://github.com/realm/SwiftLint) is a tool (a linter) to help us make sure we follow the agreed guidelines. It can be installed by running (assuming `brew` is installed): 248 | 249 | ``` 250 | brew install swiftlint 251 | ``` 252 | 253 | Then move the `.swiftlint.yml` configuration from this repo into the root of your project directory. Run SwiftLint by calling: 254 | 255 | ``` 256 | swiftlint 257 | ``` 258 | 259 | If the configuration filed has been placed correctly, it will automatically pick it up and apply it. The terminal should now show you any violations you might have made. The SwiftLint repo also has documentation on how to integrate SwiftLint into Xcode, making the IDE display the errors and warnings on the specific lines they are occurring. 260 | 261 | Depending on the project, you may want to commit the SwiftLint configuration file. The configuration file will change over time to accommodate our needs as we go along. 262 | 263 | ## Miscellaneous 264 | 265 | - Use `let` and not `var` wherever possible. 266 | - Avoid force-unwrapping optionals, instead use a `if let` or similar. Alternatively use optional chaining `myVar?.someFuncToCallIfMyVarIsNotNil()`. 267 | 268 | ## License 269 | 270 | This guide was heavily inspired by [Ray Wenderlich's Swift style guide](https://github.com/raywenderlich/swift-style-guide) and the [Nodes iOS Playbook](https://github.com/nodes-ios/Playbook/blob/master/styleguide.md). The copyright notice from the Ray Wenderlich Swift style guide repo is added below: 271 | 272 | ``` 273 | This style guide is available under the MIT license: 274 | 275 | Original work Copyright (c) 2016-2017 Razeware LLC 276 | Modified work Copyright (c) 2016 Nodes Vapor 277 | 278 | Permission is hereby granted, free of charge, to any person obtaining a copy 279 | of this software and associated documentation files (the "Software"), to deal 280 | in the Software without restriction, including without limitation the rights 281 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 282 | copies of the Software, and to permit persons to whom the Software is 283 | furnished to do so, subject to the following conditions: 284 | 285 | The above copyright notice and this permission notice shall be included in 286 | all copies or substantial portions of the Software. 287 | ``` 288 | 289 | -------------------------------------------------------------------------------- /Documentation/how-to-firebase-cloud-message.md: -------------------------------------------------------------------------------- 1 | # Sending push notifications with Firebase cloud message (FCM) 2 | 3 | Before digging into details it's important to understand the architectural idea 4 | ![image](https://user-images.githubusercontent.com/1279756/41772490-2402772a-7619-11e8-9acf-f0c17cbd0b75.png) 5 | 6 | # Diagram 7 | 1) Through the FCM-SDK (or the native integration) the app will request a push-token at its platform push-network. This will trigger a popup to allow push notification on most platforms. This push-token is unique for the App, device & certification (release, debug etc) and will expire after x days. Luckily the UA SDK will deal with refreshing it. 8 | 9 | 2) Through FCM-SDK / FCM-API the push-token will be sent to FCM and stored. FCM can now send push notifications to that device & app & certificate. Since we do not want to deal with push-tokens vs users references. It's important to register to topic as well. [Read more](https://github.com/nodes-projects/readme/blob/master/mobile/firebase-push-guide.md#topics) 10 | 11 | 3) Sending a push notification from a backend project happens via the API. It's normally triggered by 12 | - Admin panel as a view, send to X, Y & Z with a custom alert. 13 | - Triggered by an event, fx some else liked your post. The alert message would be from NStack / Localization 14 | 15 | 4) FCM will look up the topic or topic conditions, loop through each push-tokens registered to this channel. And sent 1-by-1 to each push-network. This is done Async in queue. 16 | 17 | 5) The push-network will queue the request from FCM and sent the push notification through a socket connection to the device. If the device is not online, it will save it for x hours (depending on push-network) 18 | 19 | # SDKs 20 | 21 | Vapor: https://github.com/mdab121/vapor-fcm :warning: 22 | 23 | **Until they have merged my PR, please use https://github.com/cweinberger/vapor-fcm/releases/tag/2.1.1. Otherwise your will always receive an unsuccessful Response from the framework (even though the notification was sent successfully).** 24 | 25 | PHP: https://github.com/kreait/firebase-php 26 | 27 | # Read here for detailed description of push options for iOS, Android & Web 28 | 29 | iOS:https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#ApnsConfig 30 | 31 | Android: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidNotification 32 | 33 | Web: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushconfig 34 | 35 | # Setting up projects in Firebase 36 | 37 | In FCM Console you create projects for each application & environment. 38 | 39 | For application ABC there should be 3 FCM projects 40 | 41 | - ABC - Development 42 | - ABC - Staging 43 | - ABC - Production 44 | 45 | Within each project you can set up the apps for iOS, Android (and Web). 46 | 47 | The API credential & url can always be found at 48 | 49 | https://console.firebase.google.com/u/0/project/[app-name]/settings/serviceaccounts/adminsdk 50 | 51 | Set up android / ios / web cloud message credentials here 52 | 53 | https://console.firebase.google.com/u/0/project/[app-name]/settings/serviceaccounts/cloudmessaging 54 | 55 | Note: Credentials (json) and database URI should be setup as server variables and never be hardcoded 56 | 57 | # Alert 58 | A push message has a "alert" which is the string showed in the notification center. It has a limit of 255 chars. So limit it to less. 59 | 60 | Always put the alert message in NStack / Localization 61 | 62 | Note: iOS already truncate them earlier (around 110 chars) 63 | 64 | __We highly recommend using both title & body, but body is option__ 65 | 66 | You can either use the notification object in firebase sdk or use the APNS / Android specific objects 67 | 68 | 69 | PHP: Generic alert both with title & body 70 | ```php 71 | $notification = Notification::create() 72 | ->withTitle($title) 73 | ->withBody($body); // Optional 74 | ``` 75 | PHP: Android specific message 76 | ```php 77 | $config = AndroidConfig::fromArray([ 78 | 'ttl' => '3600s', 79 | 'priority' => 'normal', 80 | 'notification' => [ 81 | 'title' => '$GOOG up 1.43% on the day', 82 | 'tag' => '$GOOG', 83 | 'body' => '$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.', 84 | 'icon' => 'ic_stock_ticker', 85 | 'color' => '#f45342' 86 | ], 87 | ]); 88 | ``` 89 | 90 | *Notification keys and value options*: 91 | `tag`: Identifier used to replace existing notifications in the notification drawer. If not specified, each request creates a new notification. If specified and a notification with the same tag is already being shown, the new notification replaces the existing one in the notification drawer. 92 | 93 | `color`: The notification's icon color, expressed in #rrggbb format. 94 | 95 | `icon`: Unless specified, let the Android app deal with this. Kind of similar to sound, but with notification icons instead. 96 | 97 | 98 | PHP: iOS Specifc message 99 | ```php 100 | $config = ApnsConfig::fromArray([ 101 | 'headers' => [ 102 | 'apns-priority' => '10', 103 | ], 104 | 'payload' => [ 105 | 'aps' => [ 106 | 'alert' => [ 107 | 'title' => '$GOOG up 1.43% on the day', 108 | 'body' => '$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.', 109 | ], 110 | 'badge' => 42, 111 | ], 112 | ], 113 | ]); 114 | ``` 115 | 116 | 117 | # Payloads / Extra / data 118 | 119 | Payload is a way to pass more information, fx for deeplinking 120 | 121 | PHP: Example of a payload 122 | ```php 123 | $message = $message->withData($[ 124 | 'first_key' => 'First Value', 125 | 'second_key' => 'Second Value', 126 | ]); 127 | ``` 128 | 129 | This will let the app know where to deeplink 130 | 131 | FCM does not support nested data. If you need to send a full model, consider json-encoding it to 1 key 132 | 133 | Note: there is limits on iOS for maximum 2kb of data 134 | 135 | # Topics 136 | 137 | We always register userId as a topic `user_[ID]`eg `user_1` 138 | 139 | Topics can be used very smart, and should avoid every situation of having to store push settings in the DB. 140 | 141 | ## Example 1) 142 | 143 | Feature: Push on breaking news which is optional for users (e.g. configurable in settings) 144 | 145 | The app has to subscribe to a topic `breakingNews` if the user turns on that setting (i.e. through a toggle). 146 | If the user disables the setting, the app has to unsubscribe from `breakingNews`. 147 | 148 | Whenever breaking news are to be shared, the backend will send a FCM (push) message to the topic `breakingNews`. 149 | 150 | ## Example 2) 151 | 152 | Feature: Recieving push notifications about athlete with id: 12345, after favoriting them 153 | 154 | The app will subscribe to the topic `athelete_12345` 155 | 156 | The backend will now just push to the topic `athlete\_[ID]` instead of looping users which have favorited it (this way, it might be even not necessary to store the relation/state (user favorited athlete) on the backend) 157 | 158 | ## Example 3) 159 | 160 | The app has options to only receive news push notification during race or always 161 | 162 | The app will subscribe to the topic `newsAlways` or `newsDuringRace` or both 163 | 164 | The Backend will push to those topics, but will figure out if the the current time is during race and add that topic as well. 165 | 166 | Note: Users who subscribed to both topics will not get the notification twice. 167 | 168 | PHP: topic 169 | ```php 170 | $topic = 'a-topic'; 171 | 172 | $message = MessageToTopic::create($topic) 173 | ->withNotification($notification) // optional 174 | ->withData($data) // optional 175 | ; 176 | ``` 177 | 178 | PHP: Topic conditions 179 | ```php 180 | $condition = "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"; 181 | 182 | $message = ConditionalMessage::create($condition) 183 | ->withNotification($notification) // optional 184 | ->withData($data) // optional 185 | ; 186 | ``` 187 | 188 | ## Sound 189 | 190 | There is 3 options for sound 191 | 192 | ### Standard device sound 193 | 194 | PHP iOS Standard sound 195 | ```php 196 | ->withApnsConfig(ApnsConfig::fromArray([ 197 | 'payload' => [ 198 | 'aps' => [ 199 | 'alert' => $message, 200 | 'sound' => 'default', 201 | ], 202 | ], 203 | ])); 204 | ``` 205 | 206 | PHP Android Standard sound 207 | ```php 208 | $config = AndroidConfig::fromArray([ 209 | 'ttl' => '3600s', 210 | 'priority' => 'normal', 211 | 'notification' => [ 212 | 'title' => '$GOOG up 1.43% on the day', 213 | 'body' => '$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.', 214 | 'sound' => 'default' 215 | ], 216 | ]); 217 | ``` 218 | 219 | ### No sound 220 | 221 | Android: The documentation is pretty lackluster on this. For Android 8.0+ notification channel's will probably override this anyway. 222 | iOS: empty string. "" 223 | 224 | PHP iOS No sound 225 | ```php 226 | ->withApnsConfig(ApnsConfig::fromArray([ 227 | 'payload' => [ 228 | 'aps' => [ 229 | 'alert' => $message, 230 | 'sound' => '', 231 | ], 232 | ], 233 | ])); 234 | ``` 235 | 236 | ### Custom sound 237 | 238 | Here we have to set sound for ios & android specificly 239 | 240 | `sound`: The sound to play when the device receives the notification. Supports "default" or the filename of a sound resource bundled in the app i.e. `stocksound.mp3`. Android: Sound files must reside in /res/raw/. 241 | 242 | PHP Android example 243 | 244 | ```php 245 | ->withAndroidConfig(AndroidConfig::fromArray([ 246 | 'notification' => [ 247 | 'title' => $message, 248 | 'sound' => $androidSound ? ('arrivedsound.mp3') : null, 249 | ], 250 | ])) 251 | ``` 252 | 253 | PHP Android example 254 | 255 | ```php 256 | ->withApnsConfig(ApnsConfig::fromArray([ 257 | 'payload' => [ 258 | 'aps' => [ 259 | 'alert' => $message, 260 | 'sound' => 'arrivedsound.wav', 261 | ], 262 | ], 263 | ])); 264 | ``` 265 | 266 | ## Priority 267 | 268 | This is starting to be a very important matter, notification without high prio. Can easily take minutes to be send 269 | 270 | *PHP: Android high priority* 271 | 272 | `priority`: Message priority. Can take "normal" and "high" values. 273 | 274 | *Normal priority messages* are delivered immediately when the app is in the foreground. When the device is in Doze or the app is in app standby, delivery may be delayed to conserve battery. For less time-sensitive messages, such as notifications of new email, keeping your UI in sync, or syncing app data in the background, choose normal delivery priority. 275 | 276 | FCM attempts to deliver *high priority messages* immediately, allowing the FCM service to wake a sleeping device when necessary and to run some limited processing (including very limited network access). High priority messages generally should result in user interaction with your app. If FCM detects a pattern in which they don't, your messages may be de-prioritized. 277 | 278 | ```php 279 | ->withAndroidConfig(AndroidConfig::fromArray([ 280 | 'priority' => 'high', 281 | 'notification' => [ 282 | 'title' => $message, 283 | ], 284 | ])) 285 | ``` 286 | 287 | *PHP: iOS high priority* 288 | 289 | ```php 290 | ->withApnsConfig(ApnsConfig::fromArray([ 291 | 'payload' => [ 292 | 'aps' => [ 293 | 'alert' => $message, 294 | 'sound' => 'arrivedsound.wav', 295 | 'header' => [ 296 | 'apns-priority' => 10, 297 | ], 298 | ], 299 | ], 300 | ])); 301 | ``` 302 | 303 | No priority 10 is highest, but does not work together with content-available 304 | 305 | ## Silent / Content-available 306 | 307 | Sometimes we use push to notifity the phone about a update, but we do not want it to show in the notification center. 308 | eg booking was updated, with updates in payload or please pull newest booking 309 | 310 | ### PHP iOS example 311 | ```php 312 | $apns = [ 313 | 'payload' => [ 314 | 'aps' => [ 315 | 'alert' => $message, 316 | 'sound' => $iosSound, 317 | 'content-available' => $silent ? 1 : 0, 318 | 'header' => [ 319 | 'apns-priority' => $silent ? 5 : 10, 320 | ], 321 | ], 322 | ], 323 | ]; 324 | 325 | if($silent) { 326 | unset($apns['payload']['aps']['alert']); 327 | unset($apns['payload']['aps']['sound']); 328 | } 329 | 330 | ->withApnsConfig($apns); 331 | ``` 332 | The reason of this code, is that "alert" & "sound" is not allowed when sending silent pushes. It should not even be in object, else it will be ignored as silent 333 | Same goes for priorty, 5 is highest for silent 334 | 335 | 336 | ### PHP Android example 337 | To send silent push notifications send a payload with only `data` object set. The Android app will then receive data payload in `onMessageReceived`. 338 | ```php 339 | $message = $message->withData([ 340 | 'someKey' => 'someValue', 341 | ]); 342 | ``` 343 | 344 | 345 | ## Badge count 346 | This is originally an iOS feature 347 | ![image](https://cloud.githubusercontent.com/assets/1279756/25580264/c21753e6-2e7f-11e7-9499-265f73ea79e9.png) 348 | 349 | But there is an option for doing something simular on android also 350 | 351 | ![image](https://cloud.githubusercontent.com/assets/1279756/25580290/084cdfe8-2e80-11e7-9f09-e13c0c282866.png) 352 | 353 | There is 2 ways of doing badge count 354 | 355 | ### The big solution 356 | 357 | Like FB, Google etc, let the server keep track of unread count. And send silent push notification when this updates, to keep all you divices / platforms aligned 358 | 359 | This can be very time consuming and often require a full activity / notification system before hand 360 | 361 | ### The simple solution 362 | 363 | Use the +1 value, which will increase the counter by one in ios. And when you open to the app / specific view, you clear the count 364 | 365 | PHP: Android, there is no build in badge count, just add it to payload 366 | ```php 367 | $message = $message->withData([ 368 | 'badge' => 45, 369 | ]); 370 | ``` 371 | 372 | PHP: iOS 373 | ```php 374 | ->withApnsConfig(ApnsConfig::fromArray([ 375 | 'payload' => [ 376 | 'aps' => [ 377 | 'badge' => 45 378 | 'alert' => $message, 379 | 'sound' => 'arrivedsound.wav', 380 | 'header' => [ 381 | 'apns-priority' => 10, 382 | ], 383 | ], 384 | ], 385 | ])); 386 | ``` 387 | 388 | # Localization 389 | 390 | First you need to setup a system to keep track of language for each user. 391 | 392 | Setup a field on the user model "locale" and store the Accept-Language header here. 393 | Now when sending a push notification you can look up the user's locale beforehand, and thereby translate it. 394 | 395 | This solution requires you to loop through each user. It is possible to send arrays of namedUsers/aliases to minimize the network to Firebase (which is the slow part) 396 | 397 | Note: Also a creative solutions with tags can be used 398 | 399 | # Logs 400 | 401 | Unfortually there is no logs in FCM of push messages sent. Therefore we integrated this feature in NStack, where push logs are stored for 3 months 402 | 403 | It's called "Push logs" [Link](https://nstack.io/admin/ugc/push-logs) 404 | 405 | PHP example using the nodes/nstack package 406 | ```php 407 | 408 | $request = $messageObject->jsonSerialize(); 409 | 410 | try { 411 | $response = $this->getClient()->getMessaging()->send($messageObject); 412 | 413 | try { 414 | nstack()->pushLog( 415 | 'fcm', 416 | app()->environment(), 417 | $data['type'] ?? 'N/A', 418 | true, 419 | $request, 420 | $response, 421 | $message, 422 | $userId, 423 | $relation 424 | ); 425 | } catch (\Throwable $e) { 426 | bugsnag_report($e); 427 | } 428 | } catch (\Throwable $e) { 429 | // report 430 | bugsnag_report($e); 431 | 432 | try { 433 | $response = [ 434 | 'code' => $e->getCode(), 435 | 'message' => $e->getMessage(), 436 | ]; 437 | 438 | nstack()->pushLog( 439 | 'fcm', 440 | app()->environment(), 441 | $data['type'] ?? 'N/A', 442 | false, 443 | $request, 444 | $response, 445 | $message, 446 | $userId, 447 | $relation 448 | ); 449 | } catch (\Throwable $e) { 450 | bugsnag_report($e); 451 | } 452 | } 453 | 454 | ``` 455 | 456 | 457 | -------------------------------------------------------------------------------- /Documentation/how-to-write-apis.md: -------------------------------------------------------------------------------- 1 | # !!! DEPRECATED !!! 2 | These guidelines are no longer in use 3 | Please use [The API Manifesto](https://github.com/monstar-lab-oss/API-manifesto) instead 4 | 5 | # How to write APIs 6 | 7 | ## Table of Contents 8 | 9 | - [Introduction](#introduction) 10 | - [Endpoints](#endpoints) 11 | - [Anatomy of an endpoint](#anatomy-of-an-endpoint) 12 | - [Request methods](#request-methods) 13 | - [Examples](#examples) 14 | - [Tips on what NOT to do](#tips-on-what-not-to-do) 15 | - [Headers](#headers) 16 | - [Images](#images) 17 | - [A few things to remember](#a-few-things-to-remember) 18 | - [Examples](#examples-1) 19 | - [Pagination](#pagination) 20 | - [Using `lastId`](#using-lastid) 21 | - [Examples](#examples-2) 22 | - [Using pages](#using-pages) 23 | - [Examples](#examples-3) 24 | - [Authentication](#authentication) 25 | - [3rd party authentication](#3rd-party-authentication) 26 | - [HTTP Response Status Codes](#http-response-status-codes) 27 | - [Response](#response) 28 | - [Errors](#errors) 29 | - [Vapor 1](#vapor-1) 30 | - [Vapor 2](#vapor-2) 31 | - [Examples](#examples-4) 32 | - [A single item](#a-single-item) 33 | - [An endpoint without meaningful data to return](#an-endpoint-without-meaningful-data-to-return) 34 | - [An error in Vapor 2](#an-error-in-vapor-2) 35 | - [A collection of items](#a-collection-of-items) 36 | - [A paginated collection of items](#a-paginated-collection-of-items) 37 | 38 | ## Introduction 39 | 40 | This document is a specification of how we are doing (and should do) our internal API’s at Nodes. Our API’s should support usage from our front-end and mobile applications. 41 | 42 | If you’re in one of the development departments, consider this to be your personal API bible. If you are ever in doubt about a response code or how to format a URL, look to this document. If it’s not described in here, feel free to make a pull request or reach out to any of the backend developers at Nodes. 43 | 44 | We all know that not all projects are the same. So at some point, you might have to bend the rules, use a different convention or even an "incorrect" response code. This is okay, just make sure to go over it with your backend colleagues and inform who is implementing the API of your change. 45 | 46 | ## Endpoints 47 | 48 | Endpoints with literal and readable URLs is what makes an API awesome. So to make everything easy and convenient for you, we have specified how you should do it. No more thinking about if it should be plural or where you should put your slug. 49 | 50 | ### Anatomy of an endpoint 51 | 52 | The anatomy of an endpoint should look like this: 53 | 54 | ``` 55 | /api/{objects}/{slug}/{action}?[filters]&[sorting]&[limit]&[...] 56 | ``` 57 | 58 | Below is a breakdown of the different pieces in the endpoint: 59 | 60 | 1. All our API’s are prefixed with `/api/`. 61 | 2. `{objects}` is the name of the object(s) you are returning. Let’s say you’re retrieving a collection of posts. Then `{objects}` should be `posts`. 62 | 3. `{slug}` can theoretically be anything you’d like. It’s just used as an identifier, usually a unique one. A `{slug}` is used to retrieve a specific item in a collection of objects. 63 | 4. `{action}` is used when we want to perform a certain action. This could be, for example, `like`, `follow` or `comment`. The purpose of the action usually depends on the request method. 64 | 5. `[filters]` is mostly used when we’re retrieving a collection of objects. There’s no limit to how many filters you can use. All that is required is that they are specified as query parameters. 65 | 6. `[sorting]` is exactly like filters. No limit to how many you can use. Just make sure they’re specified as query parameters. 66 | 7. `[limit]` is where we specify how many items in a collection we want returned. 67 | 8. `[...]` is basically there to tell you that all custom stuff for an endpoint should be specified as parameters. 68 | 69 | ### Request methods 70 | 71 | The request method is the way we distinguish what kind of action our endpoint is being "asked" to perform. For example, `GET` pretty much gives itself. But we also have a few other methods that we use quite often. 72 | 73 | | Method | Description | 74 | | -------- | ---------------------------------------- | 75 | | `GET` | Used to retrieve a single item or a collection of items. | 76 | | `POST` | Used when creating new items e.g. a new user, post, comment etc. | 77 | | `PATCH` | Used to update one or more fields on an item e.g. update e-mail of user. | 78 | | `PUT` | Used to replace a whole item (all fields) with new data. | 79 | | `DELETE` | Used to delete an item. | 80 | 81 | ### Examples 82 | 83 | Now that we’ve learned about the anatomy of our endpoints and the different request methods that we should use, it’s time for some examples: 84 | 85 | | Method | URL | Description | 86 | | -------- | ---------------------------------------- | ---------------------------------------- | 87 | | `GET` | `/api/posts` | Retrieve all posts. | 88 | | `POST` | `/api/posts` | Create a new post. | 89 | | `GET` | `/api/posts/28` | Retrieve post #28. | 90 | | `PATCH` | `/api/posts/28` | Update data in post #28. | 91 | | `POST` | `/api/posts/28/comments` | Add comment to post #28. | 92 | | `GET` | `/api/posts/28/comments?status=approved&limit=10&page=4` | Retrieve page 4 of the comments for post #28 which are approved, with 10 comments per page. | 93 | | `DELETE` | `/api/posts/28/comments/1987` or `/api/comments/1987` | Delete comment #1987.                   | 94 | | `GET` | `/api/users?active=true&sort=username&direction=asc&search=nodes` | Search for "nodes" in active users, sorted by username ascendingly. | 95 | 96 | #### Tips on what NOT to do 97 | 98 | Hopefully you’re already aware of this. But not at any given time should your endpoint end with `.json`, `.xml` or `.something`. If in doubt, reach out to one of the backend developers at Nodes. 99 | 100 | ## Headers 101 | 102 | We love headers! And we have a few that we use pretty much in every one of our endpoints. It’s also a very nice way to send information along with your request without "parameter bombing" your endpoint, e.g. with language or 3rd party tokens. 103 | 104 | Below is a list of our most used headers. You might end up working on a project where you have to integrate with this weird 3rd party API, which requires you to make a custom header that is not listed below. Go for it, Batman. No requirements here. 105 | 106 | | Header key | Description | 107 | | ----------------- | ---------------------------------------- | 108 | | `Accept` | This header is **required by all endpoints**. It’s used to identify the request as our own and for versioning our endpoints. **Default value**: `application/vnd.nodes.v1+json`. | 109 | | `Accept-Language` | The [ISO 639](http://www.loc.gov/standards/iso639-2/php/code_list.php) code of language translations should be returned in. | 110 | | `Authorization` | The authorized user’s token. This is used to gain access to protected endpoint. | 111 | | `N-Meta` | Meta data about the consumer, such as platform, environment and more, see the [N-Meta package](https://github.com/nodes-vapor/n-meta) | 112 | | `Facebook-Token` | Facebook Graph token. | 113 | | `Instagram-Token` | Instagram OAuth token. | 114 | | `Twitter-Token` | Twitter OAuth token. | 115 | | `LinkedIn-Token` | LinkedIn OAuth token. | 116 | | `Latitude` | Latitude of a geolocation. This is **only** sent as a header if it’s being used in (almost) every endpoint. | 117 | | `Longitude` | Longitude of a geolocation. This is **only** sent as a header if it’s being used in (almost) every endpoint. | 118 | 119 | ## Images 120 | 121 | Our APIs will always return the URL to the originally uploaded image. The controlling of dimension, cropping method etc., should be handled by the applications. 122 | 123 | The table below shows all available settings you can use on an image. The parameters should be appended to the URL of the image as query parameters. 124 | 125 | | Query key | Type | Description | Extra comments | 126 | | ---------- | -------- | ---------------------------------------- | ---------------------------------------- | 127 | | `w` | `Int` | Width of image. | Optional when `mode` is set to `resize`. | 128 | | `h` | `Int` | Height of image. | Optional when `mode` is set to `resize` or `fit`. | 129 | | `mode` | `String` | Available modes: `crop`, `resize` and`fit`. | Defaults to `resize`. | 130 | | `download` | `Bool` | If set to `true` it will force the image to be downloaded. | Defaults to `false`. | 131 | 132 | ### A few things to remember 133 | 134 | - When using the mode `resize` and you only provide either width or height, it will resize by the image’s aspect ratio. 135 | - When using the mode `crop` both width and height are **required**. It will always crop from the center of the image and out. 136 | - When using the mode `fit` the image will both be resized **AND** cropped to fit the desired dimensions. 137 | - Images **can not** be upscaled. Meaning that if you request a height of `200` but the image only has a height of `100`, than it will be returned with a height of `100`. 138 | 139 | ### Examples 140 | 141 | Fear not! No table with a list of available options comes without a section with examples of how to use them. 142 | 143 | | URL | Description | 144 | | ---------------------------------- | ---------------------------------------- | 145 | | `/image.jpg?w=500` | Set width to `500` and use the height from image’s aspect ratio. | 146 | | `/image.jpg?h=250` | Set height to `250` and use the width from the image’s aspect ratio. | 147 | | `/image.jpg?w=100&h=100` | Resize image to `100x100`. | 148 | | `/image.jpg?w=250&h=250&mode=crop` | Crop image to `250x250` from the center of image. | 149 | | `/image.jpg?w=300&mode=fit` | Fit image to a width of `300`. | 150 | | `/image.jpg?w=150&h=100&mode=fit` | Fit image to `150x100`. | 151 | | `/file.pdf` | No parameters makes it viewable. | 152 | | `/file.pdf?download=true` | Force download of file. | 153 | 154 | ## Pagination 155 | 156 | This is one of the tricky parts. Depending on what type of API you’re doing, pagination is implemented in different ways. 157 | 158 | If you're making an API to be used on the web, simple pagination using "pages" would work just fine. This is supported by our [Paginator package](https://github.com/nodes-vapor/paginator) and should work out of the box. 159 | 160 | But if you’re making an API for the mobile team(s) then you need to do it in a bit more complex way. Because devices usually have a "load more" feature, we can’t use the "pages" approach, since we could risk getting duplicates or even miss new entries. Therefore we return the collection in "batches" instead of pages. 161 | 162 | See the Response section for examples of how to return meta data for pagination. 163 | 164 | ### Using `lastId` 165 | 166 | Let’s assume we want to have a feed of posts. 10 posts per "load more". Every time our endpoint is being requested we return a collection of 10 posts. When a user then scrolls to the bottom of the feed, they trigger the "load more" feature. The mobile app then requests the same endpoint but with a `lastId` query parameter. 167 | 168 | This parameter contains the ID of the last item in the previous returned batch of posts. When we receive the `lastId` parameter, we use that ID to only return posts that have IDs which are greater than the received `lastId`. 169 | 170 | Please note that in order for this approach to work, the collection needs to be sorted by ID. 171 | 172 | #### Examples 173 | 174 | It might sound a bit complicated, but it’s really not. Here is a few examples, which hopefully makes it a bit more understandable. Otherwise just ask one of your co-workers. 175 | 176 | | Endpoint | Description | 177 | | ----------------------- | ---------------------------------------- | 178 | | `/api/posts` | Initial request. Return first 10 posts. | 179 | | `/api/posts/?lastId=10` | 1st time we "load more". Return 10 posts starting from ID #11. | 180 | | `/api/posts/?lastId=20` | 2nd time we "load more". Return 10 posts starting from ID #21. | 181 | | `/api/posts/?lastId=30` | 3rd time we "load more". Return 10 posts starting from ID #31. | 182 | 183 | ### Using pages 184 | 185 | If one decides that a "pages" approach works for their API, the `Paginator` package can be used. This package takes care of paginating the results as well as building the meta data for the response. 186 | 187 | #### Examples 188 | 189 | | Endpoint | Description | 190 | | -------------------- | ---------------------------------------- | 191 | | `/api/posts` | Initial request. Return first 10 posts. | 192 | | `/api/posts/?page=2` | Second page, returning 10 posts using an offset of 10. | 193 | | `/api/posts/?page=3` | Third page, returning 10 posts using an offset of 20. | 194 | 195 | ## Authentication 196 | 197 | One of the most essential parts of an API is authentication. This is why authentication is also one of the most important parts - security wise - when you’re making an API. Therefore we’ve set up a few guidelines, which you should follow at any time. Most of them are pretty obvious, but nonetheless it’s better to be safe than sorry. 198 | 199 | - **Always** use a SSL-encrypted connection when trying to authenticate an user. 200 | - **Always** save passwords hashed/encrypted. Never save passwords as plain text. 201 | - **Never** save 3rd party tokens (i.e. Facebook, Twitter or Instagram). 202 | - The **only time** you should ever return an user’s API token is when a user either is **successfully created** or **successfully authenticated**. 203 | 204 | ### 3rd party authentication 205 | 206 | In some projects we might have to give a user the option to authenticate with e.g. their Facebook account. To support this, you need to have a column on your user where you can save the user’s Facebook ID. 207 | 208 | Then you make an endpoint, which requires a valid Facebook token. You extract the Facebook ID from the received token and look for an entry with that Facebook ID in your database. If found, you authenticate the found user. If not found, either create the user or return invalid login, depending on the project specification. 209 | 210 | Not at any given time should you perform token authentication. The department implementing your API should always have done that before requesting your endpoint. If they don’t send a valid token, you should return an error and not continue with your authentication. 211 | 212 | ## HTTP Response Status Codes 213 | 214 | One of the most important things in an API is how it returns response codes. Each response code means a different thing and consumers of your API rely heavily on these codes. 215 | 216 | | Code | Title | Description | 217 | | ----- | ------------------------- | ---------------------------------------- | 218 | | `200` | `OK` | When a request was successfully processed (e.g. when using `GET`, `PATCH`, `PUT` or `DELETE`). | 219 | | `201` | `Created` | Every time a record has been added to the database (e.g. when creating a new user or post). | 220 | | `304` | `Not modified` | When returning a cached response. | 221 | | `400` | `Bad request` | When the request could not be understood (e.g. invalid syntax). | 222 | | `401` | `Unauthorized` | When authentication failed. | 223 | | `403` | `Forbidden` | When an authenticated user is trying to perform an action, which he/she does not have permission to. | 224 | | `404` | `Not found` | When URL or entity is not found. | 225 | | `440` | `No accept header` | When the required "Accept" header is missing from the request. | 226 | | `422` | `Unprocessable entity` | Whenever there is something wrong with the request (e.g. missing parameters, validation errors) even though the syntax is correct (ie. `400` is not warranted). | 227 | | `500` | `Internal server error` | When an internal error has happened (e.g. when trying to add/update records in the database fails). | 228 | | `502` | `Bad Gateway` | When a necessary third party service is down. | 229 | 230 | The response codes often have very precise definition and are easily misunderstood when just looking at their names. For example, `Bad Request` refers to malformed requests and not, as often interpreted, when there is something semantically wrong with the reuquest. Often `Unprocessable entity` is a better choice in those cases. 231 | Another one that is often used incorrectly is `Precondition Failed`. The precondition this status code refers to are those defined in headers like `If-Match` and `If-Modified-Since`. Again, `Unprocessable entity` is usually the more appropriate choice if the request somehow isn't valid in the current state of the server. 232 | When in doubt, refer to [this overview](https://httpstatuses.com) and see if the description of an status code matches your situation. 233 | 234 | ## Response 235 | 236 | Generally we have a few rules the response has to follow: 237 | 238 | - Root should always be returned as an object. 239 | - Keys should always be returned as camelCase. 240 | - When we don’t have any data, we need to return in the following way: 241 | - Collection: Return empty array. 242 | - Empty key: Return null or unset the key. 243 | - Consistency of key types. e.g. always return IDs as an integer in all endpoints. 244 | - Date/timestamps should always be returned with a time zone. 245 | - Content (being a single object or a collection) should be returned in a key (e.g. `data`). 246 | - Pagination data should be returned in a `meta` key. 247 | - Endpoints should always return a JSON payload. 248 | - When an endpoint doesn't have meaningful data to return (e.g. when deleting something), use a `status` key to communicate the status of the endpoint. 249 | 250 | ### Errors 251 | 252 | When errors occur the consumer will get a JSON payload verifying that an error occurred together with a reason for why the error occurred. 253 | 254 | Error handling has changed from Vapor 1 through 3, these are the keys to expect from the different versions. 255 | 256 | #### Vapor 3 257 | 258 | | Endpoint | Description | 259 | | ---------- | ---------------------------------------- | 260 | | `error` | A boolean confirming an error occurred. | 261 | | `reason` | A description of the error that occurred. For some errors this value provides extra information on non-production environments. | 262 | 263 | #### Vapor 2 264 | 265 | | Endpoint | Description | 266 | | ---------- | ---------------------------------------- | 267 | | `error` | A boolean confirming an error occurred. | 268 | | `reason` | A description of the error that occurred. | 269 | | `metadata` | Any custom metadata that might be included. **Only** available on a non-production environment. | 270 | 271 | #### Vapor 1 272 | 273 | | Key | Description | 274 | | ---------- | ---------------------------------------- | 275 | | `code` | The HTTP code. | 276 | | `error` | A boolean confirming an error occurred. | 277 | | `message` | A description of the error that occurred. | 278 | | `metadata` | Any custom metadata that might be included. **Only** available on a non-production environment. | 279 | 280 | ### Examples 281 | 282 | Just to round it all off, here’s a few examples of how our response will return depending on whether you’re about to return a single item, a collection or a paginated result set. 283 | 284 | #### A single item 285 | 286 | ``` 287 | { 288 | "data": { 289 | "id": 1, 290 | "name": "Shane Berry", 291 | "email": "shane@berry.com" 292 | "created_at": "2015-03-02T12:59:02+0100", 293 | "updated_at": "2015-03-04T15:50:40+0100" 294 | } 295 | } 296 | ``` 297 | 298 | #### An endpoint without meaningful data to return 299 | 300 | ``` 301 | { 302 | "status": "ok" 303 | } 304 | ``` 305 | 306 | #### An error in Vapor 2 or 3 307 | 308 | ``` 309 | { 310 | "error": true, 311 | "reason": "Invalid email or password" 312 | } 313 | ``` 314 | 315 | #### A collection of items 316 | 317 | ``` 318 | { 319 | "data": [ 320 | { 321 | "id": 1, 322 | "name": "Shane Berry", 323 | "email": "shane@berry.com" 324 | "created_at": "2015-03-02T12:59:02+0100", 325 | "updated_at": "2015-03-04T15:50:40+0100" 326 | }, 327 | { 328 | "id": 2, 329 | "name": "Albert Henderson", 330 | "email": "albert@henderson.com" 331 | "created_at": "2015-03-02T12:59:02+0100", 332 | "updated_at": "2015-03-04T15:50:40+0100" 333 | }, 334 | { 335 | "id": 3, 336 | "name": "Miguel Phillips", 337 | "email": "miguel@phillips.com" 338 | "created_at": "2015-03-02T12:59:02+0100", 339 | "updated_at": "2015-03-04T15:50:40+0100" 340 | } 341 | ] 342 | } 343 | ``` 344 | 345 | #### A paginated collection of items 346 | ``` 347 | { 348 | "data": [ 349 | { 350 | "id": 1, 351 | "name": "Shane Berry", 352 | "email": "shane@berry.com" 353 | "created_at": "2015-03-02T12:59:02+0100", 354 | "updated_at": "2015-03-04T15:50:40+0100" 355 | }, 356 | { 357 | "id": 4, 358 | "name": "Albert Henderson", 359 | "email": "albert@henderson.com" 360 | "created_at": "2015-03-02T12:59:02+0100", 361 | "updated_at": "2015-03-04T15:50:40+0100" 362 | } 363 | ], 364 | "meta": { 365 | "pagination": { 366 | "currentPage": 2, 367 | "links": { 368 | "next": "/api/users/?page=3&count=20", 369 | "previous": "/api/users/?page=1&count=20" 370 | }, 371 | "perPage": 20, 372 | "total": 258, 373 | "totalPages": 13 374 | } 375 | } 376 | } 377 | ``` 378 | --------------------------------------------------------------------------------