We all write CRUD (Create-Read-Update-Delete) routes all the time. The intention of this package is to reduce repeating code and to provide a fast start for an API.
This project is open for contributions. Feel free to clone, fork or make a PR. Help is very welcome!
Add this package to your Package.swift
as dependency and to your target.
dependencies: [
.package(url: "https://github.com/simonedelmann/crud-kit.git", from: "1.1.0")
],
targets: [
.target(name: "App", dependencies: [
.product(name: "CRUDKit", package: "crud-kit")
])
]
import CRUDKit
final class Todo: Model, Content {
@ID()
var id: UUID?
@Field(key: "title")
var title: String
@Field(key: "done")
var done: Bool
// ...
}
extension Todo: CRUDModel { }
app.crud("todos", model: Todo.self)
This will register basic CRUD routes:
POST /todos # create todo
GET /todos # get all todos
GET /todos/:todos # get todo
PUT /todos/:todos # replace todo
DELETE /todos/:todos # delete todo
Please note! The endpoints name (e.g. "todos") will be used as name for the named id parameter too. This is for avoiding duplications when having multiple parameters.
You can return a custom struct as public instance, which will be returned from all CRUD routes then.
extension Todo: CRUDModel {
struct Public: Content {
var title: String
var done: Bool
}
var `public`: Public {
Public.init(title: title, done: done)
}
}
That computed property will be converted to an EventLoopFuture<Public>
afterwards. If you need to run asynchronous code to create your public instance (e.g. loading relationships), you can customize that conversion. Although you will have access to the database there, this should not be used to do any business logic.
extension Todo: CRUDModel {
// ...
// This is the default implementation
func `public`(eventLoop: EventLoop, db: Database) -> EventLoopFuture<Public> {
eventLoop.makeSucceededFuture(self.public)
}
// You can find an example for loading relationship in /Tests/CRUDKitTests/Models/Todo.swift
}
You can add specific logic while create / replace. This is especially helpful, if your create / replace request should take a subset of the models properties or if you need to do special stuff while creating / replacing.
extension Todo: CRUDModel {
struct Create: Content {
var title: String
}
convenience init(from data: Create) {
// Call model initializer with default value for done
Todo.init(title: data.title, done: false)
// Do custom stuff (e.g. hashing passwords)
}
struct Replace: Content {
var title: String
}
func replace(with data: Replace) -> Self {
// Replace all properties manually
self.title = data.title
// Again you can add custom stuff here
// Return self
return self
// You can also return a new instance of your model, the id will be preserved.
}
}
You can add patch support to your model by confirming to Patchable
.
PATCH /todos/:todos # patch todo
extension Todo: Patchable {
struct Patch: Content {
var title: String?
var done: Bool?
}
func patch(with data: Patch) {
if let title = data.title {
self.title = title
}
// Shorter syntax
self.done = data.done ?? self.done
}
}
To add automatic validation, you just need to conform your model (or your custom structs) to Validatable
.
extension Todo: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("title", as: String.self, is: .count(3...))
}
}
// Using custom structs
extension Todo.Create: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("title", as: String.self, is: .count(3...))
}
}
extension Todo.Replace: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("title", as: String.self, is: .count(3...))
}
}
extension Todo.Patch: Validatable {
static func validations(_ validations: inout Validations) {
validations.add("title", as: String.self, is: .count(3...))
}
}
Experimental You can add your own child routes via a closure to .crud()
.
// routes.swift
app.crud("todos", model: Todo.self) { routes, _ in
// GET /todos/:todos/hello
routes.get("hello") { _ in "Hello World" }
}
Experimental Currently only Children relations are supported. See example below...
// Todo -> Tag
final class Todo: Model, Content {
@Children(for: \.todo)
var tags: [Tag]
// ...
}
final class Tag: Model, Content {
@Parent(key: "todo_id")
var todo: Todo
// ...
}
extension Todo: CRUDModel { }
extension Tag: CRUDModel { }
// routes.swift
app.crud("todos", model: Todo.self) { routes, parentController in
routes.crud("tags", children: Tag.self, on: parentController, via: \.$tags)
}
This will register CRUD routes for tags:
POST /todos/:todos/tags # create tag
GET /todos/:todos/tags # get all tags
GET /todos/:todos/tags/:tags # get tag
PUT /todos/:todos/tags/:tags # replace tag
PATCH /todos/:todos/tags/:tags # patch tag (if Tag conforms to Patchable)
DELETE /todos/:todos/tags/:tags # delete tag
Children relations support all features (public instances, custom create/replace, patch support, validations).
Currently Vapor does require to add the parent's id into a create / replace request.
final class Tag: Model, Content {
// ...
@Parent(key: "todo_id")
var todo: Todo
init(id: Tag.IDValue? = nil, title: String, todo_id: Todo.IDValue) {
// ...
self.$todo.id = todo_id
}
}
extension Tag: CRUDModel { }
This requires a create payload like this:
{
title: "Foo",
todo {
id: 1
}
}
You can avoid that using a custom create / replace struct. This package will take care and fill the correct id for you.
final class Tag: Model, Content {
// ...
@Parent(key: "todo_id")
var todo: Todo
// Make todo_id parameter optional
init(id: Tag.IDValue? = nil, title: String, todo_id: Todo.IDValue?) {
// ...
// Use if let for unwrapping the optional
if let todo = todo_id {
self.$todo.id = todo
}
}
}
extension Tag: CRUDModel {
struct Create: Content {
var title: String
var todo_id: Todo.IDValue?
}
convenience init(from data: Create) throws {
self.init(title: data.title, todo_id: data.todo_id)
}
struct Replace: Content {
var title: String
var todo_id: Todo.IDValue?
}
func replace(with data: Replace) throws -> Self {
Self.init(title: data.title, todo_id: data.todo_id)
}
}
Then you can create a child without parent id within payload.
{
title: "Foo"
}