jeffijoe / awilix-express

Awilix helpers/middleware for Express
MIT License
114 stars 7 forks source link

Accessing controller instance on before/after middlewares #8

Closed ghost closed 6 years ago

ghost commented 6 years ago

I'm setting up a controller with awilix-express, it works well, but I have some doubts.

In this example below, is it possible to setup a middleware on before that access the instance TodoController?

const { createController } = require('awilix-express')

class TodoController {
  constructor ({ todosService, cacheService }) {
    this.todos = todoService
    this.cacheMiddleware = cacheService
  }

  getAll(req, res) => {
    return res.send(this.todosService.getTodos())
  }

}

module.exports = createController(TodoController)
  .get('/todos', 'getAll', {
    before: [ this.cacheMiddleware() ] // I know "this" doesn't work here, but I need to access cacheMiddleware, How?
  })

Is there a way to do that? Maybe changing approach and not using a class? Can you show an example please? Thanks!

jeffijoe commented 6 years ago

@IGZgustavomarin you can use inject to have Awilix build the middleware:

const { createController, inject } = require('awilix-express')
// Assuming `cacheMiddleware` is a factory function
const { cacheMiddleware } = require('./your/cache/middleware')

class TodoController {
  constructor ({ todosService }) {
    this.todos = todoService
  }

  getAll(req, res) => {
    return res.send(this.todosService.getTodos())
  }

}

module.exports = createController(TodoController)
  .get('/todos', 'getAll', {
    before: [ inject(cacheMiddleware) ]
  })
ghost commented 6 years ago

Ok, this way works, but my middleware gets created each time I call the endpoint. Even if I set the lifetime to SINGLETON. I'm doing this:

const cacheProducts = ({ config }) => {
   return (req, res, next) => {
     /** cache logic here, return memory data or return next() **/  
  }
  return cache
}

cacheProducts[RESOLVER] = {
  lifetime: Lifetime.SINGLETON
}
module.exports = cacheProducts

Is is possible to inject the same middleware every time? Otherwise, it never hits my cache data.

FYI: All this was working on previous version with code like this:

classs ProductsRouter {
/** code **/
this.router = Router()
this.router.get('/products', 
   cacheProductsMiddleware, // cache layer
   inject('getAllProducts'),  // data access layer
   this.getAll // <- the endpoint handler
)
jeffijoe commented 6 years ago

Lifetimes only apply to registrations, not resolvers built directly with container.build (which is what inject does), so you want to wrap it to use the registration:

module.exports = createController(TodoController)
  .get('/todos', 'getAll', {
    // assuming your middleware registration is named "cacheService"
    before: [ inject(({ cacheService }) => cacheService) ]
  })
jeffijoe commented 6 years ago

In fact, Awilix has a resolver for this called aliasTo which does exactly what the previous example does:

const { aliasTo } = require('awilix')

module.exports = createController(TodoController)
  .get('/todos', 'getAll', {
    // assuming your middleware registration is named "cacheService"
    before: [ inject(aliasTo('cacheService')) ]
  })

Hope this helps. 😄

ghost commented 6 years ago

Ok, now the middleware gets injected each time. But the cache layer makes some patching to the Response object that doesn't work with inject, meaning it never cache anything. I think the middleware function that inject actually adds, collides with the apicache (npm module) that I'm using.

Is there a way to create a controller and register the routes differently?

jeffijoe commented 6 years ago

But the cache layer makes some patching to the Response object that doesn't work with inject, meaning it never cache anything

How so?

How do you register cacheService? It would have to be singleton. Also, that cacheProducts middleware has return cache as well as return (req, res) => ..., is that a typo in the example?

Could you try creating a minimal reproduction repository? That would make it way easier for me to help you, cause I'm certain we can make it work. 😄

ghost commented 6 years ago

Finally solve it. It turns out that the cache layer didn't work with HEAD request only and that was messing my example. It works as intended. Sorry about that!

I've separated the logic into two controllers, like this:

mainRouter
 .use(loadControllers('cache/*'))
 .use(loadControllers('products/*'))

Each of those folders contains a class wrapped with a createController call. But I noticed that on each request a new instance is created. This is too expensive for the cache layer (ex: connection, warmup, etc). Is it possible to make that instance a singleton?

My cache controllers is as this:


class ProductsCache {
  constructor ({ config }) {
    /** THIS IS CALLED ON EACH REQUEST **/
    this.cacheAll = apicache.options({
       ...(config.cache.defaultConfig),
       enabled: config.cache.defaultConfig.enabled === 'yes'
      })
      .middleware(config.cache.expiration.productList)
    }

    deleteAll = (req, res) => {
      res.send(apicache.clear('/api/products'))
    }

}

module.exports =  createController(ProductsCache)
  .get('/api/products', 'cacheAll')
  .delete('/api/products/_cache', 'deleteAll')

I tried as a function instead of class, but it also gets called on each request. Can that behavior be changed?

jeffijoe commented 6 years ago

The whole point is for the controller to be instantiated with each request, so you can inject request-specific stuff in the constructor. 😄

Seems to me you want the apicache to be a singleton.

// In your container config
const apicache = require('apicache')

container.register({
  productCache: asFunction(
    ({ config }) => 
      apicache
        .options({
          ...(config.cache.defaultConfig),
          enabled: config.cache.defaultConfig.enabled === 'yes'
        })
        .middleware(config.cache.expiration.productList)
    )
    .singleton()
})

// Controller
class ProductsCache {
  constructor ({ productCache }) {
    this.cacheAll = productCache
  }

  deleteAll = (req, res) => {
    res.send(apicache.clear('/api/products'))
  }

}

module.exports =  createController(ProductsCache)
  .get('/api/products', 'cacheAll')
  .delete('/api/products/_cache', 'deleteAll')
ghost commented 6 years ago

Thanks, I did something similar to that. :tada: