@loopback/context
Version: 
Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container
270 lines (253 loc) • 7.67 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {DecoratorFactory} from '@loopback/metadata';
import assert from 'assert';
import debugFactory from 'debug';
import {Context} from './context';
import {invokeMethodWithInterceptors} from './interceptor';
import {ResolutionSession} from './resolution-session';
import {resolveInjectedArguments} from './resolver';
import {transformValueOrPromise, ValueOrPromise} from './value-promise';
const debug = debugFactory('loopback:context:invocation');
const getTargetName = DecoratorFactory.getTargetName;
/**
 * Return value for a method invocation
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InvocationResult = any;
/**
 * Array of arguments for a method invocation
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InvocationArgs = any[];
/**
 * An interface to represent the caller of the invocation
 */
export interface InvocationSource<T = unknown> {
  /**
   * Type of the invoker, such as `proxy` and `route`
   */
  readonly type: string;
  /**
   * Metadata for the source, such as `ResolutionSession`
   */
  readonly value: T;
}
/**
 * InvocationContext represents the context to invoke interceptors for a method.
 * The context can be used to access metadata about the invocation as well as
 * other dependencies.
 */
export class InvocationContext extends Context {
  /**
   * Construct a new instance of `InvocationContext`
   * @param parent - Parent context, such as the RequestContext
   * @param target - Target class (for static methods) or prototype/object
   * (for instance methods)
   * @param methodName - Method name
   * @param args - An array of arguments
   */
  constructor(
    parent: Context,
    public readonly target: object,
    public readonly methodName: string,
    public readonly args: InvocationArgs,
    public readonly source?: InvocationSource,
  ) {
    super(parent);
  }
  /**
   * The target class, such as `OrderController`
   */
  get targetClass() {
    return typeof this.target === 'function'
      ? this.target
      : this.target.constructor;
  }
  /**
   * The target name, such as `OrderController.prototype.cancelOrder`
   */
  get targetName() {
    return getTargetName(this.target, this.methodName);
  }
  /**
   * Description of the invocation
   */
  get description() {
    const source = this.source == null ? '' : `${this.source} => `;
    return `InvocationContext(${this.name}): ${source}${this.targetName}`;
  }
  toString() {
    return this.description;
  }
  /**
   * Assert the method exists on the target. An error will be thrown if otherwise.
   * @param context - Invocation context
   */
  assertMethodExists() {
    const targetWithMethods = this.target as Record<string, Function>;
    if (typeof targetWithMethods[this.methodName] !== 'function') {
      const targetName = getTargetName(this.target, this.methodName);
      assert(false, `Method ${targetName} not found`);
    }
    return targetWithMethods;
  }
  /**
   * Invoke the target method with the given context
   * @param context - Invocation context
   * @param options - Options for the invocation
   */
  invokeTargetMethod(
    options: InvocationOptions = {skipParameterInjection: true},
  ) {
    const targetWithMethods = this.assertMethodExists();
    if (!options.skipParameterInjection) {
      return invokeTargetMethodWithInjection(
        this,
        targetWithMethods,
        this.methodName,
        this.args,
        options.session,
      );
    }
    return invokeTargetMethod(
      this,
      targetWithMethods,
      this.methodName,
      this.args,
    );
  }
}
/**
 * Options to control invocations
 */
export type InvocationOptions = {
  /**
   * Skip dependency injection on method parameters
   */
  skipParameterInjection?: boolean;
  /**
   * Skip invocation of interceptors
   */
  skipInterceptors?: boolean;
  /**
   * Information about the source object that makes the invocation. For REST,
   * it's a `Route`. For injected proxies, it's a `Binding`.
   */
  source?: InvocationSource;
  /**
   * Resolution session
   */
  session?: ResolutionSession;
};
/**
 * Invoke a method using dependency injection. Interceptors are invoked as part
 * of the invocation.
 * @param target - Target of the method, it will be the class for a static
 * method, and instance or class prototype for a prototype method
 * @param method - Name of the method
 * @param ctx - Context object
 * @param nonInjectedArgs - Optional array of args for non-injected parameters
 * @param options - Options for the invocation
 */
export function invokeMethod(
  target: object,
  method: string,
  ctx: Context,
  nonInjectedArgs: InvocationArgs = [],
  options: InvocationOptions = {},
): ValueOrPromise<InvocationResult> {
  if (options.skipInterceptors) {
    if (options.skipParameterInjection) {
      // Invoke the target method directly without injection or interception
      return invokeTargetMethod(ctx, target, method, nonInjectedArgs);
    } else {
      return invokeTargetMethodWithInjection(
        ctx,
        target,
        method,
        nonInjectedArgs,
        options.session,
      );
    }
  }
  // Invoke the target method with interception but no injection
  return invokeMethodWithInterceptors(
    ctx,
    target,
    method,
    nonInjectedArgs,
    options,
  );
}
/**
 * Invoke a method. Method parameter dependency injection is honored.
 * @param target - Target of the method, it will be the class for a static
 * method, and instance or class prototype for a prototype method
 * @param method - Name of the method
 * @param ctx - Context
 * @param nonInjectedArgs - Optional array of args for non-injected parameters
 */
function invokeTargetMethodWithInjection(
  ctx: Context,
  target: object,
  method: string,
  nonInjectedArgs?: InvocationArgs,
  session?: ResolutionSession,
): ValueOrPromise<InvocationResult> {
  const methodName = getTargetName(target, method);
  /* istanbul ignore if */
  if (debug.enabled) {
    debug('Invoking method %s', methodName);
    if (nonInjectedArgs?.length) {
      debug('Non-injected arguments:', nonInjectedArgs);
    }
  }
  const argsOrPromise = resolveInjectedArguments(
    target,
    method,
    ctx,
    session,
    nonInjectedArgs,
  );
  const targetWithMethods = target as Record<string, Function>;
  assert(
    typeof targetWithMethods[method] === 'function',
    `Method ${method} not found`,
  );
  return transformValueOrPromise(argsOrPromise, args => {
    /* istanbul ignore if */
    if (debug.enabled) {
      debug('Injected arguments for %s:', methodName, args);
    }
    return invokeTargetMethod(ctx, targetWithMethods, method, args);
  });
}
/**
 * Invoke the target method
 * @param ctx - Context object
 * @param target - Target class or object
 * @param methodName - Target method name
 * @param args - Arguments
 */
function invokeTargetMethod(
  ctx: Context, // Not used
  target: object,
  methodName: string,
  args: InvocationArgs,
): InvocationResult {
  const targetWithMethods = target as Record<string, Function>;
  /* istanbul ignore if */
  if (debug.enabled) {
    debug('Invoking method %s', getTargetName(target, methodName), args);
  }
  // Invoke the target method
  const result = targetWithMethods[methodName](...args);
  /* istanbul ignore if */
  if (debug.enabled) {
    debug('Method invoked: %s', getTargetName(target, methodName), result);
  }
  return result;
}