Search code examples
vaporvapor-fluent

Understanding how to initialize a Vapor 4 repository


I am trying to migrate some code using a Repository pattern from Vapor 3 to Vapor 4. I have gone through the documentation of this specific pattern from the Vapor 4 documentation, and I think I understand it for the most part.

The one thing I am not getting, however, is the way that the repository factory gets set within the Application extension. The example from the documentation shows this:

extension Application {
    private struct UserRepositoryKey: StorageKey { 
        typealias Value = UserRepositoryFactory 
    }

    var users: UserRepositoryFactory {
        get {
            self.storage[UserRepositoryKey.self] ?? .init()
        }
        set {
            self.storage[UserRepositoryKey.self] = newValue
        }
    }
}

If I am reading the getter method correctly (and I might not be - I'm far from a Swift expert), a new instance of the UserRepositoryFactory structure will be created and returned when app.users is referenced. At that time, however, it does not appear that the contents of self.storage[UserRepositoryKey.self] is changed in any way. So if I happened to access app.users two times in a row, I would get 2 different instances returned to me and self.storage[UserRepositoryKey.self] would remain set to nil.

Following through the rest of the sample code in the document, it appears to define the make function that will be used by the factory when configuring the app as so:

app.users.use { req in
    DatabaseUserRepository(database: req.db)
}

Here it seems like app.users.use would get a new factory instance and call its use function to set the appropriate make method for that instance.

Later, when I go to handle a request, I use the request.users method that was defined by this Request extension:

extension Request {
    var users: UserRepository {
        self.application.users.make!(self)
    }
}

Here it seems like self.application.users.make would be invoked on a different repository factory instance that is referenced by self.application.users. It would therefore not apply the factory's make method that was set earlier when configuring the application.

So what am I missing here?


Solution

  • It looks like the docs are slightly out of date for that. You can have a look at how views or client is done, but somewhere you need to call initialize() to set the repository. Here's what my working repository looks like:

    import Vapor
    
    extension Application {
        struct Repositories {
            
            struct Provider {
                let run: (Application) -> ()
                
                public init(_ run: @escaping (Application) -> ()) {
                    self.run = run
                }
            }
            
            final class Storage {
                var makeRepository: ((Application) -> APIRepository)?
                init() { }
            }
            
            struct Key: StorageKey {
                typealias Value = Storage
            }
            
            let application: Application
            
            var repository: APIRepository {
                guard let makeRepository = self.storage.makeRepository else {
                    fatalError("No repository configured. Configure with app.repositories.use(...)")
                }
                return makeRepository(self.application)
            }
            
            func use(_ provider: Provider) {
                provider.run(self.application)
            }
            
            func use(_ makeRepository: @escaping (Application) -> APIRepository) {
                self.storage.makeRepository = makeRepository
            }
            
            func initialize() {
                self.application.storage[Key.self] = .init()
            }
            
            private var storage: Storage {
                if self.application.storage[Key.self] == nil {
                    self.initialize()
                }
                return self.application.storage[Key.self]!
            }
        }
        
        var repositories: Repositories {
            .init(application: self)
        }
    }
    

    That autoinitializes itself the first time it's used. Note that APIRepository is the protocol used for my repostiory. FluentRepository is the Fluent implementation of that protocol. Then like you I have an extension on Request to use it in request handlers:

    extension Request {
        var repository: APIRepository {
            self.application.repositories.repository.for(self)
        }
    }
    

    Finally, you need to configure it to use the right repository. So in my configure.swift I have:

    app.repositories.use { application in
        FluentRepository(database: application.db)
    }
    

    and in tests I can switch it for the in-memory repository that doesn't touch the DB:

    application.repositories.use { _ in
        return inMemoryRepository
    }