How To: REST API in Swift with Vapor RestKit — ORM Extensions
#Server Side Swift, #Swift, #Software Engineering
One of the things I worked on some time ago was Vapor Rest Kit
It started as a set of utils I implemented to speed up the development of my side project.
Gradually my Vapor utils grew rather large and decided to move it to a separate package and publish.
Features
Currently, the feature set has grown so large that it's probably worth breaking down into separate smaller libs🤔
One of the very first features was Fluent Model Extensions allowing to find answers for the following questions.
How to stop suffering from string literals in Fluent Models' Fields
When I first started using Vapor 4 in spring 2020, I was surprised that Fluent Models now require all those handwritten fields keys everywhere: when defining model fields, when adding migrations, etc.
Vapor docs examples were full of string literals that were duplicated everywhere.
That seemed rather error prone and unnatural, so I added a simple helper to define model fields as enum and avoid errors caused by fat fingers.
FieldsProvidingModel
extension User: FieldsProvidingModel {
enum Fields: String, FieldKeyRepresentable {
case username
}
}
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: Fields.username.key)
var username: String?
}
extension User: InitMigratableSchema {
static func prepare(on schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void> {
return schemaBuilder
.id()
.field(Fields.username.key, .string)
.create()
}
}
How to stop suffering from Fluent Models initial migrations boilerplate
Every model in Vapor ORM Fluent requires initial migration that also include some additional boilerplate code. I've added some sugar just to make it faster.
InitMigratableSchema
Easy-breazy stuff for creating initial migrations in three simple steps:
Make your model conform to InitMigratableSchema protocol, by implementing static prepare(...) method (almost as the same as in Vapor Docs)
extension User: InitMigratableSchema {
static func prepare(on schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void> {
return schemaBuilder
.id()
.field(Fields.username.key, .string)
.create()
}
}
Add initial migration for the model in your configuration file:
func configureDatabaseInitialMigrations(_ app: Application) {
app.migrations.add(User.createInitialMigration())
}
That's it!
How to stop suffering from Siblings pivot models boilerplate
Creating siblings relations in Vapor requires extra work. We have to define a model for pivot table and migration for it which is not very convenient.
SiblingRepresentable
I've implemented a helper to turn it into almost one-liner.
Define whatever as SiblingRepresentable (enum for example)
extension Todo {
enum Relations {
enum RelatedTags: SiblingRepresentable {
typealias From = Todo
typealias To = Tag
typealias Through = SiblingModel<Todo, Tag, Self>
}
}
}
Add corresponding property with @Siblings property wrapper
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Siblings(through: Relations.RelatedTags.through, from: \.$from, to: \.$to)
var relatedTags: [Tag]
}
Add initial migration for pivot Fluent model representing the sibling right after related models initial migrations:
private func configureDatabaseInitialMigrations(_ app: Application) {
app.migrations.add(Todo.createInitialMigration())
app.migrations.add(Tag.createInitialMigration())
app.migrations.add(Todo.Relations.RelatedTags.Through.createInitialMigration())
}
Profit! No need to feel pain from pivot models manual creation any more.
Comments