Search code examples
swiftvaporvapor-fluent

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?


Solution

  • 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!