I'd like to put forward an idea I had with the goal of reducing the amount of boilerplate required for schema migrations in Fluent. It came to me when reading the following:
Introduction
A very common pattern I utilize in my code in order to avoid stringly typed errors is the following:
// Model
final class User: Model {
static let schema = "users"
@ID
var id: UUID?
// Previously:
// @Field(key: FieldKeys.name)
// var name: String
@Field(key: FieldKeys.firstName)
var firstName: String
@Field(key: FieldKeys.lastName)
var lastName: String
}
extension User {
enum FieldKeys {
static let id: FieldKey = .id
static let name: FieldKey = "name"
static let firstName: FieldKey = "first_name"
static let lastName: FieldKey = "last_name"
}
}
// Migration
struct UserNameMigration: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.schema)
.deleteField(User.FieldKeys.name)
.field(User.FieldKeys.firstName, .string)
.field(User.FieldKeys.lastName, .string)
.update()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.schema).delete()
}
}
AFAIK, the only reason SchemaBuilder
uses strings over generics + key paths to represent fields is because fields may be deleted at any point, causing future compilation errors.
Proposed Solution
I'd like to propose the introduction of a @DeletedField
property wrapper in order to enable strongly typed SchemaBuilder
migrations, roughly as follows:
// Model
final class User: Model {
static let schema = "users"
@ID
var id: UUID?
@DeletedField(key: "name")
var name: String
@Field(key: "firstName")
var firstName: String
@Field(key: "lastName")
var lastName: String
}
// Migration
struct UserNameMigration: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.self) // is SchemaBuilder<User>
.deleteField(\.$name)
.field(\.$firstName, .string)
.field(\.$lastName, .string)
.update()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.self).delete()
}
}
@DeletedField
would look just like @Field
in its signature, but it'd be a stripped down version of it. It'd have none of the querying functionality that its counterpart has, and it'd only serve the SchemaBuilder
to retrieve the raw field key.
Of course, other @Deleted...
variants would be needed too, e.g. @DeletedOptionalField
, @DeletedParent
, etc... but that's just the cost of implementation.
With this approach, I think the pros in maintainability, readability and safety would outweigh the slight con of needing to keep the deleted properties around in the model. In a real world scenario, these properties could just be grouped away at the developer's discretion. Furthermore, this untidiness also occurs in the original approach of keeping around old FieldKeys
, so it's not exactly a new con.