https://github.com/mcasimir/bootwire
Application and dependencies bootstrap for node.js
https://github.com/mcasimir/bootwire
boot dependency-injection ioc nodejs
Last synced: 2 months ago
JSON representation
Application and dependencies bootstrap for node.js
- Host: GitHub
- URL: https://github.com/mcasimir/bootwire
- Owner: mcasimir
- Created: 2017-07-02T18:47:39.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2018-05-17T11:53:51.000Z (about 7 years ago)
- Last Synced: 2025-01-28T03:17:39.947Z (4 months ago)
- Topics: boot, dependency-injection, ioc, nodejs
- Language: JavaScript
- Homepage:
- Size: 94.7 KB
- Stars: 2
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# bootwire
[](https://travis-ci.org/mcasimir/bootwire) [](https://codecov.io/gh/mcasimir/bootwire)
Application and dependencies bootstrap for node.js.
``` sh
npm install --save bootwire
```## A super-minimal way to boot and compose application dependencies using ES6.
Bootwire is a very simple library that leverages ES6 destructuring
to provide a _no-black-magick-please_ way to boot node js applications
and perform dependency injection.## Features
- Support asynchronous boot (ie. wait for database connection to be ready).
- Simplify tests allowing to mock parts of your application without `stub` and `spies`.
- Predictable top-down flow.
- IoC with plain old functions and constructors.
- No `require` hijacking.
- No `module.exports['@something-magick-and-weird-here']`
- Wire components together not to the IoC container- [Getting Started](#getting-started)
+ [Dependency injection](#dependency-injection)
- [Usage patterns for complex applications](#usage-patterns-for-complex-applications)
+ [Split bootstrap into phases](#split-bootstrap-into-phases)
+ [Top Down $wireGlob](#top-down-wireglob)
+ [Bootstrap of many components](#bootstrap-of-many-components)
+ [Wiring classes and services](#wiring-classes-and-services)
- [Api](#api)
* [Classes](#classes)
* [Functions](#functions)
* [App : Object](#app--object)
+ [app.boot(...initialContext) ⇒ Promise](#appbootinitialcontext-%E2%87%92-promise)
* [Context : Object](#context--object)
+ [context.$context ⇒ [Context](#Context)](#contextcontext-%E2%87%92-context%23context)
+ [context.$set(keyOrObject, value)](#contextsetkeyorobject-value)
+ [context.$provide(key, fn) ⇒ Promise](#contextprovidekey-fn-%E2%87%92-promise)
+ [context.$wire(...fns) ⇒ Promise](#contextwirefns-%E2%87%92-promise)
+ [context.$wireGlob(...patterns) ⇒ Promise](#contextwireglobpatterns-%E2%87%92-promise)
+ [context.$waitFor(...keys) ⇒ Promise](#contextwaitforkeys-%E2%87%92-promise)
+ [context.$get(key, [defaultValue]) ⇒ Any](#contextgetkey-defaultvalue-%E2%87%92-any)
* [bootwire(bootAndWireFn) ⇒ [App](#App)](#bootwirebootandwirefn-%E2%87%92-app%23app)## Getting Started
Bootwire provides a way to create an **application context** and pass it down to a **boot procedure**.
The __context object__ is just an object that exposes a few methods to manipulate and use it:
- `$set`: set one or many properties
- `$provide`: set a property to the result of the invocation of a provider function.
- `$wire`: wire a function passing the context as parameter
- `$wireGlob`: wire any files matching a glob pattern
- `$waitFor`: await for specific dependencies to be wired
- `$get`: get a value in the context by key or by pathUsing `$set` and `$provide` on the context object will ensure that all of its properties **will be only set once**, allowing to inject providers, services connections, configs and so on during tests.
The __boot procedure__ to which the context is passed is a function that acts as the single starting point of an application.
#### Dependency injection
As opposed to many IoC containers `bootwire` takes a radical approach to handle dependencies:
- Dependencies resolution is not lazy: all of the components are wired together during the boot phase.
- Dependency injection follows one and only one simple rule: if a dependency is already set it will not be set again.Which result into an extremely simple way to replace a component or a setting during tests: just set it before the boot phase.
``` js
// index.jsrequire('./app').boot().catch(console.error);
`````` js
// app.jsconst bootwire = require('bootwire');
function bootProcedure({$provide, $set, $wire} /* this is the context object destructured */) {
$set({
logger: require('winston'),
config: require('./config')
});await $provide('db', async function({config}) {
return await MongoClient.connect(config.mongodbUrl);
});await $provide('userRepository', async function({db}) {
return new UserRepository({db});
});await $wire(startExpress);
}module.exports = bootwire(bootProcedure);
`````` js
// user.routes.jsmodule.exports = function({router, userRepository}) {
router.get('/users', async function(req, res) {
res.json(await userRepository.find());
});};
```Integration tests are now extremely easy:
``` js
// app.spec.js
const app = require('./app');it('does something', async function() {
await app.boot({
config: {port: await randomAvailablePort()},
db: fakeMongodb // db will not be set during the boot
// since is already set here
});// ...
});
```And unit tests as well:
``` js
const UserRepository = require('./services/UserRepository');it('retrieves all the users', async function() {
const repo = new UserRepository({db: {
find() {
return Promise.resolve(usersFixture);
}
}});deepEqual(await repo.find(), expectedUsers);
});
```The boot procedure also accepts multiple initial contexts that will be merged
together, doing so will be easy to $provide a default initial context on each tests
and override it on each test case:``` js
// app.spec.js
const app = require('./app');const defaultTestContext = {
config: defaultConfig
};it('does something', async function() {
await app.boot(defaultTestContext,
{
config: {port: await randomAvailablePort()},
db: fakeMongodb
}
);// ...
});
```## Usage patterns for complex applications
#### Split bootstrap into phases
``` js
// ./boot/index.jsconst {promisify} = require('util');
const express = require('express');
const winston = require('winston');module.exports = async function({$wireGlob, $set, $context}) {
const config = require('./config');
const app = express();
const logger = winston;$set({
config,
app,
logger
});await $wireGlob('./services/**/*.wire.js');
await $wireGlob('./middlewares/**/*.wire.js');
await $wireGlob('./routes/**/*.wire.js');await promisify(app.listen)(config.port);
logger(`Application running on port ${config.port}`);
};
```#### Top Down $wireGlob
`$wireGlob` never process a file twice and ensure files are always processed in
depth order from the most generic path to the deepest.It can be leveraged to delegate complex wiring from a general boot file to more
specialized procedures.``` js
// ./index.jsconst {promisify} = require('util');
const express = require('express');module.exports = async function({$wireGlob, $set, $context}) {
const app = express();$set({
app
});await $wireGlob('./routes/**/*.wire.js');
await promisify(app.listen)(config.port);
};
`````` js
// ./routes/wire.jsmodule.exports = async function({$wireGlob, $set, $context}) {
await $wireGlob('./middlewares/**/*.middeware.js');
await $wireGlob('./api/**/wire.js'); // NOTE: this will be processed only once
// and from this file even if the path
// matches also the glob from the call in
// ./index.js
};
```#### Bootstrap of many components
Using `$wireGlob` and `$waitFor` is possible to create self contained modules that
can be wired together without having a main boot procedure knowing about everything.``` js
// ./boot/index.jsmodule.exports = async function({$wireGlob}) {
await $wireGlob('./*.wire.js');
};
`````` js
// ./boot/logger.wire.jsmodule.exports = async function({$waitFor, $set}) {
const {correlator} = await $waitFor('correlator');$set('logger', new CorrelationLogger(correlator));
};
`````` js
// ./boot/correlator.wire.jsmodule.exports = function({$set}) {
$set('correlator', new ZoneCorrelator());
};
```#### Wiring classes and services
One way to perform IoC without any magic container is to use explicitly the
constructor of services to inject dependencies.Although it may seem a tight constraint it is actually a good way to create
independent components that are easy to reuse in different context
and applications.This explicit and __manual__ injection is intended and is necessary to achieve one of
the goal of `bootwire`: don't require components to depend on the dependency injection
framework.``` js
// services/UserRepository.jsclass UserRepository {
constructor({db}) {
this.collection = db.collection('users');
}find() {
return this.collection.find().toArray();
}
}
```Note how the `UserRepository` class is completely usable both with `bootwire`:
``` js
// boot/index.jsmodule.exports = function({$provide}) {
await $provide('db', async function({config}) {
return await MongoClient.connect(config.mongodbUrl);
});await $provide('userRepository', async function({db}) {
return new UserRepository({db});
});
};
```And without `bootwire`:
``` js
// tasks/dumpUsers.jsasync main() {
const db = await MongoClient.connect(process.env.MONGODB_URL);
const repo = UserRepository({db});
const users = await repo.find();
console.info(JSON.stringify(users, null, 2));
}main().catch(console.error);
```## Api
### Classes
-
App :Object
-
App is a bootable application.
-
Context :Object
-
Context
is the main application context object. It acts as dependency
container and is intended to be passed down through all the initialization
procedure.
### Functions
-
bootwire(bootAndWireFn) ⇒App
-
Build a new App that will use invoke the boot and $wire procedure passed
as parameter on boot.Example usage:
const bootwire = require('bootwire');
const app = bootwire(require('./src/boot'));if (require.main === module) {
app.boot()
.catch((err) => {
// Something extremely bad happened while booting
console.error(err);
process.exit(1);
});
}module.exports = app;
Example tests:
const app = require('../..');
describe('app', function() {
it('runs', async function() {
const port = await getRandomPort();await app.boot({
config: { port }
});await request('http://localhost:${port}/health');
// ...
});
});
### App : Object
App is a bootable application.
#### app.boot(...initialContext) ⇒ Promise
Start an application with an initialContext
**Kind**: instance method of [App
](#App)
**Returns**: Promise
- A promise resolving to Context when the boot procedure will complete.
| Param | Type | Description |
| --- | --- | --- |
| ...initialContext | Object
| One or more object to be merged in the context and build the initialContext. Note that any function already present in the prototype of Context (ie. $wire, $set, $provide) will NOT be overriden. |
### Context : Object
`Context` is the main application context object. It acts as dependency
container and is intended to be passed down through all the initialization
procedure.
**Kind**: global class
* [Context](#Context) : Object
* [.$context](#Context+$context) ⇒ [Context
](#Context)
* [.$set(keyOrObject, value)](#Context+$set)
* [.$provide(key, fn)](#Context+$provide) ⇒ Promise
* [.$wire(...fns)](#Context+$wire) ⇒ Promise
* [.$wireGlob(...patterns)](#Context+$wireGlob) ⇒ Promise
* [.$waitFor(...keys)](#Context+$waitFor) ⇒ Promise
* [.$get(key, [defaultValue])](#Context+$get) ⇒ Any
#### context.$context ⇒ [Context
](#Context)
Returns the same context instance.
Useful in factory and provider functions to destructure both the context
and its internal properties.
ie.
``` js
module.exports = function setupRoutes({app, context}) {
// NOTE: config === context.config
app.get('/users', require('./users.routes')(context));
}
```
**Kind**: instance property of [Context
](#Context)
**Returns**: [Context
](#Context) - the context object itself
#### context.$set(keyOrObject, value)
`$set` sets one or more keys in the context if they are not already present.
ie.
``` js
$set('logger', winston);
```
``` js
$set({
config: require('./config'),
logger: winston
});
```
**Kind**: instance method of [Context
](#Context)
| Param | Type | Description |
| --- | --- | --- |
| keyOrObject | String
\| Object
| a string key in case of single assignment or a key-value map in case of multiple assignment. |
| value | Any
| the value to be assigned in case a string key is provided. |
#### context.$provide(key, fn) ⇒ Promise
`$provide` allows to assign to a contpext key the result of a function (provider)
that is invoked with context as parameter.
If the context key is already taken the `$provide` returns without doing
anything.
The function to be evaluated can be synchronous or asynchronous. In either
cases `$provide` returns a Promise to wait for to be sure the assignment took
place (or has been rejected).
**Kind**: instance method of [Context
](#Context)
**Returns**: Promise
- a promise that will be resolved once `$provide` has completed the
assignment or refused to assign.
| Param | Type | Description |
| --- | --- | --- |
| key | String
| the key to be assigned |
| fn | function
| the function to be evaluated. Context will be passed as param to this function. |
#### context.$wire(...fns) ⇒ Promise
`$wire` invokes one or more asynchronous function passing the context as first parameter.
**Kind**: instance method of [Context
](#Context)
**Returns**: Promise
- a promise that will be resolved once `fn` will complete.
| Param | Type | Description |
| --- | --- | --- |
| ...fns | function
| the function or functions to be evaluated. Context will be passed as param. |
#### context.$wireGlob(...patterns) ⇒ Promise
`$wireGlob` requires and wires files by patterns from the caller folder.
ie.
``` js
await $wireGlob('routes/*.wire.js');
```
**Kind**: instance method of [Context
](#Context)
**Returns**: Promise
- A promise that will be resolved once all the files are required
and wired
| Param | Type | Description |
| --- | --- | --- |
| ...patterns | String
| One or more pattern expression (see https://github.com/isaacs/minimatch#usage for help) NOTE: path patterns are relative to the caller file and not to `process.cwd()` |
#### context.$waitFor(...keys) ⇒ Promise
`$waitFor` wait for the resolution of the dependencies passed as argument and
then it returns the context;
```
const {logger} = await $waitFor('logger');
```
**Kind**: instance method of [Context
](#Context)
**Returns**: Promise
- A promise resolving to the context once all the dependencies
are ready
| Param | Type | Description |
| --- | --- | --- |
| ...keys | String
| A list of dependencies to be awaited |
#### context.$get(key, [defaultValue]) ⇒ Any
Get a value from context by key or path.
``` js
const context = await app.boot();
const port = context.get('config.port');
const info = await request(`http://localhost:${port}/api/info`);
// ...
```
**Kind**: instance method of [Context
](#Context)
**Returns**: Any
- the value if found or `defaultValue`.
| Param | Type | Description |
| --- | --- | --- |
| key | String
| a single key or a path of the form 'key1.key2.key3'. |
| [defaultValue] | Any
| a value to be returned if the key is not found. |
### bootwire(bootAndWireFn) ⇒ [App
](#App)
Build a new App that will use invoke the boot and $wire procedure passed
as parameter on boot.
Example usage:
``` js
const bootwire = require('bootwire');
const app = bootwire(require('./src/boot'));
if (require.main === module) {
app.boot()
.catch((err) => {
// Something extremely bad happened while booting
console.error(err);
process.exit(1);
});
}
module.exports = app;
```
Example tests:
``` js
const app = require('../..');
describe('app', function() {
it('runs', async function() {
const port = await getRandomPort();
await app.boot({
config: { port }
});
await request('http://localhost:${port}/health');
// ...
});
});
```
**Kind**: global function
**Returns**: [App
](#App) - A bootable `App` instance.
| Param | Type | Description |
| --- | --- | --- |
| bootAndWireFn | function
| The function to be called. |