UNPKG

contexify

Version:

A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.

279 lines (260 loc) 7.6 kB
import assert from 'assert'; import { DecoratorFactory } from 'metarize'; import { Context } from '../context/context.js'; import type { ResolutionSession } from '../resolution/resolution-session.js'; import { resolveInjectedArguments } from '../resolution/resolver.js'; import createDebugger from '../utils/debug.js'; import { transformValueOrPromise, type ValueOrPromise, } from '../utils/value-promise.js'; import { invokeMethodWithInterceptors } from './interceptor.js'; const debug = createDebugger('contexify: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, (...args: unknown[]) => unknown >; 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); } 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, (...args: unknown[]) => unknown >; 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, (...args: unknown[]) => unknown >; /* 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; }