Hi Everybody,
during my Endeavour of using a JSONB-column using Fluent I encountered the following crash of my Server:
[ INFO ] POST /MapEditorBackend
[ INFO ] query create geo_jsons input=[[description: "Kampnagel Tabellen Test 4", id: 13C33818-DC92-4ECE-8A2D-919EC0FCBF2F]]
[ INFO ] query create geo_json_payloads input=[[geojson_id: 13C33818-DC92-4ECE-8A2D-919EC0FCBF2F, json: Optional(AnyCodable([:])), id: 2B91354A-6D91-4E0D-B994-7E8D620B24AA]]
[ INFO ] GET /MapEditorBackend
[ INFO ] query read geo_jsons
[ INFO ] DELETE /MapEditorBackend/13C33818-DC92-4ECE-8A2D-919EC0FCBF2F
[ INFO ] query read geo_jsons filters=[geo_jsons[id] = 13C33818-DC92-4ECE-8A2D-919EC0FCBF2F] limits=[count(1)]
[ INFO ] query read geo_json_payloads filters=[geo_json_payloads[geojson_id] ~~ [13C33818-DC92-4ECE-8A2D-919EC0FCBF2F]]
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/lars/Documents/Projects/MapEditor/MapEditorBackend/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresDataDecoder.swift, line 143
being unsure whether this is a bug in PostgresDataDecoder
or my code I want to show you what I did.
my model:
import Fluent
import Vapor
final class GeoJSON: Model, Content {
static let schema = "geo_jsons"
@ID(key: .id)
var id: UUID?
@Field(key: "description")
var description: String
@Children(for: \.$geoJSON)
var geoJSONPayloads: [GeoJSONPayload]
init() { }
init(id: UUID? = nil, description: String) {
self.id = id
self.description = description
}
}
import Fluent
import Vapor
final class GeoJSONPayload: Model, Content {
static let schema = "geo_json_payloads"
@ID(key: .id)
var id: UUID?
@Field(key: "json")
var json: AnyCodable?
@Parent(key: "geojson_id")
var geoJSON: GeoJSON
init() { }
init(id: UUID? = nil, json: AnyCodable, geoJSONID: UUID) {
self.id = id
self.json = json
self.$geoJSON.id = geoJSONID
}
}
my migrations:
import Fluent
struct CreateGeoJSON: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
return database.schema("geo_jsons")
.id()
.field("description", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
return database.schema("geo_jsons").delete()
}
}
import Fluent
struct CreateGeoJSONPayload: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
return database.schema("geo_json_payloads")
.id()
.field("json", .json)
.field("geojson_id", .uuid, .references("geo_jsons", "id"))
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
return database.schema("geo_json_payloads").delete()
}
}
plus the code from AnyCodable/Sources/AnyCodable at master · Flight-School/AnyCodable · GitHub
my controller:
import Fluent
import Vapor
struct GeoJSONController {
func index(req: Request) throws -> EventLoopFuture<[GeoJSON]> {
return GeoJSON.query(on: req.db).all()
}
func create(req: Request) throws -> EventLoopFuture<GeoJSON> {
let geoJSON = try req.content.decode(GeoJSON.self)
return geoJSON.save(on: req.db).flatMap {
let geoJSONPayload = GeoJSONPayload(json: AnyCodable(), geoJSONID: geoJSON.id!)
return geoJSONPayload.save(on: req.db)
}.map { geoJSON }
}
func get(req: Request) throws -> EventLoopFuture<String> {
return GeoJSON.find(req.parameters.get("GeoJSONID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMapThrowing { found in
try Data.fromFile(found.id!.uuidString)
}.map { data in
String(decoding: data, as: UTF8.self)
}.flatMapErrorThrowing { error in
throw Abort(.notFound)
}
}
func put(req: Request) throws -> EventLoopFuture<HTTPStatus> {
let json = req.body.string ?? ""
guard let jsonData = json.data(using: .utf8) else {
throw Abort(.internalServerError)
}
guard let geoJSONID = req.parameters.get("GeoJSONID") , let uuid = UUID(geoJSONID) else {
throw Abort(.notFound)
}
return GeoJSON.query(on: req.db).filter(\._$id == uuid)
.with(\.$geoJSONPayloads).first()
.unwrap(or: Abort(.notFound))
.map { found in
if let anyCodable = try? JSONDecoder().decode(AnyCodable.self, from: jsonData) {
found.geoJSONPayloads.first!.json = anyCodable
} else {
found.geoJSONPayloads.first!.json = AnyCodable()
}
}.transform(to: .ok)
}
func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> {
guard let geoJSONID = req.parameters.get("GeoJSONID") , let uuid = UUID(geoJSONID) else {
throw Abort(.notFound)
}
return GeoJSON.query(on: req.db).filter(\._$id == uuid)
.with(\.$geoJSONPayloads).first()
.unwrap(or: Abort(.notFound))
.flatMap { found -> EventLoopFuture<Void> in
found.geoJSONPayloads.first?.delete(on: req.db)
return found.delete(on: req.db)
}.transform(to: .ok)
}
}
my routing:
import Fluent
import Vapor
func routes(_ app: Application) throws {
app.get { req in
return "It works!"
}
app.group("MapEditorBackend") { mapEditorBackend in
let geoJSONController = GeoJSONController()
mapEditorBackend.get(use: geoJSONController.index)
mapEditorBackend.post(use: geoJSONController.create)
mapEditorBackend.group(":GeoJSONID") { geoJSON in
geoJSON.get(use: geoJSONController.get)
geoJSON.put(use: geoJSONController.put)
geoJSON.delete(use: geoJSONController.delete)
}
}
}
create (POST) works, the top level GET works, the GET using the GeoJSONID path param is still old (I was working with files in the file system before), PUT and DELETE fail alike (same error in Log like above)
Is this my fault or a bug in PostgresKit?
Thanks in advance for your help!
kind regards,
Lars