npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

service-builder

v2.0.0

Published

Service builder DSL without the boilerplate

Downloads

37

Readme

npm version Build Status

Introduction

service-builder is a dependency injection framework that helps developers write factories that satsify the principle of lest privilege. The target audience for this library are those that like having factories but not necessarily writing them.

Example

Let's assume we are building a car module. The initial approach might be to create an object that has implicit dependencies:

function createCar() {
  const engine = createEngine();
  const wheels = createWheels();
  return {
    start() {
      engine.start();
    },
    drive() {
      if (engine.isRunning) {
        wheels.rotate();
      }
    }
  }
}
  • [ ] Supports dependency injection
  • [x] Supports principle of least privilege

This initial version is great because calling createCar is all that is needed to make a car. Nothing more is needed to be known about how a car works. However, the car that is being create is not very flexible. Only one type of car can exist.

Later developers learn that engines can have a variable number of cylinders and wheels can have different friction coefficients. To support these the createCar factory is modified:

function createCar(engineCylinders, wheelFriction) {
  const engine = createEngine(engineCylinders);
  const wheels = createWheels(wheelFriction);
  return {
    start() {
      engine.start();
    },
    drive() {
      if (engine.isRunning) {
        wheels.rotate();
      }
    }
  }
}
  • [ ] Supports dependency injection
  • [ ] Supports principle of least privilege

Uh-oh... Now the createCar class is becoming very tightly coupled to its dependencies. Furthermore, any changes to createEngine or createWheels now requires changes to createCar and any code that is dependent on Car. We still don't support dependency injection, but also now createCar needs to know more about createEngine and createWheels.

For reasons of dependency injection, passing in dependencies as arguments helps write code that supports dependency injection.

function createCar(engine, wheels) {
  return {
    start() {
      engine.start();
    },
    drive() {
      if (engine.isRunning) {
        wheels.rotate();
      }
    }
  }
}
  • [x] Supports dependency injection
  • [ ] Supports principle of least privilege

Great! Now createCar doesn't care where engine and wheels came from. However, the problem of least privilege has only been moved around. Now, in order to make a car instance, clients need to also make an engine and wheels. Our code for instantiating a car has gone from:

const car = createCar();

to

const car = createCar(6, 0.2);

to

const car = createCar(createEngine(6), createWheels(0.2));

Now imagine that createEngine and createWheels end up needing additional dependencies. Before long, in order to use createCar, the developer will need to first create the rubber and metal when all we wanted to do was make it go!

service-builder can create a factory for building cars.

import builder from 'service-builder;

const carBlueprint = builder({
  car(engine, wheels): createCar,
  engine(cylinders): createEngine,
  wheels(friction): createWheels,
});

const sportsCarFactory = carBlueprint.construct({
  cylinders: 8,
  friction: 0.9,
});

// Make a sports car:
const sportsCar = sportsCarFactory.getCar();

service-builder will automatically create, walk and fill the dependency tree in order to create a instances. Users of the factory do not need to car how instances are built.

Now that we have a blueprint, we can create new factories with different dependencies:

const sedanFactory = blueprint.construct({
  cylinders: 4,
  friction: 0.6,
});

// Make a sedan
const sedan = sedanFactory.getCar();

Or even with entirely new types:

const rocketCarFactory = blueprint.construct({
  engine: createJetEngine,
  wheels: createRetractableWheels,
});

const rocketCar = rocketCarFactory.getCar();

Usage

import builder from 'service-builder;

const blueprint = builder({
  breakfast: function (meat, eggs, drink) {
    return `${meat} with ${eggs} and ${drink}`;
  },
  eggs: eggStyle => `${eggStyle} eggs`,
  solids: (meat, eggs) => [meat, eggs].join(', ')
});

const factory = blueprint.construct();

factory
  .withMeat('ham')
  .withEggStyle('scrambled')
  .withDrink('orange juice')
  .getBreakfast();
// => 'ham with scrambled eggs and orange juice'

const anotherFactory = blueprint.construct({
  meat: 'sausage'
})
  .withEggStyle('scrambled');

anotherFactory.getSolids();
// => 'ham, scrambled eggs'

anotherFactory.getEggs();
// => 'scrambled eggs'

anotherFactory.getBreakfast();
// => Error

Lazy properties

Instead of using the getter functions, there are also lazily evaluated properties on the factory

const blueprint = builder({
  eggs: eggStyle => `${eggStyle} eggs`,
});
const factory = blueprint.construct({
  eggStyle: 'fried',
});
console.log(factory.eggs);
// => 'fried eggs'

Implicit properties

If a non-function non-array is passed in as a dependency, then it will be implicitly wrapped in a function.

const blueprint = builder({
  foo: bar => `foo${bar}`,
  bar: 'bar',  
});
const factory = blueprint.construct();
console.log(factory.foo);
// => 'foobar'

Resolver

$ is a resolver for dependencies

const blueprint = builder({
  eggs: eggStyle => `${eggStyle} eggs`,
  meat: meatStyle => `${meatStyle} steak`,
});
const factory = blueprint.construct({
  eggStyle: 'fried',
  meatStyle: 'rare',
});
console.log(factory.$((meat, eggs) => `${eggs} and ${meat}`));
// => 'fried eggs and rare steak'

Alternatively, you can forgo the DSL syntax of the constructed factory entirely in favor of a resolver:

builder.config({ dsl: false });
const blueprint = builder({
  eggs: eggStyle => `${eggStyle} eggs`,
  meat: meatStyle => `${meatStyle} steak`,
});
const $ = blueprint.construct({
  eggStyle: 'fried',
  meatStyle: 'rare',
});
console.log($((meat, eggs) => `${eggs} and ${meat}`));
// => 'fried eggs and rare steak'

Promises

If any dependencies return a promise, then the promise will be resolved before being used as dependency. A side effect of this behavior is that any service that depends on a promise will also return a promise.

import builder from 'service-builder;

const blueprint = builder({
  breakfast: function (meat, eggs, drink) {
    return `${meat} with ${eggs} and ${drink}`;
  },
  // Get eggs async
  eggs: eggStyle => Promise.resolve(`${eggStyle} eggs`),
  solids: (meat, eggs) => [meat, eggs].join(', ')
});

const factory = blueprint.construct();

factory
  .withMeat('ham')
  .withEggStyle('scrambled')
  .withDrink('orange juice')
  .getBreakfast()
  .then(console.log.bind(console));
// => 'ham with scrambled eggs and orange juice'

const anotherFactory = blueprint.construct()
  .withMeat('ham')
  .withEggStyle('scrambled');

anotherFactory.getSolids()
  .then(console.log.bind(console));
// => 'ham, scrambled eggs'

anotherFactory.getEggs()
  .then(console.log.bind(console));
// => 'scrambled eggs'

anotherFactory.getBreakfast()
  .then(console.log.bind(console));
// => Error

Async

Promise providers work well with async / await

import builder from 'service-builder;

const blueprint = builder({
  breakfast: function (meat, eggs, drink) {
    return `${meat} with ${eggs} and ${drink}`;
  },
  // Get eggs async
  eggs: async eggStyle => await asyncOperation(`${eggStyle} eggs`),
  solids: (meat, eggs) => [meat, eggs].join(', ')
});

const factory = blueprint.construct();

console.log(await factory
  .withMeat('ham')
  .withEggStyle('scrambled')
  .withDrink('orange juice')
  .getBreakfast());
// => 'ham with scrambled eggs and orange juice'

const anotherFactory = blueprint.construct()
  .withMeat('ham')
  .withEggStyle('scrambled');

console.log(await anotherFactory.getSolids());
// => 'ham, scrambled eggs'

console.log(await anotherFactory.getEggs());
// => 'scrambled eggs'

console.log(await anotherFactory.getBreakfast());
// => Error

Surviving Uglification

Minification / uglifying code mangles variable names which breaks being able to resolve dependencies from function names. The fix is the same as Angular 1.x, to use array style dependencies. service-builder also supports annotate-ng style tags and is compatible with ng-annotate and babel-plugin-angularjs-annotate

import builder from 'service-builder;

const blueprint = builder({
  // Array style
  breakfast: ['meat', 'eggs', 'drink', function (meat, eggs, drink) {
    return `${meat} with ${eggs} and ${drink}`;
  }],
  // Directive style
  eggs: eggStyle => {
    "ngInject";
    return `${eggStyle} eggs`
  },
  // Wrapper function style
  solids: ng((meat, eggs) => [meat, eggs].join(', ')),
});

const factory = blueprint.construct();

// Everything continues to work as before, except you can now safely uglify code
factory
  .withMeat('ham')
  .withEggStyle('scrambled')
  .withDrink('orange juice')
  .getBreakfast();
// => 'ham with scrambled eggs and orange juice'

const anotherFactory = blueprint.construct()
  .withMeat('steak')
  .withEggStyle('pouched');

anotherFactory.getSolids();
// => 'steak, pouched eggs'

anotherFactory.getEggs();
// => 'puched eggs'

anotherFactory.getBreakfast();
// => Error unable to resolve drink at breakfast

License

License: MIT