Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 677
Tim Dean Avatar asked Aug 10 '20 01:08

Tim Dean


2 Answers

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
}
like image 180
0xTim Avatar answered Oct 07 '22 03:10

0xTim


I have managed to get the example from the docs working as-is.

Tracing through the execution with the debugger, there is the predictable call to get, as you say, and this returns the instance from .init() as the failover from not having a previously stored value. Included in the example you refer to is:

struct UserRepositoryFactory {
    var make: ((Request) -> UserRepository)?
    mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
        self.make = make
    }
}

This use function is executed next, which is mutating and updates the variable make. I believe it is this change to make that then triggers a call to set. It certainly happens immediately after use and before execution moves on in configure.swift. So, by the time the server formally starts and you actually use the Repository in a route, there is a stored instance that is reused as required.

like image 22
Nick Avatar answered Oct 07 '22 02:10

Nick