Vapor 4: Children relation not eager loaded, use $ prefix to acces

1.5k views Asked by At

Currently I am working on a school assignment where we have to build an API using Vapor. I have a few basic API calls working and I am trying a bit more advanced API calls now but I can't get this to work.

I have this function addToParty that is being called when the URL /party/join/:partyID is called with a body

{
    "id": "CC1FAC6B-A2B3-471C-A488-147300196981",
    "username": "string",
    "is_ready": true
}

I am trying to find a party by the partyId and add the user to the list of users of the party.

func addToParty (req: Request) throws -> EventLoopFuture<Party.Output> {
    guard let id = req.parameters.get("partyID", as: UUID.self) else {
        throw Abort(.badRequest)
    }
    let input = try req.content.decode(Party.JoinParty.self)
    return Party.find(id, on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMap { element in
        element.users.append(User(id: UUID(input.id), username: input.username, is_ready: input.is_ready))
        return element.save(on: req.db)
            .map{ Party.Output(code: "200") }
        }
    } 

When I try the code above I get the error Fatal error: Children relation not eager loaded, use $ prefix to access: Children<Party, User>(for: [party_id]): file FluentKit/Children.swift, line 33 from the line

element.users.append(User(id: UUID(input.id), username: input.username, is_ready: input.is_ready))

When I comment this line the code runs and I get a return code.

I tried adding the prefix to element.$users and $User but then it complains about not being able to find element.$users and $User in scope.

Party model

import Fluent
import Vapor

final class Party: Model, Content {
    static let schema = "parties"

    struct JoinParty: Content {
        let id: String
        let username: String
        let is_ready: Bool
    }

    struct Output: Content {
        let code: String
    }
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "party_code")
    var party_code: String
    
    @Field(key: "host_id")
    var host_id: UUID

    @Field(key: "is_active")
    var is_active: Bool

    // change to Game when model is made
    @Field(key: "selected_games")
    var selected_games: [String]?

    // change to Setting when model is made  
    @Field(key: "settings")
    var settings: String

    @Field(key: "results")
    var results: Array<GameResult>?

    @Children(for: \.$party)
    var users: [User]

    init() { }

    init(id: UUID? = nil,
        party_code: String,
        host_id: UUID,
        is_active: Bool,
        selected_games: [String]? = nil,
        settings: String,
        results: Array<GameResult>? = nil) {
        self.id = id
        self.party_code = party_code
        self.host_id = host_id
        self.is_active = is_active
        self.selected_games = selected_games
        self.settings = settings
        self.results = results
    }
}

User model

import Fluent
import Vapor

final class User: Model, Content {
    static let schema = "users"

    struct Input: Content {
        let id: UUID
        let username: String
    }

    struct Output: Content {
        let id: String
        let username: String
    }
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "username")
    var username: String

    @Field(key: "is_ready")
    var is_ready: Bool

    @OptionalParent(key: "party_id")
    var party: Party?

    @Children(for: \.$user)
    var gameResults: [GameResult]

    init() {}

    init(id: UUID? = nil, username: String, is_ready: Bool, partyID: UUID? = nil) {
        self.id = id
        self.username = username
        self.is_ready = is_ready
        self.$party.id = partyID
    }
}

I have a similar function to update a username from a user already working which is almost the same thing.

func update(req: Request) throws -> EventLoopFuture<User.Output> {
      let input = try req.content.decode(User.Input.self)
        return User.find(input.id, on: req.db)
            .unwrap(or: Abort(.notFound))
            .flatMap { user in
                user.username = input.username
                return user.save(on: req.db)
                    .map { User.Output(id: user.id!.uuidString, username: user.username) }
            }
      }

Any help would be really appreciated. Thank you in advance.

1

There are 1 answers

0
Caleb Kleveter On

Unfortunately, adding children to a parent model is not that intuitive yet. I hope that someday we can get that added, but it's not there yet. Fluent 5 maybe?

Anyway, what you'll need to do instead is create your new User model, passing in the party's ID value to the partyID initializer parameter, and then save the User model.

let user = User(id: UUID(input.id), username: input.username, is_ready: input.is_ready, partyID: element.id)
return user.save(on: request.db)

So your method should end up looking like this:

func addToParty(req: Request) throws -> EventLoopFuture<Party.Output> {
    guard let id = req.parameters.get("partyID", as: UUID.self) else {
        throw Abort(.badRequest)
    }
    let input = try req.content.decode(Party.JoinParty.self)

    return Party.find(id, on: req.db).unwrap(or: Abort(.notFound)).flatMap { element in
        return User(
            id: UUID(input.id), 
            username: input.username, 
            is_ready: input.is_ready,
            partyID: element.id
        ).save(on: req.db)
    }.transform(to: Party.Output(code: "200"))
}