How do I add an additional property to a Model? - swift

I have a Model which conforms to Content:
import Vapor
import Fluent
final class User: Model, Content {
static let schema = "user"
#ID(key: .id)
var id: UUID?
#Field(key: "email")
var email: String
init() { }
init(id: UUID? = nil, email: String) {
self.id = id
self.email = email
}
}
This can be fetched from the database and directly returned from a route, so the client receives the users as JSON.
Now I want to add an additional property to the user, which isn't stored in the database but a fixed value or just added after the user is loaded from the database:
final class User: Model, Content {
// ...
var someOther: String = "hello"
// ...
}
or
final class User: Model, Content {
// ...
var someOther: String? // set later
// ...
}
Now the problem is that this property is never added to the JSON, so the client only gets the user's id and email and NOT the someProperty. How do I fix this?

When you want to send or receive data from your client that has a different representation from what your model has, it's common practice to define a custom type that you can then convert to and from the model type.
Given this, you would define a new User.Response type that conforms to Content instead of the User model, and return that from your route instead.
extension User {
struct Response: Content {
let id: UUID
let email: String
let otherValue = "Hello World"
}
}
func user(request: Request) throws -> EventLoopFuture<User.Response> {
return User.find(UUID(), on: request.db).flatMapThrowing { user in
guard let id = user.id else {
throw Abort(.internalServerError, "User is missing ID")
}
return User.Response(id: id, email: user.email)
}
}
The reason the additional value that you added wasn't encoded from your model is because the default encoder for model types iterates over the field properties and encodes those properties, so if you have a property that doesn't use one of the ID, Field, Parent, Children, or other field property wrappers, it won't be encoded.

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.

How to add custom data instances to an array in Firestore?

I have a question where and how to properly save users who are logged into a rooms, now I save them inside the collection "rooms" but I save the user as a reference
func addUserToRoom(room: String, id: String) {
COLLETCTION_ROOMS.document(room).updateData(["members": FieldValue.arrayUnion([COLLETCTION_USERS.document(id)])])
}
But when I create a rooms, I need to pass in an array of users
func fetchRooms() {
Firestore.firestore()
.collection("rooms")
.addSnapshotListener { snapshot, _ in
guard let rooms = snapshot?.documents else { return }
self.rooms = rooms.map({ (queryDocumentSnapshot) -> VoiceRoom in
let data = queryDocumentSnapshot.data()
let query = queryDocumentSnapshot.get("members") as? [DocumentReference]
// ======== I'm making a request here, but I don't understand how to save users next
query.map { result in
result.map { user in
let user = user.getDocument { userSnapshot, _ in
let data = userSnapshot?.data()
guard let user = data else { return }
let id = user["id"] as? String ?? ""
let first_name = user["first_name"] as? String ?? ""
let last_name = user["last_name"] as? String ?? ""
let profile = Profile(id: id,
first_name: first_name,
last_name: last_name)
}
}
}
let id = data["id"] as? String ?? ""
let title = data["title"] as? String ?? ""
let description = data["description"] as? String ?? ""
let members = [Profile(id: "1", first_name: "Test1", last_name: "Test1")]
return VoiceRoom(id: id,
title: title,
description: description,
members: members
})
}
}
This is how my room and profile model looks like
struct VoiceRoom: Identifiable, Decodable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Decodable {
var id: String
var first_name: String?
var last_name: String?
}
Maybe someone can tell me if I am not saving users correctly and I need to do it in a separate collection, so far I have not found a solution, I would be grateful for any advice.
I feel like this answer is going to blow your mind:
struct VoiceRoom: Identifiable, Codable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Codable {
var id: String
var first_name: String?
var last_name: String?
}
final class RoomRepository: ObservableObject {
#Published var rooms: [VoiceRoom] = []
private let db = Firestore.firestore()
private var listener: ListenerRegistration?
func addUserToRoom(room: VoiceRoom, user: Profile) {
let docRef = COLLECTION_ROOMS.document(room.id)
let userData = try! Firestore.Encoder().encode(user)
docRef.updateData(["members" : FieldValue.arrayUnion([userData])])
}
func fetchRooms() {
listener = db.collection("rooms").addSnapshotListener { snapshot, _ in
guard let roomDocuments = snapshot?.documents else { return }
self.rooms = roomDocuments.compactMap { try? $0.data(as: VoiceRoom.self) }
}
}
}
And that's it. This is all it takes to correctly store members of a room and simply decode a stored room into an instance of VoiceRoom. All of it is pretty self explanatory but if you have any questions feel free to ask it in the comments.
P.S. I changed COLLETCTION_ROOMS to COLLECTION_ROOMS
Edit:
I decided to elaborate on my changes anyway so that people who just started coding can understand how I reduced the code to just a few lines (skip this if you understood my code).
When your custom data instances conform to the Codable protocol it allows those instances to be automatically transformed into data the database can store (known as Encoding) AND allows them to converted back into the concrete types in your code when you retrieve them from the database (known as Decoding). And the magic of it is all you need to do is add : Codable to your structs and classes to get all this functionality for free!
Now, what about the functions? The addUserToRoom(room:user:) function takes in a VoiceRoom instead of a String because it's generally easier for the caller to just pass in room instead of room.id. This extra step can be done by the function itself and saves a little bit of clutter in your Views while also making it easier.
Finally the fetchRooms() function attaches a listener to the "rooms" collection and fires a block of code any time the collection changes in any way. In the block of code, I check if the document snapshot actually contains documents by using guard let roomDocuments = snapshot?.documents else { return }. After I know I have the documents all that's left to do is convert those documents back into VoiceRoom instances which can easily be done because VoiceRoom conforms to Codable (Remember how I said: "AND allows them to converted back into the concrete types in your code when you retrieve them from the database"). I do this by mapping the array of [QueryDocumentSnapshot] i.e. roomDocuments, into concrete VoiceRoom instances. I use a compact map because if any of the documents fail to decode it won't be contained in self.rooms.
So
try? $0.data(as: VoiceRoom.self)
tries to take every document (represented by $0) and represent its "data" "as" "VoiceRoom" instances.
And that's all it takes!

How do you define multiple UUID in a model

I am using Vapor and Fluent. I want to define a user model like below, but I get an error saying:
Fatal error: Error raised at top level: previousError(server: multiple primary keys for table "users" are not allowed
Is it not possible to define multiple UUIDs in one model?
import Vapor
import FluentPostgresDriver
final class User: Model, Content {
static let schema = "users"
#ID(custom: "id")
var id: Int?
#Field(key: "email")
var email: String
#Field(key: "password")
var password: String
#ID(custom: "public_id")
var public_id: UUID?
init() { }
init(id: Int? = nil,email: String, password:String, public_id: UUID? = nil) {
self.id = id
self.email = email
self.password = password
self.public_id = public_id
}
}
struct CreateUser: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("users")
.field("id", .int, .identifier(auto: true))
.field("email", .string)
.field("password", .string)
.field("public_id", .uuid,?????)
.create()
}
.....
}
Instead of marking the public UUID as #ID, just mark it as another #Field (string type is easiest, but you can also do binary [16 bytes] if you're feeling like wearing a propeller beanie), and make it required.
During the create, you'll have to actually invoke a UUID generation function, but that ought to be easy enough.
But, why not make the primary key the UUID, instead of having two identifiers? It takes a little more room in the database, but might be worth avoiding the headache of having several different ids.

DocumentId wrapper is not working properly

I am using #DocumentId in my data struct:
struct UserProfile : Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
}
When I try to access the id I get a random string of letters (04F9C67E-C4A5-4870-9C22-F52C7F543AA5) Instead of the name of the document in firestore (GeG23o4GNJt5CrKEf3RS)
To access the documentId I am using the following code in an ObservableObject:
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
print("userId \(currentUser.id ?? "no Id")")
print("name \(currentUser.name)")
return userID
}
Where currentUser is a UserProfile struct. currentUser.name returns the proper value. What am I doing wrong?
currentUser is a member of an array populated using the following method:
func fetchData() {
db.collection("Users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.compactMap { (queryDocumentSnapshot) -> UserProfile? in
return try? queryDocumentSnapshot.data(as: UserProfile.self)
}
}
}
The id of the UserProfile struct and the id of your data in Firestore are separate. That's why you're seeing the random string of letters.
I would change your struct similar to the below and you can just ignore the regular "id" variable when you initialize and refer to the documentID instead.
struct UserProfile : Codable, Identifiable {
var id = UUID() // ID for the struct
var documentID: String // ID for the post in Database
}
The problem is that you're overwriting the fetched DocumentID with a newly generated UUID string. You should keep it uninitialized, there is no need to set an ID manually. Even if there is, then you should set it only if the object is created from code but not if it gets decoded from Firestore
found my specific problem, I defined CodingKeys enum and when you do that, you have to make sure that you have a mapping for every attribute.
make sure you put all of your model attributes in CodingKeys enum or don't use custom CodingKeys at all
EDIT: taking the above back...it's still buggy...maybe decodes fine but not encodes and uploading
my workaround for now is to not use .adddocument but instead .setdataenter image description here

Using vapor-fluent to upsert models

I am currently struggling with doing an upsert with vapor/fluent. I have a model something like this:
struct DeviceToken: PostgreSQLModel {
var id: Int?
var token: String
var updatedAt: Date = Date()
init(id: Int? = nil, token: String, updatedAt: Date = Date()) {
self.id = id
self.token = token
self.updatedAt = updatedAt
}
}
struct Account: PostgreSQLModel {
var id: Int?
let username: String
let service: String
...
let deviceTokenId: DeviceToken.ID
init(id: Int? = nil, service: String, username: String, ..., deviceTokenId: DeviceToken.ID) {
self.id = id
self.username = username
....
self.deviceTokenId = deviceTokenId
}
}
From the client something like
{
"deviceToken": {
"token": "ab123",
"updatedAt": "01-01-2019 10:10:10"
},
"account": {
"username": "user1",
"service": "some service"
}
}
is send.
What I'd like to do is to insert the new models if they do not exist else update them. I saw the create(orUpdate:) method however this will only update if the id is the same (in my understanding). Since the client does not send the id i am not quite sure how to handle this.
Also I can't decode the model since the account is send without the deviceTokenId and therefore the decoding will fail. I guess I can address the latter problem by overriding NodeCovertible or by using two different models (one for decoding the json without the id and the actual model from above). However the first problem still remains.
What I exactly want to do is:
Update a DeviceToken if an entry with token already exists else create it
If an account with the combination of username and service already exists update its username, service and deviceTokenId else create it. DeviceTokenId is the id returned from 1.
Any chance you can help me out here ?
For everyone who is interested:
I solved it by writing an extension on PostgreSQLModel to supply an upsert method. I added a gist for you to have a look at: here.
Since these kind of links sometimes are broken when you need the information here a quick overview:
Actual upsert implementation:
extension QueryBuilder
where Result: PostgreSQLModel, Result.Database == Database {
/// Creates the model or updates it depending on whether a model
/// with the same ID already exists.
internal func upsert(_ model: Result,
columns: [PostgreSQLColumnIdentifier]) -> Future<Result> {
let row = SQLQueryEncoder(PostgreSQLExpression.self).encode(model)
/// remove id from row if not available
/// otherwise the not-null constraint will break
row = row.filter { (key, value) -> Bool in
if key == "id" && value.isNull { return false }
return true
}
let values = row
.map { row -> (PostgreSQLIdentifier, PostgreSQLExpression) in
return (.identifier(row.key), row.value)
}
self.query.upsert = .upsert(columns, values)
return create(model)
}
}
Convenience methods
extension PostgreSQLModel {
/// Creates the model or updates it depending on whether a model
/// with the same ID already exists.
internal func upsert(on connection: DatabaseConnectable) -> Future<Self> {
return Self
.query(on: connection)
.upsert(self, columns: [.keyPath(Self.idKey)])
}
internal func upsert<U>(on connection: DatabaseConnectable,
onConflict keyPath: KeyPath<Self, U>) -> Future<Self> {
return Self
.query(on: connection)
.upsert(self, columns: [.keyPath(keyPath)])
}
....
}
I solved the other problem I had that my database model could not be decoded since the id was not send from the client, by using a inner struct which would hold only the properties the client would send. The id and other database generated properties are in the outer struct. Something like:
struct DatabaseModel: PostgreSQLModel {
var id: Int?
var someProperty: String
init(id: Int? = nil, form: DatabaseModelForm) {
self.id = id
self.someProperty = form.someProperty
}
struct DatabaseModelForm: Content {
let someProperty: String
}
}