Creation of instance set id, although it should be nil - swift

I am working on a small vapor project with fluent. Right now I should be able to create a user, create a token for said created user and return the user with the session token. Afterwards I should be able to login and create a new token.
The problem right now is that whenever I create a token, the id value of said token is set to 1, even though I set it to nil. This does not happen when creating a user, then the auto increment is working as intended.
The database I am using is MySQL.
My migration CreateTokens.swift:
import Fluent
struct CreateTokens: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Token.schema)
.field("id", .int, .identifier(auto: true))
.unique(on: "id")
.field("user_id", .int, .references("users", "id"))
.field("value", .string, .required)
.unique(on: "value")
.field("created_at", .datetime, .required)
.field("expires_at", .datetime)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Token.schema).delete()
}
}
My Token (Token.swift):
import Vapor
import Fluent
final class Token: Model {
static let schema = "tokens"
#ID(custom: "id", generatedBy: .database)
var id: Int?
#Parent(key: "id")
var user: User
#Field(key: "value")
var value: String
#Field(key: "expires_at")
var expiresAt: Date?
#Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: Int? = nil,
userId: User.IDValue,
token: String,
expiresAt: Date?
) {
self.id = id
self.$user.id = userId
self.value = token
self.expiresAt = expiresAt
}
}
extension Token: ModelTokenAuthenticatable {
static let valueKey = \Token.$value
static let userKey = \Token.$user
var isValid: Bool {
guard let expiryDate = expiresAt else {
return true
}
return expiryDate > Date()
}
}
In my User struct, when I create a token:
func createToken() throws -> Token {
let calendar = Calendar(identifier: .gregorian)
let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
let token = try Token(userId: requireID(), token: [UInt8].random(count: 16).base64, expiresAt: expiryDate)
return token
}
If I set a breakpoint on the return token and print out token, I get the following:
(lldb) po token
Token(input: [expires_at: Optional(2021-11-11 21:01:10 +0000), value: "wn8kvw/GYSfqL280RLCDbQ==", id: 1])
The ID value is set to 1, however in the code it is (to me) clear that I've set it to nil. Even if I add token.id = nil before the return, it is still set to 1.
What am I doing wrong here? Or did I stumble upon a fluent bug?

As you mentioned, the key "id" seems to be used for both the id and the user properties, which is a common type of mistake in this kind of stringly-typed API.
One way I tried to mitigate this is to extend FieldKey and force myself to always use these static properties instead of string, that way the IDE's auto-complete provides an opportunity for me to think about the right field key to use—something that is missing when you type these manually.
extension FieldKey {
static var userID: FieldKey { "user_id" }
}
// And in the model
#Parent(key: .userID)
var user: User
Hope this helps in the future!

I've found the issue which I've spend at least 1.5h on, and it is a stupid mistake.
The problem lies in:
#Parent(key: "id")
var user: User
This should have been user_id as key, not id. Now it re-writes the id to whatever userId I've send in.

Related

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.

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

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

How do I add an additional property to a Model?

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.

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

Realm Swift subclassing

I have two models that I am trying to subclass in swift using RealmSwift
Model 1: Users
#objcMembers class User: Object, Codable {
dynamic var id: Int = 0
dynamic var username: String = ""
dynamic var email: String = ""
dynamic var counts: Counts = Counts(follows: 0, followed_by: 0)
convenience init(id: Int, username: String, email: String, counts: Counts) {
self.init()
self.id = id
self.username = username
self.email = email
self.counts = counts
}
}
class Counts: Codable {
let follows: Int
let followed_by: Int
init(follows: Int, followed_by: Int) {
self.follows = follows
self.followed_by = followed_by
}
}
Here I created a model which inherits codable because I am using Swift 4 new decoding method. This model works fine. I can go add a user and read it fine. So doing this works fine:
let newUser = User(id: 1, username: "user1", email: "user1#gmail.com", counts: Counts(follows: 100, followed_by: 500000000))
RealmService.shared.write(newUser)
Now I am trying to create a model called Friend which has a field that inherits from User
Model 2: Friend
#objcMembers class Friend: Object {
dynamic var user: User = User(id: 0, username: "", email: "", counts: Counts(follows: 0, followed_by: 0))
dynamic var dateAdded: Date = Date(timeIntervalSinceNow: 0)
convenience init(user: User, dateAdded: Date = Date(timeIntervalSinceNow: 0)) {
self.init()
self.user = user
self.dateAdded = dateAdded
}
}
When I do this and load up my app I get a SIGBART and it highlights my initialization of realm : let realm = RealmService.shared.realm
When I remove my Friend Model the app works fine.
How do you properly subclass in Realm?
You cannot declare properties as normal Swift properties whose types are other Realm subclasses. What you are looking for is a many-to-one relationship.
Many-to-one relationships have to be declared optionals and you can't provide a default value
#objcMembers class Friend: Object {
dynamic var user: User?
dynamic var dateAdded: Date = Date()
convenience init(user: User, dateAdded: Date = Date()) {
self.init()
self.user = user
self.dateAdded = dateAdded
}
}
Unrelated, but when you want to create a Date object representing the current time, just call the default, empty initializer of Date, it will do that for you and it's much shorter than Date(timeIntervalSinceNow: 0).