Skip to content
/ cerise Public

Intuitive and lightweight Dependency Injection library for Node.js

Notifications You must be signed in to change notification settings

izeau/cerise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cerise

Intuitive Dependency Injection (DI) library for Node.js, written in JavaScript and weighing less than 10 KB. Ironically, Cerise does not depend on any package.

API documentation -- Examples -- FAQ

Installation

Install with npm or yarn

$ npm install cerise
$ yarn add cerise

Both CommonJS and ES modules builds are included; the latter will be automatically selected if your build system supports it.

Usage

Using Cerise is dead simple. There are two concepts you'll need to understand first: containers and factories.

createContainer

A container (also known as an injector) is a master object that knows how to create services, thanks to factories.

const { createContainer, constant, factory, service } = require('cerise');

// Create a container and immediately register a factory
// for the `name` service.
const container = createContainer({
  package_name: constant('cerise'),
});

// You can also register a service for an existing container.
container.register('package_name', constant('cerise'));

// You can retrieve services using either container as a
// function, or its `proxy` property.
assert('cerise' === container('package_name'));
assert('cerise' === container.proxy.package_name);

There are multiple ways to declare a service: using the constant, factory and service helpers.

constant

When using constant you cannot depend on an other service. You can register any value: a number, string, function, etc.

container.register('base_url', constant('https://npmjs.com'));
container.register('concat', constant((...args) => args.join('')));

assert('string' === typeof container('base_url'));
assert('function' === typeof container('concat'));

factory

If you need to depend on an other service, use a factory. factory takes a function that will be passed container.proxy (which can be destructured to access other services) and returns a service.

container.register(
  'package_url',
  factory(proxy => {
    return proxy.concat(proxy.base_url, '/', proxy.package_name);
  }),
);

// Using destructuring
container.register(
  'package_url',
  factory(({ concat, base_url: baseUrl, package_name: packageName }) => {
    return concat(baseUrl, '/', packageName);
  }),
);

// Alternatively, call the proxy as a function
container.register(
  'package_url',
  factory(inject => {
    return inject('concat')(inject('base_url'), '/', inject('package_name'));
  }),
);

assert('https://npmjs.com/cerise' === container('package_url'));

You'll notice that constant(x) is equivalent to factory(() => x): it's just sugar.

service

Lastly, service is passed a class and will return an instance on retrieval. Use it if you're more familiar with OOP.

class PackageUrl {
  constructor({ concat, base_url: baseUrl }) {
    this._concat = concat;
    this._baseUrl = baseUrl;
  }

  get(packageName) {
    return this._concat(this._baseUrl, '/', packageName);
  }
}

container.register('package_url', service(PackageUrl));

assert('https://npmjs.com/cerise' === container('package_url').get('cerise'));

Once again, it's just sugar: service(T) is equivalent to factory(proxy => new T(proxy)).

Scopes

Oftentimes you'll want to create a scope from a container. Scopes inherit their parent and their registered service, but can also have their own service. For instance, if you're using Express, you might want to have a master container to store your database connexion, and another container for request-specific data.

const express = require('express');
const { Database } = require('sqlite3');
const { createContainer, constant } = require('cerise');

const app = express();
const container = createContainer({
  db: constant(new Database(':memory:')),
});

// For each request, create a scope and fetch session data.
app.use((req, res, next) => {
  const id = req.get('x-session-id');
  const db = container('db');

  req.scope = container.scope();

  db.get('select * from sessions where id = ?', [id], (err, session) => {
    // Only alter the request scope, not the parent container
    req.scope.register('session', constant(session));
    next();
  });
});

// Session data is available on child scope.
app.get('/session', (req, res) => {
  res.json(req.scope('session'));
});

// Parent container services are also available on child scope.
app.get('/time', (req, res) => {
  const db = req.scope('db');

  db.get('select current_timestamp as time', (err, { time }) => {
    res.json({ time });
  });
});

Lifetimes

default lifetime

By default (except for constant) the factory will be called each time you wish to retrieve a value from a factory.

// Each resolution will result in a new Thing instance being created.
container.register('thing', service(class Thing {}));

const foo = container('thing');
const bar = container('thing');

assert(foo !== bar);

singletons

You may however wish to specify a lifetime to your factory in order to cache its result.

// Now the first instance will be cached and returned each time.
container.register('thing', service(class Thing {}).singleton());

const foo = container('thing');
const bar = container('thing');

assert(foo === bar);

scoped

Singletons only make sense on the root container; if you wish to cache a service for scopes you will want to use the scoped lifetime qualifier:

const winston = require('winston');

container.register(
  'logger',
  constant(
    winston.createLogger({
      transports: winston.transports.Console(),
    }),
  ),
);

// Create a scope on every request
app.use((req, res, next) => {
  req.scope = container.scope();
  next();
});

// Register a *scoped* logger (with request id metadata)
app.use((req, res, next) => {
  req.scope.register(
    'reqlog',
    factory(({ logger }) =>
      logger.child({ requestId: req.get('x-request-id') }),
    ).scoped(),
  );
  next();
});

// Logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  const logger = req.scope('reqlog');

  req.on('finish', () => {
    const elapsed = Date.now() - start;

    logger.info('[%s] %s %s', elapsed, req.method, req.path);
  });

  next();
});

Saving and restoring state

Root containers' state can be saved and restored which can be useful for testing. For instance, in Mocha's beforeEach and afterEach hooks:

describe('My API', () => {
  beforeEach(() => container.save());
  afterEach(() => container.restore());

  it('...', () => {
    // package_url service will be 'nope' but only for this particular test
    container.register('package_url', constant('nope'));
  });
});

Utils

Middlewares: Cerise provides middlewares for Express and Koa.
See the API documentation.

Controllers: since calling req.scope gets old really fast, Cerise also provides a controller helper -- with an async error handler for convenience. Pass it a callback, and it will get called with req.scope.proxy, req, res and next.
See the API documentation.

FAQ

How to overwrite a parent service in a scope?

You can register a service with the same name:

const parent = createContainer();
parent.register('scopeName', constant('parent'));

const child = parent.scope();
child.register('scopeName', constant('child'));

assert('parent' === parent('scopeName'));
assert('child' === child('scopeName'));

Can a child scope service depend on a parent scope service of the same name?

Yes, but you cannot depend directly on the parent service.

parent.register('breadcrumb', constant('/'));

// This will break: `breadcrumb` on the child cannot depend on `breadcrumb`.
child.register(
  'breadcrumb',
  factory(({ breadcrumb }) => breadcrumb + 'child/'),
);

// Workaround #1: access parent scope directly
child.register('breadcrumb', factory(() => parent('breadcrumb') + 'child/'));

// Workaround #2 (preferred): register the parent as a child service
child.register('$parent', constant(parent.scope));
child.register(
  'breadcrumb',
  factory(({ $parent: { breadcrumb } }) => breadcrumb + 'child/'),
);

Examples

Head over to the examples directory for in-depth examples.

Contributing

Constructive feedback is always welcome! Feel free to create issues if you have any question, suggestion or bug reports. A pull request is also always appreciated.

Clone this repository, run npm install or yarn to install the development dependencies, launch npm test -- -w or yarn test -w and start hacking!

Before you submit your pull request, please make sur you've run Prettier (npm run lint or yarn lint) and that your test coverage is at 100% (npm run coverage or yarn coverage).