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

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.

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"))
}

Related

How can I fetch a json file using Vapor for my leaf template to show the data?

I have a JSON hosted somewhere and I want to fetch the content, put it in a context for my leaf template to read.
However, I cannot make it work. I get the code to compile, but I get an error in the localhost
{"error":true,"reason":"Unsupported Media Type"}
Can somebody help me please! Happy holidays for all.
struct WebsiteController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.get(use: indexHandler)
}
func indexHandler(_ req: Request) -> EventLoopFuture<View> {
return req.client.get("https://streeteasydaily.s3.us-west-2.amazonaws.com/streeteasy1.json").flatMap { res in
do {
let json = try res.content.decode([Listing].self)
print(json[0].photos[0])
let context = IndexContext(title: "Homepage", listings: json)
return try req.view.render("index", context)
} catch {
// Handle error
print("cayo en error")
return req.eventLoop.makeFailedFuture(error)
}
}
}
}
struct IndexContext: Encodable {
let title: String
let listings: [Listing]
}
Model
final class Listing: Model {
static let schema = "listings" //basically the table name
#ID
var id: UUID?
#Field(key: "address")
var address: String
#Field(key: "description")
var description: String
#Field(key: "photos")
var photos: [String]
init() {}
//to initialize the db
init(id: UUID? = nil, address: String, description: String, photos: [String]) {
self.id = id
self.address = address
self.description = description
self.photos = photos
}
}
//to make acronym conform to CONTENT, and use it in Vapor
extension Listing: Content {}
This error is because the decode is failing to identify all the fields in your JSON to match against those defined in Listing and/or the array of such objects. The filenames must match those in the JSON exactly - i.e. case-sensitive and every field in the structure/model must exist in the JSON. Additional fields in the JSON that are not needed/included in the structure/model are fine.

Vapor / Leaf | Pass DB-Data to Leaf-View throws a FatalError (superEncoder)

I' am new to Vapor and Leaf.
I have defined a route that should query data from the DB and pass it to a view. The problem: As soon as I call this route I get a fatal error in the LeadEncoder File Row 162 (superEncoder Function). Here is a Picture:
Fatal Error Picture
Route File:
app.get("dashboard") { req async throws -> View in
let result = try await Cocktail.query(on: req.db).all()
let context = CocktailContext(cocktails: result)
return try await req.view.render("dashboard", context)
}
Context File:
struct CocktailContext: Codable, Content {
let cocktails: [Cocktail]
}
Model File:
final class Cocktail: Model, Content, Codable {
static let schema = "cocktail"
#ID(key: .id)
var id: UUID?
#Field(key: "name")
var name: String
#Field(key: "description")
var description: String
#Field(key: "amount_ml")
var amount_ml: Int
#Field(key: "img_url")
var img_url: String
#OptionalField(key: "video_url")
var video_url: String?
#Parent(key: "difficulty_id")
var difficulty_id: Difficulty
#Parent(key: "glass_id")
var glass_id: Glass
init() { }
init(name: String, description: String, amount_ml: Int, image_url: String, video_url: String?, difficulty_id: Difficulty.IDValue, glass_id: Glass.IDValue) {
self.id = UUID()
self.name = name
self.description = description
self.amount_ml = amount_ml
self.img_url = image_url
self.video_url = video_url
self.$difficulty_id.id = difficulty_id
self.$glass_id.id = glass_id
}
}
What am I doing wrong? I can't find it in the vapor documentary and I can't find a more detailed description of the error so far. All I know is that the data is queried correctly from the DB...
Thank you in advance!

Add user using Authentication + Create User

Currently, I have successfully implemented allowing a user to create an account using an email address and password and inserting that info into Firebase Authentication. What I'm trying to figure out now is how to simultaneously take the information entered in the sign up form and insert a new record into FireStore. I'd like to insert the user's email address, first and last name as a record.
I've tried adding the code to insert the record inside of my "SignUp" function, but that seems to break the function. Below is the working code followed by the code I tried that didn't work.
//Working Code
struct SignUpView: View {
#State var email: String = ""
#State var password: String = ""
#State var error: String = ""
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func signUp() {
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
} else {
self.email = ""
self.password = ""
}
}
}
//Does Not Work (Error: Type 'SignUpView' does not conform to protocol 'View')
struct SignUpView: View {
#State var email: String = ""
#State var password: String = ""
#State var error: String = ""
#State var fname: String = ""
#State var lname: String = ""
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func signUp() {
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
} else {
self.email = ""
self.password = ""
}
}
// Insert record into Users collection
db.collection("users").addDocument(data: [
"fname": fname,
"lname":lname,
"email": email ])
}
// End Insert
}
Session.SignUp is asynchronous.
struct SignUpView: View {
#State var email: String = ""
#State var password: String = ""
#State var error: String = ""
#State var fname: String = ""
#State var lname: String = ""
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func signUp() {
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
} else {
self.email = ""
self.password = ""
// Insert record into Users collection
db.collection("users").addDocument(data: [
"fname": self.fname,
"lname":self.lname,
"email": self.email ])
}
// End Insert
}
}
}
But the code you posted can not be the full code for your SignUpView because it needs to contain
var body: some View {}
Adding a user model to Firestore in SwiftUI is very simple. I used a completionBlock to handle completion, you can show a loading indicator till the function returned a value. This may be useful for all queries.
The following code will add a user to Firestore:
func addUserToDatabase(name: String, email: String, phone: String, completionBlock: #escaping (_ success: Bool) -> Void) {
let accountData = [
"name" : name,
"email" : email,
"phone" : phone
]
db.collection("users").document(self.user.id).setData(accountData) { err in
if err != nil {
completionBlock(false)
} else {
completionBlock(true)
}
}
}
Note that: 'self.user.id' is the user ID from the authentication. So when you create a user in the firebaseAuth, a unique ID will be created, you can store this ID in your app so you can create a reference in your database. Useful for easily removing or changing user's data.
Example of using this method in your app
self.addUserToDatabase(name: name, email: email, phone: phone) { (succes) in
if (succes) {
print("User added to the database")
UserDefaults.standard.set(self.user.id, forKey: "uid_current_user")
completionBlock(true)
} else {
print("Something went wrong when trying to add user to the database")
completionBlock(false)
}
}

How should I retrieve data from firebase and put it into a dictionary?

I have an organization document with a Members collection inside of it and then a members document inside of that. Inside the members document includes a Map of a user which is a member. The Key is the UserID and 3 values (firstName, lastName, username). I am trying to load in this data into my "Event" class that holds a membersInvited Property that is a dictionary. Inside the Event class is a method to get this data called getOrgMembers(). Even though I have that data in firebase I am getting a nil value for my dictionary. I also am using Dispatch but kind of new to it.
Below is code in the Event Class:
var membersInvited: [Member: Bool]?
func getOrgMembers(dispatch: DispatchGroup?) {
let membRef = BandzDatabase.collection("Organizations").document(currentUser.currentOrgID!).collection("Members").document("members")
membRef.getDocument { (snapshot, error) in
if let error = error {
print (error.localizedDescription)
} else {
if let data = snapshot?.data() {
for (key,value) in data {
if let membArray = value as? [String: Any] {
let username = membArray["username"] as? String
let firstName = membArray["firstName"] as? String
let lastName = membArray["lastName"] as? String
let userID = key
let member = Member(username: username ?? "", firstName: firstName ?? "", lastName: lastName ?? "", userID: userID)
self.membersInvited?.updateValue(true, forKey: member)
}
}
}
}
dispatch?.leave()
}
}
struct Member: Hashable {
var username: String
var firstName: String
var lastName: String
var userID: String
init (username: String, firstName: String, lastName: String, userID: String) {
self.username = username
self.firstName = firstName
self.lastName = lastName
self.userID = userID
}
}
Below is were I call this method from another class:
func getMembers() {
showActivityIndicatory(uiView: self.view)
self.dispatchGroup.enter()
eventMade?.getOrgMembers(dispatch: self.dispatchGroup)
self.dispatchGroup.notify(queue: .main) {
//self.tableView.reloadData()
stopActivityIndicator()
print("happens")
print(self.eventMade?.membersInvited)
}
}
After some research, I discovered that since I never initilized the dictionary, whenever I was calling to append key-value paires it would not even run since it was an optional. So I changed the decleration to this:
var membersInvited: [Member: Bool] = [:]

I found a generic way to turn a class into a dictionary, is there a generic way to reverse that process?

So I'm trying to build a data-mapper style ORM in Swift. I was trying to find a way to turn any entity into a dictionary so I could map that to a database table. I managed to do that with this code:
public protocol Entity: class {
var id: Int? { get set }
}
func unwrap(any:Any) -> Any? {
let mi = Mirror(reflecting: any)
if mi.displayStyle != .Optional {
return any
}
if mi.children.count == 0 {
return nil
}
let (_, some) = mi.children.first!
return some
}
public func serialize<T: Entity>(entity: T) -> [String:String] {
let aMirror = Mirror(reflecting: entity)
var dict = [String:String]()
for child in aMirror.children {
if let label = child.label where !label.hasPrefix("_") {
switch unwrap(child.value) {
case let entity as Entity:
if let id = entity.id {
dict[label] = String(id)
}
case let .Some(test):
dict[label] = String(test)
default: break
}
}
}
return dict
}
public class User: Entity {
public var id: Int?
var username: String
var password: String
var role: UserRole?
public init(username: String, password: String) {
self.username = username
self.password = password
}
public func assignRole(role: UserRole) {
self.role = role
}
}
public class UserRole: Entity {
public var id: Int?
var name: String
var description: String
public init(name: String, description: String) {
self.name = name
self.description = description
}
}
So now I can do:
let newUser = User(username: "NotMyRealName", password: "SomeKindOfPassword")
let adminRole = UserRole(name: "admin", description: "Administrator of the website")
adminRole.id = 1
newUser.assignRole(adminRole)
print(serialize(newUser))
Which will print: ["password": "SomeKindOfPassword", "role": "1", "username": "NotMyRealName"]
That's all well and good, but now I want to reverse this process.
I'd like to write a function that could use like so:
let user = unserialize(["username":"Me", "password":"secret"]) as? User
Is that possible you think? I understand it might be a bit of a hack if I'd try to bypass the init method, but I'd like to try.