Tuesday, May 26, 2020
Creating a Database Enum in Vapor 4
Vapor has essentially become the web framework for the Swift programming language so I’ve been doing quite a bit of experimenting with it. Unfortunately, the documentation for the latest version is still rather incomplete as of this writing. As such, I thought I would add to the general body of knowledge about Vapor available on the web.
For a project I’ve started working on, I wanted to add an enumerated type (more commonly known simply as an ENUM
) to my PostgreSQL database, use that ENUM
as the type on a column on one of my tables, and have a default value. The first thing, then, was to define the ENUM
itself in Swift:
enum BillingStatus: String, Codable {
case current
case pastDue
}
Then I could define a model that uses the ENUM
:
final class Account: Model {
static let schema = "accounts"
@ID(key: .id)
var id: UUID?
@Enum(key: "billing_status")
var billingStatus: BillingStatus
init() {}
init(
id: IDValue? = nil,
billingStatus: BillingStatus = .current
) {
self.id = id
self.billingStatus = status
}
}
Lastly, I needed to define the migration code. This is a little more tricky that you might at first suppose for two reasons: (1) the ENUM
type must be created before it can be used as a column type on the table, and (2) Vapor 4’s migrations are asynchronous, so extra steps are needed to ensure a consistent order of operations. To handle this situation, I used flatMap
to provide a callback block that will execute as soon as the ENUM
definition block has finished. The inverse must be also done in the revert
method.
import Fluent
struct CreateAccount: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
return database.enum("billing_status")
.case("current")
.case("past_due")
.create()
.flatMap { billing_status in
return database.schema("accounts")
.id()
.field(
"billing_status",
billing_status,
.required,
.custom("DEFAULT 'current'")
)
.create()
}
}
func revert(on database: Database) -> EventLoopFuture<Void> {
return database.schema("accounts").delete().flatMap {
return database.enum("billing_status").delete()
}
}
}
I used Vapor’s .custom
method to specify that current
is the default, mirroring the default used in the model code’s init
method. You may well feel that specifying a default in both the database and the application layers is redundant but I prefer to keep the two in sync where feasible.
All code tested with Swift 5.2, Vapor 4.5, and Fluent 4.0.0-rc2.2 on both macOS 10.15 and Ubuntu Linux 18.04.