Last active
December 10, 2021 14:51
-
-
Save vzsg/2c964201695a2d2a2f6e5576471b5495 to your computer and use it in GitHub Desktop.
Dynamic Query Support for Vapor 3
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 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? | |
} |
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 | |
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 | |
} | |
} | |
} |
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 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) | |
} | |
} |
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 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) | |
} | |
} |
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 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 { } |
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 | |
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) | |
] | |
} | |
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 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