jeffijoe/awilix

View on GitHub
src/resolvers.ts

Summary

Maintainability
C
1 day
Test Coverage
import { AwilixContainer, FunctionReturning, ResolveOptions } from './container'
import { AwilixTypeError } from './errors'
import { InjectionMode, InjectionModeType } from './injection-mode'
import { Lifetime, LifetimeType } from './lifetime'
import { Parameter, parseParameterList } from './param-parser'
import { isFunction, uniq } from './utils'

// We parse the signature of any `Function`, so we want to allow `Function` types.
/* eslint-disable @typescript-eslint/ban-types */

/**
 * RESOLVER symbol can be used by modules loaded by
 * `loadModules` to configure their lifetime, injection mode, etc.
 */
export const RESOLVER = Symbol('Awilix Resolver Config')

/**
 * Gets passed the container and is expected to return an object
 * whose properties are accessible at construction time for the
 * configured resolver.
 *
 * @type {Function}
 */
export type InjectorFunction = <T extends object>(
  container: AwilixContainer<T>,
) => object

/**
 * A resolver object returned by asClass(), asFunction() or asValue().
 */
export interface Resolver<T> extends ResolverOptions<T> {
  resolve<U extends object>(container: AwilixContainer<U>): T
}

/**
 * A resolver object created by asClass() or asFunction().
 */
export interface BuildResolver<T> extends Resolver<T>, BuildResolverOptions<T> {
  injectionMode?: InjectionModeType
  injector?: InjectorFunction
  setLifetime(lifetime: LifetimeType): this
  setInjectionMode(mode: InjectionModeType): this
  singleton(): this
  scoped(): this
  transient(): this
  proxy(): this
  classic(): this
  inject(injector: InjectorFunction): this
}

/**
 * Options for disposable resolvers.
 */
export interface DisposableResolverOptions<T> extends ResolverOptions<T> {
  dispose?: Disposer<T>
}

/**
 * Disposable resolver.
 */
export interface DisposableResolver<T>
  extends Resolver<T>,
    DisposableResolverOptions<T> {
  disposer(dispose: Disposer<T>): this
}

/**
 * Disposer function type.
 */
export type Disposer<T> = (value: T) => any | Promise<any>

/**
 * The options when registering a class, function or value.
 * @type RegistrationOptions
 */
export interface ResolverOptions<T> {
  /**
   * Only used for inline configuration with `loadModules`.
   */
  name?: string
  /**
   * Lifetime setting.
   */
  lifetime?: LifetimeType
  /**
   * Registration function to use. Only used for inline configuration with `loadModules`.
   */
  register?: (...args: any[]) => Resolver<T>
  /**
   * True if this resolver should be excluded from lifetime leak checking. Used by resolvers that
   * wish to uphold the anti-leakage contract themselves. Defaults to false.
   */
  isLeakSafe?: boolean
}

/**
 * Builder resolver options.
 */
export interface BuildResolverOptions<T>
  extends ResolverOptions<T>,
    DisposableResolverOptions<T> {
  /**
   * Resolution mode.
   */
  injectionMode?: InjectionModeType
  /**
   * Injector function to provide additional parameters.
   */
  injector?: InjectorFunction
}

/**
 * A class constructor. For example:
 *
 *    class MyClass {}
 *
 *    container.registerClass('myClass', MyClass)
 *                                       ^^^^^^^
 */
export type Constructor<T> = { new (...args: any[]): T }

/**
 * Creates a simple value resolver where the given value will always be resolved. The value is
 * marked as leak-safe since in strict mode, the value will only be resolved when it is not leaking
 * upwards from a child scope to a parent singleton.
 *
 * @param  {string} name The name to register the value as.
 *
 * @param  {*} value The value to resolve.
 *
 * @return {object} The resolver.
 */
export function asValue<T>(value: T): Resolver<T> {
  return {
    resolve: () => value,
    isLeakSafe: true,
  }
}

/**
 * Creates a factory resolver, where the given factory function
 * will be invoked with `new` when requested.
 *
 * @param  {string} name
 * The name to register the value as.
 *
 * @param  {Function} fn
 * The function to register.
 *
 * @param {object} opts
 * Additional options for the resolver.
 *
 * @return {object}
 * The resolver.
 */
export function asFunction<T>(
  fn: FunctionReturning<T>,
  opts?: BuildResolverOptions<T>,
): BuildResolver<T> & DisposableResolver<T> {
  if (!isFunction(fn)) {
    throw new AwilixTypeError('asFunction', 'fn', 'function', fn)
  }

  const defaults = {
    lifetime: Lifetime.TRANSIENT,
  }

  opts = makeOptions(defaults, opts, (fn as any)[RESOLVER])

  const resolve = generateResolve(fn)
  const result = {
    resolve,
    ...opts,
  }

  return createDisposableResolver(createBuildResolver(result))
}

/**
 * Like a factory resolver, but for classes that require `new`.
 *
 * @param  {string} name
 * The name to register the value as.
 *
 * @param  {Class} Type
 * The function to register.
 *
 * @param {object} opts
 * Additional options for the resolver.
 *
 * @return {object}
 * The resolver.
 */
export function asClass<T = object>(
  Type: Constructor<T>,
  opts?: BuildResolverOptions<T>,
): BuildResolver<T> & DisposableResolver<T> {
  if (!isFunction(Type)) {
    throw new AwilixTypeError('asClass', 'Type', 'class', Type)
  }

  const defaults = {
    lifetime: Lifetime.TRANSIENT,
  }

  opts = makeOptions(defaults, opts, (Type as any)[RESOLVER])

  // A function to handle object construction for us, as to make the generateResolve more reusable
  const newClass = function newClass(...args: unknown[]) {
    return Reflect.construct(Type, args)
  }

  const resolve = generateResolve(newClass, Type)
  return createDisposableResolver(
    createBuildResolver({
      ...opts,
      resolve,
    }),
  )
}

/**
 * Resolves to the specified registration. Marked as leak-safe since the alias target is what should
 * be checked for lifetime leaks.
 */
export function aliasTo<T>(
  name: Parameters<AwilixContainer['resolve']>[0],
): Resolver<T> {
  return {
    resolve(container) {
      return container.resolve(name)
    },
    isLeakSafe: true,
  }
}

/**
 * Given an options object, creates a fluid interface
 * to manage it.
 *
 * @param {*} obj
 * The object to return.
 *
 * @return {object}
 * The interface.
 */
export function createBuildResolver<T, B extends Resolver<T>>(
  obj: B,
): BuildResolver<T> & B {
  function setLifetime(this: any, value: LifetimeType) {
    return createBuildResolver({
      ...this,
      lifetime: value,
    })
  }

  function setInjectionMode(this: any, value: InjectionModeType) {
    return createBuildResolver({
      ...this,
      injectionMode: value,
    })
  }

  function inject(this: any, injector: InjectorFunction) {
    return createBuildResolver({
      ...this,
      injector,
    })
  }

  return updateResolver(obj, {
    setLifetime,
    inject,
    transient: partial(setLifetime, Lifetime.TRANSIENT),
    scoped: partial(setLifetime, Lifetime.SCOPED),
    singleton: partial(setLifetime, Lifetime.SINGLETON),
    setInjectionMode,
    proxy: partial(setInjectionMode, InjectionMode.PROXY),
    classic: partial(setInjectionMode, InjectionMode.CLASSIC),
  })
}

/**
 * Given a resolver, returns an object with methods to manage the disposer
 * function.
 * @param obj
 */
export function createDisposableResolver<T, B extends Resolver<T>>(
  obj: B,
): DisposableResolver<T> & B {
  function disposer(this: any, dispose: Disposer<T>) {
    return createDisposableResolver({
      ...this,
      dispose,
    })
  }

  return updateResolver(obj, {
    disposer,
  })
}

/**
 * Partially apply arguments to the given function.
 */
function partial<T1, R>(fn: (arg1: T1) => R, arg1: T1): () => R {
  return function partiallyApplied(this: any): R {
    return fn.call(this, arg1)
  }
}

/**
 * Makes an options object based on defaults.
 *
 * @param  {object} defaults
 * Default options.
 *
 * @param  {...} rest
 * The input to check and possibly assign to the resulting object
 *
 * @return {object}
 */
function makeOptions<T, O>(defaults: T, ...rest: Array<O | undefined>): T & O {
  return Object.assign({}, defaults, ...rest) as T & O
}

/**
 * Creates a new resolver with props merged from both.
 *
 * @param source
 * @param target
 */
function updateResolver<T, A extends Resolver<T>, B>(
  source: A,
  target: B,
): Resolver<T> & A & B {
  const result = {
    ...(source as any),
    ...(target as any),
  }
  return result
}

/**
 * Returns a wrapped `resolve` function that provides values
 * from the injector and defers to `container.resolve`.
 *
 * @param  {AwilixContainer} container
 * @param  {Object} locals
 * @return {Function}
 */
function wrapWithLocals<T extends object>(
  container: AwilixContainer<T>,
  locals: any,
) {
  return function wrappedResolve(name: string, resolveOpts: ResolveOptions) {
    if (name in locals) {
      return locals[name]
    }

    return container.resolve(name, resolveOpts)
  }
}

/**
 * Returns a new Proxy that checks the result from `injector`
 * for values before delegating to the actual container.
 *
 * @param  {Object} cradle
 * @param  {Function} injector
 * @return {Proxy}
 */
function createInjectorProxy<T extends object>(
  container: AwilixContainer<T>,
  injector: InjectorFunction,
) {
  const locals = injector(container) as any
  const allKeys = uniq([
    ...Reflect.ownKeys(container.cradle),
    ...Reflect.ownKeys(locals),
  ])
  // TODO: Lots of duplication here from the container proxy.
  // Need to refactor.
  const proxy = new Proxy(
    {},
    {
      /**
       * Resolves the value by first checking the locals, then the container.
       */
      get(target: any, name: string | symbol) {
        if (name === Symbol.iterator) {
          return function* iterateRegistrationsAndLocals() {
            for (const prop in container.cradle) {
              yield prop
            }
            for (const prop in locals) {
              yield prop
            }
          }
        }
        if (name in locals) {
          return locals[name]
        }
        return container.resolve(name as string)
      },

      /**
       * Used for `Object.keys`.
       */
      ownKeys() {
        return allKeys
      },

      /**
       * Used for `Object.keys`.
       */
      getOwnPropertyDescriptor(target: any, key: string) {
        if (allKeys.indexOf(key) > -1) {
          return {
            enumerable: true,
            configurable: true,
          }
        }

        return undefined
      },
    },
  )

  return proxy
}

/**
 * Returns a resolve function used to construct the dependency graph
 *
 * @this {Registration}
 * The `this` context is a resolver.
 *
 * @param {Function} fn
 * The function to construct
 *
 * @param {Function} dependencyParseTarget
 * The function to parse for the dependencies of the construction target
 *
 * @param {boolean} isFunction
 * Is the resolution target an actual function or a mask for a constructor?
 *
 * @return {Function}
 * The function used for dependency resolution
 */
function generateResolve(fn: Function, dependencyParseTarget?: Function) {
  // If the function used for dependency parsing is falsy, use the supplied function
  if (!dependencyParseTarget) {
    dependencyParseTarget = fn
  }

  // Parse out the dependencies
  // NOTE: we do this regardless of whether PROXY is used or not,
  // because if this fails, we want it to fail early (at startup) rather
  // than at resolution time.
  const dependencies = parseDependencies(dependencyParseTarget)

  // Use a regular function instead of an arrow function to facilitate binding to the resolver.
  return function resolve<T extends object>(
    this: BuildResolver<any>,
    container: AwilixContainer<T>,
  ) {
    // Because the container holds a global reolutionMode we need to determine it in the proper order of precedence:
    // resolver -> container -> default value
    const injectionMode =
      this.injectionMode ||
      container.options.injectionMode ||
      InjectionMode.PROXY

    if (injectionMode !== InjectionMode.CLASSIC) {
      // If we have a custom injector, we need to wrap the cradle.
      const cradle = this.injector
        ? createInjectorProxy(container, this.injector)
        : container.cradle

      // Return the target injected with the cradle
      return fn(cradle)
    }

    // We have dependencies so we need to resolve them manually
    if (dependencies.length > 0) {
      const resolve = this.injector
        ? wrapWithLocals(container, this.injector(container))
        : container.resolve

      const children = dependencies.map((p) =>
        resolve(p.name, { allowUnregistered: p.optional }),
      )
      return fn(...children)
    }

    return fn()
  }
}

/**
 * Parses the dependencies from the given function.
 * If it's a class that extends another class, and it does
 * not have a defined constructor, attempt to parse it's super constructor.
 */
function parseDependencies(fn: Function): Array<Parameter> {
  const result = parseParameterList(fn.toString())
  if (!result) {
    // No defined constructor for a class, check if there is a parent
    // we can parse.
    const parent = Object.getPrototypeOf(fn)
    if (typeof parent === 'function' && parent !== Function.prototype) {
      // Try to parse the parent
      return parseDependencies(parent)
    }
    return []
  }

  return result
}