Vapor 4 的 CRUDKit

不再积极维护 (见 #9)

我们一直在编写 CRUD (创建-读取-更新-删除) 路由。这个包的目的是减少重复的代码,并为 API 提供一个快速的开始。

贡献

这个项目是开放贡献的。欢迎克隆、Fork 或提交 PR。非常欢迎您的帮助!

安装

将此包添加到您的 Package.swift 作为依赖项,并添加到您的 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")
    ])
]

基本用法

让你的模型遵循 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 { }

routes.swift 中注册路由

app.crud("todos", model: Todo.self)

这将注册基本的 CRUD 路由

POST /todos             # create todo
GET /todos              # get all todos
GET /todos/:todos       # get todo
PUT /todos/:todos       # replace todo
DELETE /todos/:todos    # delete todo

请注意! 端点名称 (例如 "todos") 也将被用作命名 ID 参数的名称。这是为了避免在有多个参数时出现重复。

附加功能

自定义公共实例

您可以返回一个自定义结构作为公共实例,该实例将从所有 CRUD 路由中返回。

extension Todo: CRUDModel {
    struct Public: Content {
        var title: String
        var done: Bool
    }
    
    var `public`: Public {
        Public.init(title: title, done: done)
    }
}

该计算属性稍后将被转换为 EventLoopFuture<Public>。如果需要运行异步代码来创建公共实例 (例如加载关系),可以自定义该转换。虽然您可以在那里访问数据库,但应该使用它来执行任何业务逻辑。

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
}

自定义创建 / 替换

您可以在创建/替换时添加特定的逻辑。如果您的创建/替换请求应该采用模型属性的子集,或者如果您需要在创建/替换时执行特殊操作,这将特别有用。

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 支持

您可以通过遵循 Patchable 协议,为您的模型添加 patch 支持。

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

验证

要添加自动验证,您只需要让您的模型(或您的自定义结构)遵循 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...))
    }
}

自定义路由

实验性 您可以通过闭包将您自己的子路由添加到 .crud() 中。

// routes.swift
app.crud("todos", model: Todo.self) { routes, _ in
    // GET /todos/:todos/hello 
    routes.get("hello") { _ in "Hello World" }
}

关系支持

实验性 目前仅支持 Children 关系。 请参见下面的示例...

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

这将为标签注册 CRUD 路由

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 关系支持所有功能 (公共实例、自定义创建/替换、patch 支持、验证)。

关于父 ID 在 payload 中的说明

目前 Vapor 要求将父 ID 添加到创建/替换请求中。

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

这需要像这样的创建 payload

{
    title: "Foo",
    todo {
        id: 1 
    }
}

您可以使用自定义的创建/替换结构来避免这种情况。此包将负责并为您填写正确的 ID。

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

然后,您可以创建一个没有父 ID 在 payload 中的子项。

{
    title: "Foo"
}