How do you define multiple UUID in a model - swift

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.

Related

Creation of instance set id, although it should be nil

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.

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.

Realm Model Conversion Failed

I have a realm db model like:
import RealmSwift
class User: Object {
let isVerified = RealmOptional<Bool>()
#objc dynamic var pk = 0
#objc dynamic var profilePicUrl: String? = nil
}
And I am getting data from service and it returns same name and type like realm model.
I want to save this data to db. But when I try to convert model to realm model it gives error.
let data = [Users(value: serviceUser)] -> serviceUser comes from service.
Before save when I try to convert I get this error:
'RLMException', reason: 'Invalid value 'ServiceUserModel(isVerified: Optional(false), pk: Optional(123456), profilePicUrl: Optional("") of type '__UserDataValue' for 'bool' property 'Users.isVerified'.'
serviceUser Model:
public struct ServiceUserModel: Codable {
public var isVerified: Bool?
public var pk: Int?
public var profilePicUrl: String?
}
I do not want to use for loop because of performance problem. I want to save this data in one time.
Save Method:
func save(users: [Users]){
try! database.write {
database.add(users)
}
}
How can I convert it?
I think the problem is located here:
when I try to convert
When dealing with RealmOptional you have to set it's .value property.
let isVerified = RealmOptional<Bool>()
now the isVerified.value is nil
isVerified.value = true
now it's true
But you can't use the Bool? to set the value.
The same applies to pk.
You should implement the convenience init() and provide some default values for your optionals

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

Nested Queries in Realm. Swift

I am trying to create nested queries in realm. I will paste my models in and explain what I mean.
Parent Model
#objcMembers class Group: Object {
dynamic var uuid: String = ""
dynamic var admin: User?
convenience init(uuid: String, admin: User) {
self.init()
self.uuid = uuid
self.admin = admin
}
}
Child Model
#objcMembers class Message: Object {
dynamic var uuid: String = ""
dynamic var group: Group?
dynamic var message: String = ""
convenience init(uuid: String, group: Group, from: User, message: String) {
self.init()
self.uuid = uuid
self.group = group
self.message = message
}
}
What I am trying to do is filter messages that are in a group with uuid x
All the answers I have seen are outdated.
What I have right now is
let result = RealmService.shared.realm.objects(Message.self).filter("group.uuid =
0E81CDEF-B63F-4DBE-9900-B486D40F4EC9")
What is the correct way of doing this?
Figured it out:
let result = RealmService.shared.realm.objects(Message.self).filter("group.uuid = '2C5E1738-1167-40CB-BE43-C415FD5E6E5D'")
Queried value has to be wrapped in ''