get-in-di v1.2.4
Get In DI
A DI container for node (>= 10).
There are three concepts: definitions, references, and containers.
Definitions wrap values and provides methods for initialising classes, and creating factories, and singletons, References allow definitions to be shared, and a container holds it together.
Install
$ npm i get-in-diOr
$ yarn add get-in-diA contrived example
// Create a container
const container = new Container();
// We want to store some config in our container.
container.add('db_user', process.env.DB_USER);
container.add('db_pass', process.env.DB_PASS);
// set up a db connection
container.add('connection', new Definition(require('db-lib')))
// construct our connection
.construct([
new ReferenceObject({
url: 'url_to_db_1',
// use References to configure the constructor args
user: new Reference('db_user'),
password: new Reference('db_pass'),
})
])
// our db lib doesn't use promises so lets sort that out.
.decorate((connection) => {
const { promisify } = require('util');
connection.query = promisify(connection.query);
});
// set up an app with the connection
container.add('app', new Def(App))
.construct([new Reference('connection')]);
// Get the app... this will create our connection before
// injecting it into our app.
container.get('app');Recipes
add and get a value
container.add('foo', new Definition('bar'));
container.get('foo') // => 'bar'Call a function
container.add('cache', new Definition(redis.create))
.call([{
host: '127.0.0.1',
port: '6379'
}]);.call tells the definition to call the value using an optional array param as the argument list for the definition call.
Constructing a class
Here's the class were going to use:
class Counter {
constructor(count = 0) {
this.count = count;
this.incrementAmount = 1;
}
increment() {
this.count += this.incrementAmount;
}
}It's easy to add a definition for the class...
container.add('counter', new Definition(Counter));
container.get('counter') // => [Function: Counter]...but if we call get now the container will just return our class. We need tell the definition to create a new instance.
container.add returns the value that was added. So here we can user the Definition method construct to make the definition resolve its value to an instance of Counter:
container.add('counter', new Definition(Counter))
.construct();
container.get('counter') // => Counter { count: 0, incrementAmount: 1 }This is great! But what if we want to pass some values to the constructor? We can do this by passing a array of arguments to definition.construct. This can be a value, or a Reference. (In fact the argument array can be replaced by a Reference).
container.add('counter', new Definition(Counter))
.construct([new Reference('initialCount')]);
container.add('initialCount', 100);
container.get('counter') // => Counter { count: 100, incrementAmount: 1 }Note: We can define initialCount after counter because the counter isn't initialised until container.get('counter') is called.
We can also call methods and set properties on a Definition value:
container.add('counter', new Definition(Counter))
.construct([new Reference('initialCount')])
.callMethod('increment')
.setProp('incrementAmount', 10)
.callMethod('increment')
container.add('initialCount', 100);
container.get('counter') // => Counter { count: 111, incrementAmount: 10 }construct, callMethod, setProp, and decorate (which we haven't covered here) are called in order and can be called on any definition value; so dont call construct on a number definition!
Singleton vs Factory
We can set isShared to control how the behaviour of definitions.
In this example Math.random will be called once the first time shared_random is accessed. The the random value is then stored for each subsequent get:
container.add('shared_random', new Definition(Math.random))
.call(); // we need this to tell the Definition to run Math.random instead of return it
container.get('shared_random') // => 0.6240852289721388;
container.get('shared_random') // => 0.6240852289721388;By setting isShared to false we're telling the definition to call Math.random each time random_factory is accessed.
container.add('random_factory', new Definition(Math.random, { isShared: false }))
.call();
container.get('random_factory'); // => 0.5616454027240336
container.get('random_factory'); // => 0.48624780071259344Using Objects
Calling functions with argument lists is all well and good, but a lot of the time functions and classes require and object for one or more parameters. To inject values into these we need to use a ReferenceObject.
A ReferenceObject takes an object where some of it's value are instance of Reference. When it is resolved it returns a new object with the references resolved and the remaining properties shallowly copied across.
// add some values to our container
container.add('user', 'root');
container.add('password', 'lolcatz');
container.add('logFile', new Definition(LogFile))
.construct('./log.log');
container.add('db', new Definition(DB))
.construct([
new ReferenceObject({
user: new Reference('user'),
password: new Reference('password'),
// References in arrays will also be resolved
logFiles: [
new Reference('logFile'),
]
})
]);
container.get('db') // => DB { user: 'root', password: 'lolcatz', logFiles: [LogFile] }You can also use Definition.resolveProps to resolve all properties any value. This is more useful if you're not dealing with an object for some reason...
container.add('string', 'hello');
container.add('number', 123);
container.add('thing', new Definition({
foo: new Reference('string'),
bar: new Reference('number'),
}))
.resolveProps();
container.get('thing') // => { foo: "hello", bar: 123 }Definition.resolveProps can be chained like any other definition action.
Shorthand class names
We export short hand class names for less typing:
| Class | Shorthand |
|---|---|
| Definition | Def |
| Reference | Ref |
| ReferenceObject | RefObj |