Last active
September 13, 2020 21:12
-
-
Save maximkrouk/7dccc660f917e634b3b6cfea006e5cee to your computer and use it in GitHub Desktop.
Generic CRUD Controller for Vapor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Depends on `Builder`: https://gist.github.com/maximkrouk/eede7171952e044492c1fa57291bcf94 | |
// Depends on Model.swift | |
import Vapor | |
import Fluent | |
enum GenericController<Model: APIModel> | |
where Model.IDValue: LosslessStringConvertible { | |
/// ID parameter key | |
@inlinable | |
static var idKey: String { "id" } | |
/// ID path component | |
@inlinable | |
static var idPath: PathComponent { .init(stringLiteral: ":\(idKey)") } | |
/// Schema path component | |
@inlinable | |
static var schemaPath: PathComponent { .init(stringLiteral: Model.schema) } | |
/// Extracts id parameter from a database | |
@inlinable | |
static func getID(_ req: Request) throws -> Model.IDValue { | |
guard let id = req.parameters.get(idKey, as: Model.IDValue.self) else { | |
throw Abort(.badRequest) | |
} | |
return id | |
} | |
/// Creates a new l managed objects in a database | |
@inlinable | |
static func _findByID(_ req: Request) throws -> EventLoopFuture<Model> { | |
Model.find(try getID(req), on: req.db).unwrap(or: Abort(.notFound)) | |
} | |
/// Creates a new l managed objects in a database | |
@inlinable | |
static func _create(_ req: Request) throws -> EventLoopFuture<Model.Output> { | |
let request = try req.content.decode(Model.Input.self) | |
let model = try Model(request) | |
return model.save(on: req.db) | |
.flatMap { model.load(on: req.db) } | |
.unwrap(or: Abort(.notFound)) | |
} | |
/// Reads all managed objects' output from a database | |
@inlinable | |
static func _readAll(_ req: Request) throws -> EventLoopFuture<Page<Model.Output>> { | |
Model.eagerLoadedQuery(on: req.db).paginate(for: req).map { $0.map(\.output) } | |
} | |
/// Reads specified managed object' output from a database | |
@inlinable | |
static func _readByID(_ req: Request) throws -> EventLoopFuture<Model.Output> { | |
Model.load(try getID(req), on: req.db).unwrap(or: Abort(.notFound)) | |
} | |
/// Updates specified managed object in a database | |
@inlinable | |
static func _updateByID(_ req: Request) throws -> EventLoopFuture<Model.Output> { | |
let content = try req.content.decode(Model.Update.self) | |
return try _findByID(req) | |
.flatMapThrowing { model -> Model in | |
try modification(of: model) { try $0.update(content) } | |
} | |
.flatMap { $0.update(on: req.db).transform(to: $0) } | |
.flatMap { Model.load($0.id, on: req.db) } | |
.unwrap(or: Abort(.notFound)) | |
} | |
/// Deletes managed object from a database | |
@inlinable | |
static func _deleteByID(_ req: Request) throws -> EventLoopFuture<HTTPStatus> { | |
try _findByID(req).flatMap { $0.delete(on: req.db) }.transform(to: .ok) | |
} | |
/// Makes initial routes setup | |
/// | |
/// Uses default methods (without auth stuff) to setup routes: | |
/// ``` | |
/// ┌––––––––––┬–––––––––––––┐ | |
/// | GET | /schema | | |
/// ├––––––––––┼–––––––––––––┤ | |
/// | POST | /schema | | |
/// ├––––––––––┼–––––––––––––┤ | |
/// | GET | /schema/:id | | |
/// ├––––––––––┼–––––––––––––┤ | |
/// | PUT | /schema/:id | | |
/// ├––––––––––┼–––––––––––––┤ | |
/// | DELETE | /schema/:id | | |
/// └––––––––––┴–––––––––––––┘ | |
/// ``` | |
@inlinable @discardableResult | |
static func _setupRoutes(_ builder: RoutesBuilder) -> RoutesBuilder { | |
return Builder(builder.grouped(schemaPath)) | |
.set { $0.on(.GET, use: _readAll) } | |
.set { $0.on(.POST, use: _create) } | |
.set { $0.grouped(idPath) } | |
.set { $0.on(.GET, use: _readByID) } | |
.set { $0.on(.PUT, use: _updateByID) } | |
.set { $0.on(.DELETE, use: _deleteByID) } | |
.build() | |
} | |
} | |
// MARK: - Secure routing | |
extension GenericController { | |
/// Protects the route | |
@inlinable | |
static func protected<Requirement: Authenticatable, Response>( | |
_ req: Request, | |
using auth: Requirement.Type, | |
handler: @escaping (Request) throws -> Response | |
) throws -> Response { try protected(using: auth, handler: handler)(req) } | |
/// Protects the route | |
@inlinable | |
static func protected<Requirement: Authenticatable, Response>( | |
_ req: Request, | |
using auth: Requirement.Type, | |
handler: @escaping (Request, Requirement) throws -> Response | |
) throws -> Response { try protected(using: auth, handler: handler)(req) } | |
/// Protects the route | |
@inlinable | |
static func protected<Requirement: Authenticatable, Response>( | |
using auth: Requirement.Type, | |
handler: @escaping (Request) throws -> Response | |
) -> (Request) throws -> Response { | |
protected(using: auth) { request, _ in try handler(request) } | |
} | |
/// Protects the route | |
@inlinable | |
static func protected<Requirement: Authenticatable, Response>( | |
using auth: Requirement.Type, | |
handler: @escaping (Request, Requirement) throws -> Response | |
) -> (Request) throws -> Response { | |
{ request in | |
return try handler(request, try request.auth.require(auth)) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Fluent | |
import Vapor | |
// MARK: - CRUDModel | |
protocol CRUDModel: Model { | |
associatedtype Input: Content | |
associatedtype Output: Content | |
associatedtype Update: Content | |
init(_ input: Input) throws | |
var output: Output { get } | |
func update(_ update: Update) throws | |
} | |
extension CRUDModel { | |
/// Used for updating specific values in `update` method | |
@inlinable | |
func update<Value>(_ keyPath: WritableKeyPath<Self, Value>, using optional: Value?) { | |
var _self = self | |
if let value = optional { _self[keyPath: keyPath] = value } | |
} | |
} | |
// MARK: - EagerLoadProvidingModel | |
protocol EagerLoadProvidingModel: Model { | |
/// Sets up query to load child properties | |
/// | |
/// Does not modify query by default, should be implemented to load specific fields | |
/// | |
/// Implementation example: | |
/// ``` | |
/// static func eagerLoad(to builder: QueryBuilder<Self>) -> QueryBuilder<Self> { | |
/// builder.with(\.$field) | |
/// } | |
/// ``` | |
/// | |
/// Use conveniecne function for access this method | |
/// Usage example: | |
/// ``` | |
/// eagerLoadedQuery(for: SomeAPIModel.self, on: req.db) // QueryBuilder<SomeAPIModel> | |
/// ``` | |
static func eagerLoad(to builder: QueryBuilder<Self>) -> QueryBuilder<Self> | |
} | |
extension EagerLoadProvidingModel { | |
@inlinable | |
static func eagerLoad(to builder: QueryBuilder<Self>) -> QueryBuilder<Self> { builder } | |
} | |
// MARK: - APIModel | |
protocol APIModel: CRUDModel, EagerLoadProvidingModel {} | |
extension APIModel { | |
@inlinable | |
static func eagerLoadedQuery(on database: Database) -> QueryBuilder<Self> { | |
_eagerLoadedQuery(for: self, on: database) | |
} | |
/// Loads eager loaded instance from the database | |
/// | |
/// Should not be reimplemented | |
@inlinable | |
func load(on database: Database) -> EventLoopFuture<Output?> { | |
Self.load(id, on: database) | |
} | |
/// Loads eager loaded instance from the database | |
/// | |
/// Should not be reimplemented | |
@inlinable | |
static func load(_ id: IDValue?, on database: Database) -> EventLoopFuture<Output?> { | |
_load(self, id, on: database) | |
} | |
} | |
private func _eagerLoadedQuery<Model: APIModel>(for type: Model.Type, on database: Database) -> QueryBuilder<Model> { | |
type.eagerLoad(to: database.query(type)) | |
} | |
private func _load<Model: APIModel>(_ type: Model.Type, _ id: Model.IDValue?, on database: Database) | |
-> EventLoopFuture<Model.Output?> { | |
guard let id = id else { return database.eventLoop.makeSucceededFuture(nil) } | |
return _eagerLoadedQuery(for: type, on: database) | |
.filter(\._$id == id).first() | |
.map { $0?.output } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
User controller example
Todos controller example
JWT-protected routes setup
Back to index