Search code examples
swiftvaporvapor-fluent

Parent Child Relation with Vapor 4


I want to set up a parent child relationship between a League and a Team in Vapor 4. I can create a League just fine, but when I try to create a new team like this:

{
    "name": "Chicago Bulls",
    "league_id": "C21827C2-8FAD-4A89-B8D3-A3E62E421258"
}

I'm getting this error:

{
    "error": true,
    "reason": "Value of type 'League' required for key 'league'."
}

I just want to initialize a Team with a league_id that references a League from the Leagues table. I had this working in Vapor 3 but can't seem to get it right in Vapor 4.

See models and migrations below.

League model:

final class League: Model, Content {

    init() {}
    static let schema = "Leagues"

    @ID(key: .id) var id: UUID?
    @Field(key: .name) var name: String
    @Field(key: .sport) var sport: String

    @Children(for: \.$league) var teams: [Team]

    init(name: String, sport: String) {
        self.name = name
        self.sport = sport
    }

}

Team model:

final class Team: Model, Content {

    init() {}
    static let schema = "Teams"

    @ID(key: .id) var id: UUID?
    @Field(key: .name) var name: String

    @Parent(key: .leagueId) var league: League

    init(id: UUID? = nil, name: String, leagueId: UUID) throws {
        self.id = id
        self.name = name
        self.$league.id = leagueId
    }

}

CreateLeague migration:

struct CreateLeague: Migration {

    func prepare(on database: Database) -> EventLoopFuture<Void> {
        return database.schema(League.schema)
            .id()
            .field(.name, .string, .required)
            .field(.sport, .string, .required)
            .create()
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        return database.schema(League.schema).delete()
    }

}

CreateTeam migration:

struct CreateTeam: Migration {

    func prepare(on database: Database) -> EventLoopFuture<Void> {
        return database.schema(Team.schema)
            .id()
            .field(.name, .string, .required)
            .field(.leagueId, .uuid, .required, .references(League.schema, .id))
            .create()
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        return database.schema(Team.schema).delete()
    }

}

TeamsController:

class TeamsController: RouteCollection {

    func boot(routes: RoutesBuilder) throws {
        let teamsRoute = routes.grouped("teams")
        teamsRoute.post(use: createTeam)
    }

    func createTeam(req: Request) throws -> EventLoopFuture<Team> {
        let team = try req.content.decode(Team.self)
        return team.save(on: req.db).map { team }
    }

}

Solution

  • This is failing because of the way property wrappers override the JSON decoding for models. You have two options. You can either send the JSON Fluent expects:

    {
        "name": "Chicago Bulls",
        "league": {
            "id": "C21827C2-8FAD-4A89-B8D3-A3E62E421258"
        }
    }
    

    Or you can create a new type, CreateTeamData that matches the JSON you would expect to send, and manually create a Team out of it. I much prefer the second route:

    struct CreateTeamData: Content {
      let name: String
      let leagueID: UUID
    }
    
    func createTeam(req: Request) throws -> EventLoopFuture<Team> {
        let data = try req.content.decode(CreateTeamData.self)
        let team = Team(name: data.name, leagueID: data.leagueID)
        return team.save(on: req.db).map { team }
    }