├── 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 | [](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 | 
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 | 
178 |
179 | But there is an option for doing something simular on android also
180 |
181 | 
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 | 
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 | 
348 |
349 | But there is an option for doing something simular on android also
350 |
351 | 
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 |
--------------------------------------------------------------------------------