Episode #365

Refactoring to Protocols

Series: Server-side Swift with Vapor

12 minutes
Published on November 29, 2018

This video is only available to subscribers. Get access to this video and 572 others.

In the last 2 episodes we added some behavior to add automatically managed timestamp fields and some fairly complex logic to set up UUID primary keys the way we want. Now if we want to share those, or make them the default for our models, we currently have to copy & paste. In this episode we will refactor this logic into reusable protocols so that our work can be applied on any model we wish easily.

Creating an Issue Model

Let's create a new model for storing Issues for our project:

final class Issue : Model {

    // A bunch of boilerplate copied from Project

    static var name: String = "issues"

    enum Status : String, PostgreSQLEnum {
        case open
        case closed
        case wontFix
    }


    var id: UUID?
    var createdAt: Date?
    var updatedAt: Date?

    var subject: String
    var body: String
    var status: Status = .open
}

Having to copy & paste a bunch of code to make our models consistent isn't ideal. Let's extract some of this logic so we can reuse it.

Extracting a Protocol For UUID Primary Keys

protocol UUIDModel : Model, PostgreSQLTable where Self.ID == UUID, Self.Database == PostgreSQLDatabase {
    var id: UUID? { get set }
}

extension UUIDModel {
    static var idKey: IDKey { return \.id }
}

Here you can see we're defining our primary key property and also declaring that this uses a Postgres database.

Extracting the UUID migration logic into a helper

Most of the customized logic is actually when we run our migrations. If we extract this into a protocol on any type that implements UUIDModel, we can easily reuse this in other models.

extension SchemaCreator where Model : UUIDModel {
    func uuidPrimaryKey() {
        let pk = PostgreSQLColumnConstraint.primaryKey(default: nil, identifier: nil)
        let defaultUUID = PostgreSQLColumnConstraint.default(.function("uuid_generate_v4"), identifier: nil)
        field(for: \.id, type: .uuid, pk, defaultUUID)
    }
}

Extracting a Protocol For Timestamps

Next let's look at extracting the timestamp logic.

protocol TimestampModel : Model {
    var createdAt: Date? { get set }
    var updatedAt: Date? { get set }
}

extension TimestampModel {
    static var createdAtKey: TimestampKey? { return \.createdAt }
    static var updatedAtKey: TimestampKey? { return \.updatedAt }
}

And again, we can do the same trick above to make the migrations easier:

extension SchemaCreator where Model : TimestampModel {
    func timestampFields() {
        field(for: \.createdAt)
        field(for: \.updatedAt)
    }
}

Cleaning up our models

If we look at Project.swift now, it looks a lot cleaner:

final class Project : UUIDModel, TimestampModel {
    static var name: String = "projects"

    var id: UUID?
    var title: String
    var description: String

    var createdAt: Date?
    var updatedAt: Date?

    init(title: String, description: String) {
        self.title = title
        self.description = description
    }
}

extension Project : Migration {
    static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
        return PostgreSQLDatabase.create(self, on: conn) { builder in

            builder.uuidPrimaryKey()
            builder.field(for: \.title, type: .varchar(500))
            builder.field(for: \.description)            
            builder.timestampFields()

        }
    }
}

We are also set up to use this on any of our models going forward, keeping things consistent.

This episode uses Xcode 10.0, Swift 4.2, Vapor 3.0.8, Fluent postgresql-1.0.0.