Skip to content

Latest commit

 

History

History
382 lines (284 loc) · 8.78 KB

README.md

File metadata and controls

382 lines (284 loc) · 8.78 KB

CRUDKit for Vapor 4

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.

Contribution

This project is open for contributions. Feel free to clone, fork or make a PR. Help is very welcome!

Installation

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")
    ])
]

Basic Usage

Conform you model to CRUDModel

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 { }

Registering routes in routes.swift

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.

Additional features

Custom public instance

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
}

Customize create / replace

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.
    }
}

Patch support

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
    }
}

Validations

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...))
    }
}

Custom routes

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" }
}

Relationship support

Experimental

Children

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).

Notes on parent's id within payload

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"
}

Siblings

See example below...

// Planet <-> Tag
final class Planet: Model, Content {
    @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
    var tags: [Tag]
    
    // ...
}

final class Tag: Model, Content {
    @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
    var planets: [Planet]
    
    // ...
}


final class PlanetTag: Model {
    @Parent(key: "planet_id")
    var planet: Planet

    @Parent(key: "tag_id")
    var tag: Tag

    // ...
}


extension Todo: CRUDModel { }
extension Tag: CRUDModel { }

// routes.swift
app.crud("planet", model: Planet.self) { routes, parentController in
    routes.crud("tags", siblings: Tag.self, on: parentController, via: \.$tags)
}

This will register CRUD routes for tags:

GET /planets/:planets/tags              # get all tags
GET /planets/:planets/tags/:tags        # get tag
PUT /planets/:planets/tags              # attach tag
DELETE /planets/:planets/tags           # detach tag

Notes on attaching/detaching models

Take note that the attach and detach calls does not include the id of the tag. But instead, the call has been extended to use the body in order to specify which ids of tags to be attached for planet. You can pass in multiple tag ids in an array instead of just passing one id at a time.

[
    1, 2, 3 
]