simonedelmann/crud-kit
**Not actively maintained** (see https://github.com/simonedelmann/crud-kit/issues/9)
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 todoPlease 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.
```swift
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.
```swift
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.
```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 support
You can add patch support to your model by confirming to `Patchable`.
```
PATCH /todos/:todos # patch todo
```
```swift
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`.
```swift
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()`.
```swift
// routes.swift
app.crud("todos", model: Todo.self) { routes, _ in
// GET /todos/:todos/hello
routes.get("hello") { _ in "Hello World" }
}
```
### Relationship support
**Experimental** Currently only Children relations are supported. See example below...
```swift
// 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.
```swift
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.
```swift
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"
}
```Package Metadata
Repository: simonedelmann/crud-kit
Default branch: master
README: README.md