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

injecute

v0.13.1

Published

Lightweight extendable typesafe dependency injection container

Downloads

340

Readme

injecute

Lightweight extendable typesafe dependency injection container written in TypeScript.

Build and tests

Key features

  • typesafe
  • explicit dependencies by default
  • extensibility
  • browser / node environments support
  • nested containers
  • no transpiling required

Motivation

Most existing DI containers heavily rely on decorators and use them as main approach to manage dependencies. But it leads us to breaking the IOC principle and our business code become dependent on concrete library that provides container and decorators. We can handle it by creating proxy classes which created for bounding derived class to container, but it leads us to unnecessary boilerplate code.

Solution is not use the decorators as default way to register services.

How to use

  1. Create container
  2. Add services
  3. Get your services or use .injecute method when needed.

Basic usage

interface IDependency {
  value: number;
  method(): string;
}

class NotBasicService {
  constructor(srv: IDependency, logger: Logger) {}
}

const container = new DIContainer()
  .addInstance('logger', console)
  .addSingleton(
    'myService',
    (): IDependency => ({
      value: 42,
      method() {
        return 'The answer';
      },
    }),
    [],
  )
  .addTransient('notBasicService', construct(NotBasicService), [
    'myService',
    'logger',
  ]);

// TS will know that notBasicService is the NotBasicService;
const notBasicService = container.get('notBasicService');

assert(myDependantService instanceof NotBasicService);

Services registration

Constructors and functions (factories) supported. You can add your ready to use instances as well.

Service types

  • Singleton

    Instantiated/executed once. Each time will return the same result.

  • Transient

    Each time will be created new instance.

  • Instance

    Created outside of container instance.

Each added service will change the result type of container. So you should to add services in initialization order low level services first.

Configuration based service

type Logger = {
  log: (logLevel: string, message: string) => void;
};

const config = {
  useProductionLogger: process.env.USE_PRODUCTION_LOGGER === 'true',
};

class LoggerUsingService {
  constructor(logger: Logger) {}
}

const container = new DIContainer()
  .addSingleton('productionLogger', productionLoggerFactory, [])
  .addInstance('console', console)
  .addAlias(
    'logger',
    config.useProductionLogger ? 'productionLogger' : 'console',
  )
  .addTransient('service', construct(LoggerUsingService), ['logger']);

// service will use as logger `productionLogger` or `console` based on config.;

const service = container.get('service');

Nested containers

Containers nesting allows to keep local services out of parent context but use the parents services. All services registered in parent container can be accessed seamlessly. There is two ways to derive container.

  • Put the parent container as argument of new container.
  • Use .fork() method. In this case child container will use same resolvers and middlewares. It can be changed by providing optional argument.
import { DIContainer } from './container';
import * as Express from 'express';

const server = new Express();
const rootContainer = new DIContainer()
  .addInstance('logger', console)
  .addSingleton(
    'db',
    (logger) => {
      /* some db init with logger */
    },
    ['logger'],
  );

const addContainerMiddlewareCreator = (container) => (req, res, next) => {
  // lazy create container with lazy user resolving
  // this container will have access to all `rootContainer` services but adding services to this container will not modify root container
  req.getContainer = () =>
    container
      .fork() // make nested service
      .addSingleton(
        'userId',
        () => {
          /* get user id from req, from the auth header for example */
        },
        [],
      )
      .addSingleton('user', (userId, db) => db.getUserById(userId), [
        'userId',
        'db',
      ])
      .addSingleton(
        'businessService',
        async (user) => new MyBusinessService(await user),
        ['user'],
      );
  // add other request related stuff for exapmle apm / audit based on user / business services bounded to user or request
  next();
};
server.use(addContainerMiddlewareCreator(rootContainer));

server.get('/api/business', (req, res) => {
  req
    .getContainer()
    .get('businessService')
    .then((srv) => {
      // src.user is the resolved user from some auth data
      req.json(srv.doMyBusiness());
    });
});

Or you can not mutate the req by adding the getContainer function and use "functional" approach:

export const createRequestContainerWrapper = <
  RootServices extends Record<ArgumentsKey, any>,
  RequestServices extends Record<ArgumentsKey, any>,
>(
  container: IDIContainer<RootServices>,
  extension: IDIContainerExtension<
    RootServices & { req: Request },
    RequestServices
  >,
) => {
  return <
      Keys extends readonly (keyof RequestServices)[],
      RequiredServices extends DependenciesTypes<RequestServices, Keys>,
    >(
      servicesNames: [...Keys],
      handlerCreator: Callable<RequiredServices, Handler>,
    ): Handler =>
    (req, res, next) => {
      const targetHandler = container
        // make nested service
        .fork()
        // add request to container
        .addInstance('req', req)
        // apply extension which will register services related to request context
        .extend(extension)
        // create handler using required services from container
        .injecute<Handler, any, any>(handlerCreator, servicesNames);

      targetHandler(req, res, next);
    };
};

const rootContainer = new DIContainer().addSingleton(
  'userResolvingService',
  construct(UserResolvingService),
  ['db', 'etc...'],
);

const useRequestContainer = createRequestContainerWrapper(
  rootContainer,
  (c) => {
    return (
      c
        // get token from request
        .addSingleton('authToken', (req) => req.headers['Authorization'], [
          'req',
        ])
        // get user by token using service from root container
        .addSingleton(
          'user',
          (userResolvingService, token) =>
            userResolvingService.getUserByToken(token),
          ['userResolvingService', 'authToken'],
        )
        // use user in RequestContextService constructor
        .addSingleton('requestContextService', RequestContextService, ['user'])
    );
    // Add more your request related services here
  },
);

app.post(
  '/api/user-stuff',
  useRequestContainer(
    ['requestContextService'],
    (requestContextService) => (req, res) => {
      res.send(requestContextService.doUserRelatedStuff(req.body));
    },
  ),
);

Injecute

Best way to use container is hiding container with some helpers. Injecute will help there.

import { default as Express, Handler } from 'express';
import {
  ArgumentsKey,
  DependenciesTypes,
  DIContainer,
  Func,
  IDIContainer,
} from 'injecute';

// helper that helps to pull services from container
export const useContainerServices =
  <S extends Record<ArgumentsKey, any>>(container: IDIContainer<S>) =>
  <
    Keys extends readonly (keyof S)[],
    RequiredServices extends DependenciesTypes<S, Keys>,
    H extends Func<RequiredServices, Handler>,
  >(
    servicesNames: [...Keys],
    handlerCreator: H,
  ): Handler => {
    return container.injecute<() => Handler, any, any>(
      handlerCreator,
      servicesNames,
    );
  };

// business stuff service
class MyBusinessService {
  constructor(private readonly logger: any) {}

  doBusinessStuff(parameter: string) {
    this.logger.log(parameter);
    return Number(parameter);
  }
}

// root app container
const c = new DIContainer()
  .addInstance('logger', console)
  .addSingleton('businessService', construct(MyBusinessService), ['logger']);

// handler creator bounded to your app container
const useServices = useContainerServices(c);

const app = Express();

// use handler on route
app.use(
  '/api/business/stuff/:id',
  useServices(['businessService'], (service) => (req, res, next) => {
    res.json(service.doBusinessStuff(req.params.id));
  }),
);

app.listen(3000);
c.get('logger').log('Listening at port 3000');

OOP factories

For OOP style factory classes you can create own helper.

type IFactory<D, T> = {
  build: (args: D) => T;
};

function useOopFactory<D, T>(factory: IFactory<D, T>) {
  return (args: D) => factory.build(args);
}

container.addSingleton(
  'serviceFromFactory',
  useOopFactory(new ConcreteFactory()),
  ['some D'],
);

Extensions

Extensions are allows to add batch services from some module. Also, it allows to add service without breaking the chaining

function addLoggingServices(config) {
  return (c) =>
    c
      .addSingleton('elkUrl', () => config.ELK_URL, [])
      .addSingleton('elkLogger', (url) => ElkLogger(url), ['elkUrl'])
      .addInstance('console', console)
      .addAlias(
        'logger',
        config.NODE_ENV === 'production' ? 'elkLogger' : 'console',
      );
}

container
  .extend(addLoggingServices(config))
  .extend(addCryptoModules)
  .extend(addBusinessServices);
const p = new DIContainer().addTransient('s', () => ({ x: 1 }), []);

const c = new DIContainer(p).extend((c) => {
  const s = c.get('s');
  return c.addTransient('s', () => ({ ...s, y: 2 }), []);
});

expect(c.get('s')).to.be.eql({ x: 1, y: 2 });

Middlewares

Allows to add some logic before and/or after service resolving.

You can add logging or implement own strategies of resolving dependencies.

You have access to container as this in middleware.

container.use(function (key, next) {
  const willCreateNewInstance = !this.instances[key] && !!this.factories[key];
  if (willCreateNewInstance) {
    this.get('logger').debug(`New instance will be created for ${key} key.`);
  }

  return next(key);
});

Pitfalls

Declaration files

When you use dynamic containers types, when container services inferred from added entries. You must keep container entries adding code in separated files with code which uses inferred container type.

// TODO: add example

Container intermediate usage.

TBD

// TODO: add example