Migrating SteamPress To Vapor 2

Written by Tim on Sunday, August 27, 2017

This is a write up of the talk I gave at May's Vapor London Meetup. The slides from that presentation can be found here This post is also published on the SteamPress blog here

Vapor 2

Vapor 2 has now been released! It contains a load of awesome improvements, including massively simplifying some of the most common use cases, huge performance boosts and loads of great new features. I've had a long running vapor2 branch of both SteamPress and the example site since the first betas and this post highlights some of the keys changes between the two versions and how to migrate your project.

Getting Started

The first thing you need to do is make sure you have the latest version of both Swift and the toolbox. Vapor 2 requires Swift 3.1, so it is time for you to upgrade if you are still clinging to Swift 3.0! It is worth the upgrade even just for the stability improvements in SourceKit and LLDB!

You also need to upgrade your Vapor Toolbox to make sure you are on the latest version. Vapor Toolbox is now provided via brew, which should make upgrading and installation a lot easier. To install the new toolbox, remove, the old version, add the Vapor tap and install:

rm -rf /usr/local/bin/vapor
brew tap vapor/homebrew-tap

brew install vapor

Next edit your Package.swift to bring in Vapor 2. Your dependency will now look like:

.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2)

Finally once you have done that, run vapor update and you are ready to get going!

Note: you must make sure that any other dependencies you have are Vapor 2 compatible, otherwise you will either get an unsatisfiable error from Swift Package Manager, or it will get itself stuck in an endless loop.

Vapor Migration Errors

Don't be too disheartened by all of the errors! Most of them are easy to fix and you will be on Vapor 2 in no time.

Configuration

Vapor 2 introduces the concept of a Droplet's configuration, which can be thought of as a staging area for setting up everything about your Droplet. This is where you now add things such as Providers, Middleware, Preparations and any other configurable options. It also means that all of your Droplet's properties are now constant let properties. This means that they can't be changed once the Droplet has been initialised which makes testing your applications and libraries a lot simpler. It also means that you can pass around properties from your Droplet directly rather than passing around the entire droplet. This makes testing and dependency injection much simpler!

So previously you would have done something like:

drop.middleware.append(securityHeaders, at: 0)
drop.middleware.insert(abortMiddleware, at: 1)
try drop.addProvider(SteamPress.Provider.self)

This now changes to:

let config = try Config()
config.addConfigurable(middleware: securityHeaders.builder(), name: "security-headers")
config.addConfigurable(middleware: BlogErrorMiddleware.init, name: "blog-error")

try config.addProvider(SteamPress.Provider.self)
try config.addProvider(LeafProvider.Provider.self)
try config.addProvider(FluentProvider.Provider.self)

let droplet = try Droplet(config)

The order that your middleware is loaded, what commands to run and what items to load for that environment (you can specify different files for different environments) is defined by your droplet.json. For example the SteamPress website looks like:

{
    "server": "engine",
    "client": "engine",
    "console": "terminal",
    "log": "console",
    "hash": "crypto",
    "cipher": "crypto",
    "view": "leaf",
    "middleware": [
        "security-headers",
        "blog-error",
        "date",
        "file",
        "steampress-sessions",
        "blog-persist"
    ],
    "commands": [
        "prepare"
    ]
}

This file tells the Configuration to use those 6 middlewares and to load them in that order. So the Vapor Security Headers middleware is always the first one so that security headers get added to every single request, regardless of whether they fail or not.

Note that in order for you to add your Middleware it either needs to conform to ConfigInitializable or you need to give it a closure to return the middleware when asked, like how the security headers do it:

public func builder() -> ((Config) throws -> SecurityHeaders) {
    return { _ in
        return self.build()
    }
}

Fluent Models

Fluent models are mostly similar, but the responsibilities and ways they interact with the database have been changed, for the better. To start with, each model now requires a Storage property. This takes over from from your model's id and the exists property that was in the weird state of being deprecated but required in order to make things work properly. It also take over all responsibility for setting the ID in the database and extracting it from the database, so that is one less thing to worry about.

Row

Node is no longer used for getting information from and saving information to the database. This now falls over to the Row object, which works in a similar way but now provides a clear distinction between when you are interacting with the database and when you are interacting with Leaf or other instances that require a Node. This helps, for instance, in ensuring that your user's password is never passed to Node object and kept inside the Row object so only the database and model have access to it.

The initialiser for your model's now takes a Row instead of a Node but otherwise looks fairly similar. Your Vapor 1 initialiser will look something like:

required public init(node: Node, in context: Context) throws {
    id = try node.extract(“id")
    title = try node.extract(“title")
    contents = try node.extract("contents")
    author = try node.extract("bloguser_id")
    slugUrl = try node.extract("slug_url")
    published = try node.extract("published")
    let createdTime: Double = try node.extract("created")
    let lastEditedTime: Double? = try? node.extract("last_edited")

    created = Date(timeIntervalSince1970: createdTime)

    if let lastEditedTime = lastEditedTime {
        lastEdited = Date(timeIntervalSince1970: lastEditedTime)
    }
}

In Vapor 2, this will now look like:

public required init(row: Row) throws {
    title = try row.get(Properties.title)
    contents = try row.get(Properties.contents)
    author = try row.get(BlogUser.foreignIdKey)
    slugUrl = try row.get(Properties.slugUrl)
    published = try row.get(Properties.published)
    let createdTime: Double = try row.get(Properties.created)
    let lastEditedTime: Double? = try? row.get(Properties.lastEdited)

    created = Date(timeIntervalSince1970: createdTime)

    if let lastEditedTime = lastEditedTime {
        lastEdited = Date(timeIntervalSince1970: lastEditedTime)
    }
}

Relations

Relations have been simplified in Vapor 2 as well. Notice that in this initialiser above, the author property can be be extracted used the foreignIdKey of the parent object - no more trying to guess what Fluent has set the relation ID to be!

The way of extracting relations has been made much easier as well, leaning heavily on Swift's generics. Previously you would get your relations like:

func getAuthor() throws -> BlogUser? {
    return try parent(author, "bloguser_id", BlogUser.self).get()
}

func tags() throws -> [BlogTag] {
    return try siblings().all()
}

In Vapor 2 this has been changed so it is now:

var postAuthor: Parent<BlogPost, BlogUser> {
    return parent(id: author)
}

var tags: Siblings<BlogPost, BlogTag, Pivot<BlogPost, BlogTag>> {
    return siblings()
}

(These aren't quite directly comparable as I don't return the actual entity or an array of entities any more to make working with the new inbuilt Pagination easier, but you just need to add the .get() or .all() functions on.)

Notice how much simpler the parent() call is - you don't need to guess the column name in the database or provide the type! Less 'stringly-typed' improvements FTW!

Authentication

Like a lot of Vapor 2, Authentication have been pulled out of Vapor and is now a separate package. This is great for sites that don't need Authentication - less files to compile, a smaller binary and quicker compile time! If you do want authentication in your site, just add the auth-provider as a dependency.

The package contains lots of different Authenticable types for different use cases, such as Password, Token etc. SteamPress uses the PasswordAuthenticable to allow users to log in to the site, so that is what we are going to look at today, though the concepts are mostly transferable across the different types.

The big news in Vapor 2 is that authenticate() is now implemented for you! This means no more switching between different session identifiers to see if it matches with a cookie, or token etc. There are however two things that you do need to implement if you want to make your user Model work with PasswordAuthenticable:

  • hashedPassword - this returns the hashed password so Vapor can compare it with the PasswordVerifier
  • passwordVerifier - this verifies your hashed password against the one provided and the only thing you should use for this is BCryptHasher. It automatically conforms so just use it, don't scrimp on security. It is so easy to use so there is no excuse for storing passwords without a strong hashing mechanism and salt. Anyway, I digress, that's probably a topic for another blog post!

For the Blog's authors, I just make the BlogUser conform to PasswordAuthenticable and implement the two properties above like so:


extension BlogUser: PasswordAuthenticable {
    public static let usernameKey = Properties.username
    public static let passwordVerifier: PasswordVerifier? = BlogUser.passwordHasher
    public var hashedPassword: String? {
        return password.makeString()
    }
    public static let passwordHasher = BCryptHasher(cost: 10)
}

I set the usernameKey to my column name; by default it uses email so just change it if your column name is different. You will notice that I also add a passwordHasher property here, that is just a convenience property so I have a central place to call a method to hash my passwords rather than having to new up a BCryptHasher every time I want to create a new password hash and make sure they have the same cost. It is also a single line I need to change when I want to increase the cost of the hash in the future when computers get quicker, rather than trying to find every instance across my code.

You will also notice that I save the password on my model as Bytes rather than a string - this is just because the BCryptHasher returns the hash as Bytes and also stops me from being an idiot and trying to save the password as plaintext.

SessionPersistable

The Authenticable stuff is great for authenticating your user, but you obviously don't want to have to send the username and password with every request as it would be either extremely annoying for the user or be impossible to implement securely. Luckily there is SessionPersistable to make this easy for you. If your model conforms to PasswordAuthenticable you just have to add the conformance:

extension BlogUser: SessionPersistable {}

This will take care of setting a cookie when you login your users and then checking the cookie in subsequent requests. You don't have to do anything else!

Logging In

Logging in a user is fairly similar to before and now looks like:

guard let username = request.data["username"]?.string, let password = request.data["password"]?.string else {
    throw Abort.badRequest
}

let passwordCredentials = Password(username: username, password: password)

do {
    let user = try BlogUser.authenticate(passwordCredentials)
    request.auth.authenticate(user)
    return Response(redirect: pathCreator.createPath(for: "admin"))
} catch {
    let loginError = ["Your username or password was incorrect"]
    return try viewFactory.createLoginView(loginWarning: false, errors: loginError, username: username, password: "")
}

One thing to note is that the Credentials use has been simplified, so you now longer have to pass it every single property on your user or hack in a work around. Other than that, it is fairly similar. Once you have your username and password and your PasswordCredentials you call authenticate on your model. This will throw if it fails, so catch and handle the error. Otherwise you can call request.auth.authenticate(user) and this will log your user in, sets up the session and make sure the cookie gets set.

Registration

Registration is no longer required, you just need to save your user in the database as normal! If you want to log them in at the same time, just follow the steps above.

Routing

Routing is probably the biggest change in Vapor 2 conceptually and was probably the most difficult thing to get your head around! The major news for Vapor 2 is that there are no more restrictions on parameters! Previously you could only nest parameters up to three deep due to Swift's generics and strict type safety. Any more than that and the compiler would take too long to be compile your app (think hours to days...). So in Vapor 2 there is a new Parameterizable protocol and a new way to extract parameters from requests.

Parameterizable

Anything that conforms to Entity can automatically conform the Parameterizable without any extra work! All you need to do is to add the conformance:

extension BlogUser: Parameterizable {}

If you have a custom type that you want to make conform, you just need to implement the functions:

extension MyCustomType: Parameterizable {
    static var uniqueSlug: String = "mycustomtype"

    static func make(for parameter: String) throws -> MyCustomType {
        guard let customTypeObject = try MyCustomType(id: parameter) else {
            throw RouterError.invalidParameter
        }
        return customTypeObject
    }
}

Extracting Parameters

In order to give the compiler a hand, parameters must now be extracted from the request and are no longer passed in as parameters to the request handler. So if I wanted to send a request to https://steampress.io/users/1/, the handler for that would be:

drop.get("users", BlogUser.parameter) { request in
    let user = try request.parameters.next(BlogUser.self)
    ...
}

You can have as many request.parameter.next() calls as you want in your handler, including of the same types.

Leaf and Views

Leaf has not changed much at all, although the inbuilt tags have improved somewhat (the #for() loop now provides an index for example). But it requires a special mention as most people will want to provide a front end at some point and there are a couple of things that may trip people up. Like a lot of thing, Leaf is no longer built in and so to use it you need to:

  • add the provider to your Package.swift
  • add the provider to your Config
  • Add the view to your droplet.json to tell it to use Leaf when rendering views:
{
    ...
    "view": "leaf",
    ...
}

Testing

Due to all of the Configurations, testing becomes a lot simpler in Vapor 2. As everything is defined in your Config and set up when you instantiate your Droplet you don't need to try and create a testable droplet, manually run your preparations. You can set everything up in the same way as you do your normal code, including Providers, Middleware and routing. The only thing you need to make sure is that you don't call try drop.run() as that will start is listening on the port and block your tests! However, in your tests, calling routes on your Droplet will still work and it will respond as it does when running as an application.

Tidbits

The last couple of things to mention are:

  • makeNode() must now take a Context. For examples on how to use this look at the BlogPost.swift file
  • A final reminder that lots of things have been put into separate packages, including Fluent, Validation etc. If something existed in Vapor 1 but you can no longer see if, you probably just need to add the dependency.

Next Steps

The SteamPress site (https://steampress.io) is actually running on Vapor 2! You can see the two PRs that made this happen at:

The new documentation site can be found at https://docs.vapor.codes/2.0/. If you have any question go to the #help channel on Slack or post a comment below!

Tim