Skip to content

Instantly share code, notes, and snippets.

@vzsg
Last active December 10, 2021 14:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vzsg/2c964201695a2d2a2f6e5576471b5495 to your computer and use it in GitHub Desktop.
Save vzsg/2c964201695a2d2a2f6e5576471b5495 to your computer and use it in GitHub Desktop.
Dynamic Query Support for Vapor 3
import Foundation
import Fluent
protocol DynamicQueryable {
static func dynamicMapping<DB: Database, QB: QueryBuilder<DB, Self>>() -> [String: (DynamicFilter, QB) throws -> QB]
}
protocol DynamicSortable {
static var dynamicFieldMapping: [String: FluentProperty] { get }
}
struct DynamicFilter: Decodable {
let filterField: String
let filterOp: DynamicFilterOperator
let filterValue: String
}
enum DynamicFilterOperator: String, Decodable {
case eq
case neq
case lt
case gt
case lte
case gte
}
enum DynamicSortDirection: String, Decodable {
case ascending = "asc"
case descending = "desc"
}
struct DynamicSort: Decodable {
let sortBy: String
let sortOrder: DynamicSortDirection?
}
import Fluent
import Vapor
enum DynamicQueryError<Result>: Error {
case unsupportedField(String)
case invalidOperand(String, field: String)
}
extension QueryBuilder where Result: DynamicQueryable {
func dynamicFilter(_ filter: DynamicFilter) throws -> Self {
return try dynamicFilter(filter, mapping: Result.dynamicMapping())
}
}
private extension QueryBuilder {
func dynamicFilter(_ filter: DynamicFilter, mapping: [String: ((DynamicFilter, Self) throws -> Self)]) throws -> Self {
guard let modifier = mapping[filter.filterField] else {
throw DynamicQueryError<Result>.unsupportedField(filter.filterField)
}
return try modifier(filter, self)
}
}
extension QuerySupporting {
static func dynamicFilterMethod(for `operator`: DynamicFilterOperator) -> Self.QueryFilterMethod {
switch `operator` {
case .eq:
return Self.queryFilterMethodEqual
case .gt:
return Self.queryFilterMethodGreaterThan
case .lt:
return Self.queryFilterMethodLessThan
case .gte:
return Self.queryFilterMethodGreaterThanOrEqual
case .lte:
return Self.queryFilterMethodLessThanOrEqual
case .neq:
return Self.queryFilterMethodNotEqual
}
}
}
import Fluent
import Pagination
import Vapor
extension Paginatable {
static func dynamicSorts(by field: String, direction: DynamicSortDirection) -> [Self.Database.QuerySort] where Self: DynamicSortable {
guard let property = Self.dynamicFieldMapping[field] else {
return Self.defaultPageSorts
}
let qsDirection = direction == .ascending ? Self.Database.querySortDirectionAscending : Self.Database.querySortDirectionDescending
return [Self.Database.querySort(Self.Database.queryField(property), qsDirection)]
}
static func dynamicSorts(_ request: Request) -> [Self.Database.QuerySort] where Self: DynamicSortable {
guard let sort = try? request.query.decode(DynamicSort.self) else {
return Self.defaultPageSorts
}
return Self.dynamicSorts(by: sort.sortBy, direction: sort.sortOrder ?? .ascending)
}
}
import Vapor
import Fluent
extension QueryBuilder where Result: DynamicQueryable {
func dynamicFilter(_ request: Request) throws -> Self {
guard let filter = try? request.query.decode(DynamicFilter.self) else {
return self
}
return try dynamicFilter(filter)
}
}
import FluentSQLite
import Vapor
import Pagination
/// A single entry of a Todo list.
final class Todo: SQLiteModel {
typealias Database = SQLiteDatabase
/// The unique identifier for this `Todo`.
var id: Int?
/// A title describing what this `Todo` entails.
var title: String
/// Creates a new `Todo`.
init(id: Int? = nil, title: String) {
self.id = id
self.title = title
}
}
/// Allows `Todo` to be used as a dynamic migration.
extension Todo: Migration { }
/// Allows `Todo` to be encoded to and decoded from HTTP messages.
extension Todo: Content { }
/// Allows `Todo` to be used as a dynamic parameter in route definitions.
extension Todo: Parameter { }
/// Allows `Todo` to be used in paginated queries.
extension Todo: Paginatable { }
import Fluent
extension Todo: DynamicQueryable, DynamicSortable {
static func dynamicMapping<DB, QB>() -> [String: (DynamicFilter, QB) throws -> QB] where DB : QuerySupporting, QB : QueryBuilder<DB, Todo> {
[
"id": { filter, query in
guard let id = Int(filter.filterValue) else {
throw DynamicQueryError<Todo>.invalidOperand(filter.filterValue, field: "id")
}
return query.filter(\Todo.id, DB.dynamicFilterMethod(for: filter.filterOp), id)
},
"title": { filter, query in
query.filter(\Todo.title, DB.dynamicFilterMethod(for: filter.filterOp), filter.filterValue)
}
]
}
static let dynamicFieldMapping: [String : FluentProperty] = [
"id": FluentProperty.keyPath(\Todo.id),
"title": FluentProperty.keyPath(\Todo.title)
]
}
import Vapor
import FluentSQLite
import Pagination
/// Controls basic CRUD operations on `Todo`s.
final class TodoController {
/// Returns a list of all `Todo`s.
func index(_ req: Request) throws -> Future<Paginated<Todo>> {
return try Todo.query(on: req)
.dynamicFilter(req)
.paginate(for: req, Todo.dynamicSorts(req))
.map { $0.response() }
}
/// Saves a decoded `Todo` to the database.
func create(_ req: Request) throws -> Future<Todo> {
return try req.content.decode(Todo.self).flatMap { todo in
return todo.save(on: req)
}
}
/// Deletes a parameterized `Todo`.
func delete(_ req: Request) throws -> Future<HTTPStatus> {
return try req.parameters.next(Todo.self).flatMap { todo in
return todo.delete(on: req)
}.transform(to: .ok)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment