jeffijoe / awilix

Extremely powerful Inversion of Control (IoC) container for Node.JS
MIT License
3.54k stars 134 forks source link

How should I configure the container? #137

Closed eithermonad closed 5 years ago

eithermonad commented 5 years ago

In your Medium article, you create a factory function to do the work of configuring/registering the container:

export default function configureContainer () {
  const container = createContainer()

  // Ordering does not matter.
  container.register({
    // Notice the scoped() at the end - this signals
    // Awilix that we are gonna want a new instance per "scope"
    todosService: asFunction(makeTodosService).scoped(),
    // We only want a single instance of this
    // for the apps lifetime (it does not deal with user context),
    // so we can reuse it!
    todosRepository: asClass(TodosRepository).singleton()
  })

  return container
}

I'm using container.loadModules, so I was wondering if it's alright to do something like this:

// containerFactory.js

const awilix = require('awilix');

// Default Factory Function
module.exports = () => {
    // Create the DI Container.
    const container = awilix.createContainer();

    // Load modules.
    container.loadModules([
        [
            'custom-exceptions/index.js',
            {
                register: awilix.asClass,
                lifetime: awilix.Lifetime.TRANSIENT,
            }
        ], {
            formatName: 'camelCase'
        }
    ]);

    return container;
};

What I'm worried about, however, is that calling this function in all of my files is going to make Awilix go out and try to autoload all of my dependencies again.

If I have ten other files, and I do this at the top of each file:

const container = require('./containerFactory')();

Will Awilix attempt to register the container ten other times and be extremely latent (if I have hundreds of deps), or will using require make the factory function a singleton (I worry about module caching issues in different scopes), or does Awilix perform its own internal caching of registered dependencies behind the scenes?

Thank you.

jeffijoe commented 5 years ago

I do the same, but I only create a single root container per process. Why would you call it in all of your files?

eithermonad commented 5 years ago

@jeffijoe That's what I'm wondering. It sounded peculiar, which is why I asked.

If I'm using Express, and I have different routes/controllers in different files, is the only way to use Awilix though the awilix-expess middleware? EDIT: Is that where scopePerRequest comes in?

What about requiring the container in different integration test files?

jeffijoe commented 5 years ago

If I'm using Express, and I have different routes/controllers in different files, is the only way to use Awilix though the awilix-expess middleware?

Not at all! At their core, the Express (and Koa, which I would recommend you use instead) Awilix middlewares could be expressed in as little code as this:

function inject(container, handler) {
  return (req, res, next) => container.createScope().build(handler)(req, res, next)
}

However, the Express and Koa bindings have a neat "controller" declaration system which uses a router under the hood and also hides the whole container thing.

What about requiring the container in different integration test files?

If you are using Jest, each file usually gets their own environment (or at least your would-be singletons are run for as many cores you have on your machine, can't remember). Even so, I haven't seen any performance issues.


EDIT: Yeah, the library bindings' scopePerRequest are basically

function scopePerRequest(container) {
  return (req, res, next) => {
    req.container = container.createScope()
    next()
  }
}

So when using inject, it simply uses req.container to build the handler.

eithermonad commented 5 years ago

@jeffijoe Thanks for the information.

All I need is to be able to pull my Service off the container inside the Controller and allow the Service to attain a Repository (again from the Container).

What's the best way of doing that?

I have my own auth middleware that puts a user object onto each Controller for those that need it, so I don't need scopePerRequest.

Here is an example. This is my architecture, so what is the best and cleanest method by which to wire up the container dependencies to all modules that need them?

const app = express();
const container = require('./configureContainer')();

// What do I do with this container inside my app file? How does it get passed to each controller, service, and repository?

// Different file.
class UserRepository {
   constructor({ userModel }) {
       this.userModel = userModel;
   }
   getUser(id) {
      return userModel.findById(id);
   }
}

// Different file.
class UserService {
    constructor({ userRepository }) {
        this.userRepository = userRepository;
    }

    getUser(id) {
        return userRepository.getUser(id);
    }
}

// Different file.
app.get('/user/:id', (req, res) => {
    res.send(userService.getUser(req.params.id));
});

Again, thanks for your time.

jeffijoe commented 5 years ago

I would strongly recommend using scopePerRequest specifically for passing your user to the controllers.

Your middleware would simply be:

function auth(req, res, next) {
  req.container.register('user', asValue(req.user).scoped())
  next()
}

Take a look at this boilerplate, it sounds like it covers everything you want. This bit and this bit might be of interest.

I'm usually lurking in the Koa Slack if you're thinking of making the switch :)

eithermonad commented 5 years ago

@jeffijoe Thank you for the resources. Why do you prefer Koa over Express?

I would strongly recommend using scopePerRequest specifically for passing your user to the controllers.

Not every endpoint requires an authenticated user. Some are publicly accessible.

Take a look at this boilerplate

Will that show how the model gets injected into the repository, how the repository gets injected into the service, and how the service gets injected into the controller, as per my updated comment?

Sorry to bother you with these questions. Thanks for your help and thanks for making Awilix.

jeffijoe commented 5 years ago

Not every endpoint requires an authenticated user. Some are publicly accessible.

Still supported, simply register a default empty user context.

Will that show how the model gets injected into the repository, how the repository gets injected into the service, and how the service gets injected into the controller, as per my updated comment?

That's exactly what it does. πŸ˜„

eithermonad commented 5 years ago

@jeffijoe

Still supported, simply register a default empty user context.

So if I have

GET /products?skip=X&limit=Y - Public POST /products - Protected

inside the same file, and I register with an empty user context, how is my GET Route going to get the empty context and my POST Route going to get the authenticated user object?

EDIT:

In order to put user on req.container, then I would need my authentication middleware to be used before the call to create the scopePerRequest, which means that every subsequent endpoint is going to want an authenticated user, even if those endpoints do not need to be protected.

jeffijoe commented 5 years ago

Use scopePerRequest first. In configureContainer, do the following:

// Always provide a context. Initialize the user to `null`.
// This is available at all times.
container.register('context', asValue({ user: null }).scoped())

Make sure scopePerRequest is added at the top-end of your middleware chain so it runs before all your auth and controller stuff.

In your auth middleware:

function auth(req, res, next) {
  // In fact I would let requests with no auth parameters
  // pass through and make the assertion in my controller.
  req.container.resolve('context').user = req.user
  next()
}

In your controller:

const { NotAuthenticated } =  require('fejl') // shameless plug

class MyController {
  constructor(context, productRepository) {
    this.context = context
    this.productRepository = productRepository
  }

  async createProduct(req, res) {
    // Throw a NotAuthenticated if the user is not set.
    NotAuthenticated.assert(this.context.user, 'You are not authenticated')
    const product = await productRepository.create({
      created_by: this.context.user.id,
      ...req.body
    })
    res.status(200).json(json)
  }
}
jeffijoe commented 5 years ago

I prefer Koa because its' middleware system makes more sense, it's more flexible, it's async, it's leaner and faster. See this. πŸ˜„

Most importantly, error handling is way better.

eithermonad commented 5 years ago

@jeffijoe

Thanks very much for your time. I appreciate it.

I'll do some more work tomorrow with your example and with the boilerplate you sent me. I'll let you know how it goes or if I have more questions (if that's okay?).

Thank you.

jeffijoe commented 5 years ago

Sure! Slack is a better medium for instant communication though. πŸ˜„

eithermonad commented 5 years ago

@jeffijoe I'll sign up to the Slack link you sent.

Pertaining to my auth middleware function, I currently have this: (Not using DI - needs to be refactored.)

const jwt = require('jsonwebtoken');
const User = require('./../models/user');

const auth = async (req, res, next) => {
    try {
        const token = req.header('Authorization').replace('Bearer ', '');
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        const user = await User.findOne({ _id: decoded._id });

        if (!user) {
            throw new Error();
        }

        req.user = user;
        next();
    } catch (e) {
        res.status(401).send({ error: 'Please authenticate.' });
    }
}

module.exports = auth;

And would use it like this:

app.get('/products', (req, res) => res.send());
app.post('/products', auth, (req, res) => res.send());

But, you are saying to do this:

const jwt = require('jsonwebtoken');
const User = require('./../models/user');

const auth = async (req, res, next) => {
   const token = req.header('Authorization').replace('Bearer ', '');
   const decoded = jwt.verify(token, process.env.JWT_SECRET);
   const user = await User.findOne({ _id: decoded._id });

   req.user = req.container.resolve('context').user = user ? user : null;
   next();
}

module.exports = auth;

And then, in a Controller, and suppose I have my own AuthenticationError that extends Error, I'd assert that req.container.user is not null, like this:


class MyController {
    // ctor ...
   createProduct(req, res) {
       if (!req.container.cradle.user) res.status(401).send();

      // ...
   }
}

Since that if statement is going to be reused in many places, can that not be made into its own middleware and be reused in each endpoint that requires protection?

I still don't understand how context and productRepository get injected into MyController, but perhaps I'll understand better if I take a closer look at the boilerplate you sent.

jeffijoe commented 5 years ago

You can do the auth check however you want. DRY shouldn't be applied relentlessly, sometimes it makes more sense to have some composable one-liners so you can glance at a method and see how it flows as opposed to relying on some auth middleware defined elsewhere. Also, it doesn't have to be middleware! It could be as simple as

function requireAuth(context) {
  if (!context.user) { throw new AuthError() }
  return context.user
}

class Ctrl {
  // ctor..

  async doStuff(req, res) {
    if (req.params.something) {
      const user = requireAuth(this.context)
      // Do something with user
    } else {
      // Dont require auth here maybe
    }
  }
}

The advantage is also to do selective auth. Like "if this parameter is specified, require that the user is authenticated".

Don't reference the container in your controller, inject the dependencies using awilix-express. I recommend using loadControllers.

In general, try to make HTTP (Express, Koa) an implementation detail of the outer "layer". If you some day want to expose your app over Websockets or TCP, you only need to write an "adapter" that translates websocket messages into your service calls. I've done this in practice where I have a service exposed over HTTP as well as Websocket, where the service was using .scoped() lifetime and would create an in-memory cache that would be scoped to the request, but when using websockets it would be scoped to the websocket connection.

eithermonad commented 5 years ago

@jeffijoe Thank you.

I understand. The Controller layer should be very thin, sitting on top, and should only handle I/O of data. It also should never pass a req or res object directly to the service because then you would be coupling the service with either the HTTP Framework or the WebSocket implementation, etc.

I'll look into the loadControllers function in the docs for awilix-express - it sounds like what I'm looking for in terms of injecting services and repositories.

Thanks.

eithermonad commented 5 years ago

@jeffijoe

NOTE: See additional edits at the bottom.

I'm still a little stuck over what the best way to accomplish injection is. If you have time, I'd really appreciate it if you could help with this last question. I've attempted to show an extremely simple use case just to help me get my head around how Awilix and awilix-express works. The comments should show the questions I have. It would be helpful if you could show how this would be refactored to use Awilix.

Thanks again for your time.

/* ////////// Products ////////// */

// File: ProductService.js
class ProductService {
  constructor({ productRepository }) { /* ... */ }
  getProducts(limit, skip) {
    return productRepository.getAll(limit, skip);
  }
  postNewProduct(productInfo) {
    return productRepository.save(productInfo);
  }
}

// File: ProductRepository.js
class ProductRepository {
  constructor({ productModel }) { /* ... */ }
  getAll(limit, skip) {
    return productModel.find().paginate(limit, skip);
  }
  save(productInfo) {
    return productModel.save(productInfo);
  }
}

/* ////////// Users ////////// */

// File: UserService.js
class UserService {
  constructor({ userRepository }) { /* ... */ }
  signUpNewUser(userInfo) {
    return userRepository.save(userInfo);
  }
}

// File: UserRepository.js
class UserRepository {
  constructor({ userModel }) { /* ... */ }
  save(userInfo) {
    return userModel.save(userInfo);
  }
}

/* ////////// Routes ////////// */

// File: products.js
const router = new express.Router();

router.get('/products', (req, res) => {
  // How do I get productService into this endpoint?
  const products = productService.getAll(10, 0);
  res.send(products);
});

router.post('/products', auth, (req, res) => {
  // How do I handle authentication in this endpoint?
  // How do I get productService in this endpoint?
  const product = productService.postNewProduct(req.product);
  res.send(product);
});

module.exports = router;

// File: user.js
const router = new express.Router();

router.post('/users', (req, res) => {
  // How do get userService in this endpoint?
  const user = UserService.signUpNewUser(req.user);
  res.send(user);
});

router.get('/users/me', auth, (req, res) => {
  // req.user is currently coming from my auth middleware. This should use the container instead, right?
  res.send(req.user);
});

module.exports = router;

/* ////////// Auth Middleware ////////// */

// auth.js
const auth = async (req, res, next) => {
    try {
        // The Authorization Bearer Token sent in the header of the request needs to be decoded.
        const token = req.header('Authorization').replace('Bearer ', '');
        const decoded = await admin.auth().verifyIdToken(token);

        // Finding that user in the database by their Firebase UID.
        const user = await User.findOne({ _id: decoded.uid });

        if (!user) {
            throw new Error();
        }

        // Making the user accessible to the endpoint.
        req.user = user;

        // Proceed
        next();
    } catch (e) {
        // HTTP 401 Unauthorized Response Status
        res.status(401).send({ error: 'Please authenticate.' });
    }
}

module.exports = auth;

EDIT:

I've been reading the docs again and testing with cURL, and it seems that I can just do this:

const express = require('express');
const awilix = require('awilix');
const { scopePerRequest } = require('awilix-express');

const app = express();

// This would be in a loader for awilix.
const UserService = require('./services/UserService');
const UserRepository = require('./repositories/UserRepository');
const UserModel = require('./models/user');

// Would be inside configureContainer factory function.
const container = awilix.createContainer();

container.register({
    userService: awilix.asClass(UserService),
    userRepository: awilix.asClass(UserRepository),
    userModel: awilix.asValue(UserModel)
});

// App middleware.
app.use(scopePerRequest(container))

// Routes file.
app.post('/users', (req, res) => {
    const user = { username: 'Jamie' };

    req.container.cradle.userService.signUpNewUser(user);

    res.send('Complete');
});

// server.js
app.listen(3000, () => console.log('Server is up'));

I didn't know that Awilix would automatically be able to inject dependencies into other dependencies via destructuring, so it makes sense now.

With that said, you mentioned not to use req.container.cradle inside of an endpoint, so I'm still unsure what the best way is to actually resolve dependencies inside of my enpoints.

jeffijoe commented 5 years ago

Keep using scopePerRequest, then use inject for the simplest way.

Example:

const { inject } = require('awilix-express')
// Routes file.
app.post('/users', inject(({ userService }) => (req, res) => {
    const user = { username: 'Jamie' };

    userService.signUpNewUser(user);

    res.send('Complete');
}));

inject will use the req.container that scopePerRequest configures to inject dependencies into a request handler.

For auth, you can do:

const auth = async (req, res, next) => {
    try {
        // The Authorization Bearer Token sent in the header of the request needs to be decoded.
        const token = req.header('Authorization').replace('Bearer ', '');
        const decoded = await admin.auth().verifyIdToken(token);

        // Finding that user in the database by their Firebase UID.
        const user = await User.findOne({ _id: decoded.uid });

        if (!user) {
            throw new Error();
        }

        // Making the user accessible to the endpoint.
        req.user = user;
        // Register the user in the container so we can inject it into services
        req.container.register('user', asValue(user))
        // Proceed
        next();
    } catch (e) {
        // HTTP 401 Unauthorized Response Status
        res.status(401).send({ error: 'Please authenticate.' });
    }
}

Now you can depend on user in your service, given that you've registered it with scoped lifetime (hence scopePerRequest πŸ˜„ )

eithermonad commented 5 years ago

@jeffijoe Thank you very much. I understand now.

Is it possible to use inject as global middleware in any way so that I don't have to use it in every single endpoint?

With the auth example, do I still use auth inside each endpoint, i.e, app.get('/protected', auth, inject ...);?

jeffijoe commented 5 years ago

Is it possible to use inject as global middleware in any way so that I don't have to use it in every single endpoint?

This is what the controller is for in awilix-express and awilix-koa. πŸ˜„

With the auth example, do I still use auth inside each endpoint, i.e, app.get('/protected', auth, inject ...);?

Yes.

eithermonad commented 5 years ago

@jeffijoe Thank you.

Also, will Awilix overwrite files of the same name? That is, if I attempt to autoload:

src/api/routes/user.js,
src/models/user.js,
...

will the route be overwritten by the model?

jeffijoe commented 5 years ago

Yes it will overwrite. You can customize how the registration name is generated from the file path though. See formatName in the docs. Example:

// to customize how modules are named in the container (and for injection)
container.loadModules(['repository/account.js', 'service/email.js'], {
  // This formats the module name so `repository/account.js` becomes `accountRepository`
  formatName: (name, descriptor) => {
    const splat = descriptor.path.split('/')
    const namespace = splat[splat.length - 2] // `repository` or `service`
    const upperNamespace =
      namespace.charAt(0).toUpperCase() + namespace.substring(1)
    return name + upperNamespace
  }
})
eithermonad commented 5 years ago

@jeffijoe I see, thanks. Or, perhaps I could just name them as user.service.js, and user.route.js, and user.repository.js, etc..

I think I prefer your method, however.

Sorry for asking all of these questions - this is my first time using Dependency Injection. I've used Express many, many times, but never DI nor Awilix with Express, so, as you might imagine, I'm quite confused. I think I'm getting there, however. Thanks.

jeffijoe commented 5 years ago

No problem! Did you read my Medium series on DI with Awilix? It should explain a lot of this.

https://link.medium.com/LfcVPai9CX

eithermonad commented 5 years ago

Use scopePerRequest first. In configureContainer, do the following:

// Always provide a context. Initialize the user to `null`.
// This is available at all times.
container.register('context', asValue({ user: null }).scoped())

Make sure scopePerRequest is added at the top-end of your middleware chain so it runs before all your auth and controller stuff.

In your auth middleware:

function auth(req, res, next) {
  // In fact I would let requests with no auth parameters
  // pass through and make the assertion in my controller.
  req.container.resolve('context').user = req.user
  next()
}

In your controller:

const { NotAuthenticated } =  require('fejl') // shameless plug

class MyController {
  constructor(context, productRepository) {
    this.context = context
    this.productRepository = productRepository
  }

  async createProduct(req, res) {
    // Throw a NotAuthenticated if the user is not set.
    NotAuthenticated.assert(this.context.user, 'You are not authenticated')
    const product = await productRepository.create({
      created_by: this.context.user.id,
      ...req.body
    })
    res.status(200).json(json)
  }
}

This comment you sent a few hours ago ^.

My main point of misunderstanding right now is how productRespository will get into MyController - i.e, how is that set up on the Express side in my app.js/server.js file.

jeffijoe commented 5 years ago

That happens when resolving the controller which you do with inject

eithermonad commented 5 years ago

No problem! Did you read my Medium series on DI with Awilix? It should explain a lot of this.

https://link.medium.com/LfcVPai9CX

I did. I think I should read the last article a second time, however.

They were very nice and well-written articles, by the way. Not many people can inject (pardon the pun) humor into the text that way and still manage to write something informative. They are some of the best I've seen on Medium.

jeffijoe commented 5 years ago

Haha, happy to hear it!

eithermonad commented 5 years ago

That happens when resolving the controller which you do with inject

Right, so how do I use inject in that way but globally, such that I don't have to manually write inject into each endpoint?

jeffijoe commented 5 years ago

By letting awilix-express handle the routing. See the β€œnew in v1” section in the docs.

eithermonad commented 5 years ago

By letting awilix-express handle the routing. See the β€œnew in v1” section in the docs.

How do I selectively choose which routes have auth middleware when doing it that way?

jeffijoe commented 5 years ago

Using before - see the Awesome Usage section.

But again, I would always run the auth middleware and leave it up to the service to determine whether auth is required. It gives you more flexibility, like auth is only required if the product is marked private.

eithermonad commented 5 years ago

Using before - see the Awesome Usage section.

I had thought that - I must have read the entire docs five times now. What about file uploading with Multer? Can I use before in different places or will each before affect all routes beneath it, similar to fall through?

But again, I would always run the auth middleware and leave it up to the service to determine whether auth is required. It gives you more flexibility, like auth is only required if the product is marked private.

In which case, I might have two middleware functions - an attachUser to register a user object into the container, whether it's null or not, and a verifyAuth to check whether user is null, and if so, return 401.

Is there not any way to inject deps into endpoints without having to make controller classes/factories and without having to do it manually for every endpoint?

Thanks.

jeffijoe commented 5 years ago

You can use before per route and per controller.

You can just pull dependencies out of req.container.cradle but I don’t see why you wouldnt just use the controllers feature or even just plain inject?

What if you want to verify auth based on some attributes from a product you have to fetch? Adding a verifyAuth middleware would not give you that flexibility, and you are coupling your authz to HTTP.

eithermonad commented 5 years ago

@jeffijoe

I'll just use inject manually for now. I'm on a timeline with regards to completing this project, so I'll spend more time learning awilix-express and refactor later to use a more maintainable pattern.

I agree with you about Auth and HTTP. To decouple them, I have my own AuthenticationError that extends Error, and my auth middleware throws that error. That doesn't, however, abstract away the process of verifying the JWT Bearer Token from the Authorization Header. I do use an Adapter to simplify the process, however.

I use (express-async-errors)[https://www.npmjs.com/package/express-async-errors] to allow me to catch errors inside of a bottom app.use middleware function. That middleware function looks at the type of error, and if it's one of the errors I created, it pulls that error message and status code off that error object and sends it to the client, otherwise, the client gets a 500.

Thanks very much for your help. I apologize for taking up so much of your time.

eithermonad commented 5 years ago

@jeffijoe Just to check, should I load all my third-party dependencies into the Container as well, such as the Node AWS SDK, the Firebase Admin SDK, the UUID Library, etc.?

jeffijoe commented 5 years ago

Only if you think you're getting value from that. I inject SDK's (because they require configuration) but not simple utilities like UUID.

eithermonad commented 5 years ago

@jeffijoe Thank you.

The only thing would be, with UUID, for example, that I could mock it to always give me the same random ID so that tests can be easier.

I suppose, however, that a Jest Manual Mock would do just fine.

In terms of injecting the Firebase Admin SDK or the Node AWS SDK, again, I can mock then manually with Jest, but I'm not sure if that goes against DI principals. Registering them in the container, however, might present problems with ensuring they are fully configured prior to said registration.


From: Jeff Hansen notifications@github.com Sent: Wednesday, June 19, 2019 1:30:18 AM To: jeffijoe/awilix Cc: Jamie Corkhill; Author Subject: Re: [jeffijoe/awilix] How should I configure the container? (#137)

Only if you think you're getting value from that. I inject SDK's (because they require configuration) but not simple utilities like UUID.

β€” You are receiving this because you authored the thread. Reply to this email directly, view it on GitHubhttps://github.com/jeffijoe/awilix/issues/137?email_source=notifications&email_token=AG2ZVR7GFWZ36XCRLJWSPZDP3HG7VA5CNFSM4HY37TCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODYA2W2A#issuecomment-503425896, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AG2ZVR3SUNHQKHDFXCJPEULP3HG7VANCNFSM4HY37TCA.

jeffijoe commented 5 years ago

Well if you already have a DI setup, if you do need to mock UUID then go ahead and inject UUID. You shouldn't have to use Jest module mocks for anything.

Injecting the SDKs, again, if you intend to mock them, sure. If not, do whatever you feel is more pragmatic. πŸ˜„

eithermonad commented 5 years ago

@jeffijoe

Alright - I suppose, then, in a Unit Test, I could register a new Container and overwrite with mocks, whereas, in an Integration Test, I could call the containerFactory again from earlier to get everything. You also said it was okay to register a new container with hundreds of dependencies in each suite without a performance loss, I believe.

Additionally, if I wanted to use the same Mock across Unit Test Suites, then I could write the mock as a fixture, export it, and register a new container in one of the before hooks in Jest.

Indeed, I define Adapters for my SDKs, so perhaps I could just do something like this:

// Amazon Web Services and Amazon Web Services Mock
const aws = require('./../../config/aws/aws');
const awsMock = require('mock-aws-s3');

const FileCRUDAdapter = require('./FileCRUDAdapter');

const { cloudStorageConfig } = require('./../../config/config');

if (process.env.MODE === 'test') awsMock.config.basePath = '/tmp/buckets';

const s3 = process.env.MODE === 'test' ? (
    awsMock.S3({ params: { Bucket: cloudStorageConfig.buckets.testing.name }})
) : (
    new aws.S3()
);

// FileCRUDAdapterFactory (function)
module.exports = (bucket = undefined) => new FileCRUDAdapter(s3, bucket);

Also, I have a factory function that creates a MongoDB Connection. I don't believe that needs to use DI, right, because I only require it in one file and call the factory before the Express Routes are set up, so I think that's fine to leave the way it is.

eithermonad commented 5 years ago

And thanks, by the way.

jeffijoe commented 5 years ago

TBH your unit tests should not need to use a container, you should just construct your SUT (Subject Under Test) normally and provide whatever mocks you need.

I usually create my DB connections in configureContainer (see my Koa boilerplate), register them with Awilix and let Awilix dispose them on shutdown (container.dispose()).

eithermonad commented 5 years ago

@jeffijoe

I see. Thanks. So it would be normal for the file that houses the configureContainer factory function to have many require statements when it comes to third-party NPM Modules if and only if those modules are being mocked?

jeffijoe commented 5 years ago

Don't have to be mocked, if it just makes sense to inject a "prepared" module (like an SDK) then you can. If you'd rather just import the module directly because it is part of the implementation and is not something you'd want to mock, just import it.

So it would be normal for the file that houses the configureContainer factory function to have many require statements

Absolutely! In fact that's the point! It's your composition root.

eithermonad commented 5 years ago

@jeffijoe Great! Thanks.

So would this setup be considered normal? I don't want any calls to the Firebase Servers to initialize the application to occur, however, in test mode. So perhaps I make the firebase.js file below return factory function, and inject admin based on the environment.

config/firebase/firebase.js:

const admin = require('firebase-admin');

admin.initializeApp({
    credential: admin.credential.cert(process.env.GOOGLE_APPLICATION_CREDENTIALS),
    databaseURL: "[URL]"
});

module.exports = admin;

Composition Root:

const awilix = require('awilix');

// Third-party pre-configured SDKs:
const admin = require('./../config/firebase/firebase');

// Default Factory Function
module.exports = () => {
    // Create the DI Container.
    const container = awilix.createContainer({
        injectionMode: awilix.InjectionMode.CLASSIC
    });

    // Load modules.
    container.loadModules([
        [
            '../custom-exceptions/index.js',
            {
                register: awilix.asValue,
                lifetime: awilix.Lifetime.TRANSIENT,
            }
        ]
    ],  {
            cwd: __dirname,
        })
        .register({
            admin: awilix.asValue(admin) // RIGHT HERE
        });

    return container;
};

An Adapter getting the SDK injected:

module.exports = class AdminAdapter {
    constructor({ admin, AuthenticationError }) {
        // Dependency Injection
        this.admin = admin; 
        this.AuthenticationError = AuthenticationError; // A custom exception.
    }

    async verifyAuthToken(token) {
        try {
            const decoded = await this.admin.auth().verifyIdToken(token);
            return decoded;
        } catch (err) {
            throw new this.AuthenticationError();
        }
    }
}

Is it alright to be asking questions here like this, or am I taking up too much of your time?

Thanks.

jeffijoe commented 5 years ago

Yeah that looks reasonable, although not sure why you are injecting the error classes though, and also asValue does not use lifetime. πŸ˜€

Its fine, I don’t mind helping out, but it would be easier on the Koa Slack (and you really should consider Koa 😁)

eithermonad commented 5 years ago

@jeffijoe I will consider Koa. This project is for a startup, I'm the only developer, and I'm on a 1.5-month timeline for getting an MVP in front Investors, so, for now, I'll stick with Express since I know it best. Eventually, I'll be refactoring my codebase to use TypeScript instead of JavaScript, and to use PostgreSQL instead of MongoDB, so I'll ponder a possible Koa migration then.

For Slack, do you mean the Private Messaging feature and that we can use that to talk?

jeffijoe commented 5 years ago

Yeah, or you can ask your general design-related questions in the general chat and others can chime in too.

eithermonad commented 5 years ago

@jeffijoe Alright. Thanks. I'll see you there.