UNPKG

@stone-js/pipeline

Version:

An implementation based on the Chain of Responsibility (aka CoR) design pattern.

362 lines (358 loc) 12.7 kB
/** * Define a new middleware for the pipeline. * * @param module - The pipe module to add to the pipeline. * @param options - Additional options for the middleware. * @returns The metadata for the middleware. */ const defineMiddleware = (module, options = {}) => { return { ...options, module }; }; /** * Check if the value is a string. * * @param value - The value to check. * @returns `true` if the value is an string, otherwise `false`. */ const isString = (value) => typeof value === 'string'; /** * Check if the value is a function. * * @param value - The value to check. * @returns `true` if the value is a function, otherwise `false`. */ const isFunction = (value) => typeof value === 'function'; /** * Checks if the given value is a constructor function. * * @param value - The value to be checked. * @returns True if the value is a constructor function, false otherwise. */ const isConstructor = (value) => { return isFunction(value) && Object.prototype.hasOwnProperty.call(value, 'prototype'); }; /** * Check if the meta pipe is a function pipe. * * @param metaPipe - The meta pipe to check. * @returns `true` if the meta pipe is a function pipe, otherwise `false`. */ const isFunctionPipe = (metaPipe) => { return !isAliasPipe(metaPipe) && !isClassPipe(metaPipe) && !isFactoryPipe(metaPipe); }; /** * Check if the meta pipe is an alias pipe. * * @param metaPipe - The meta pipe to check. * @returns `true` if the meta pipe is an alias pipe, otherwise `false`. */ const isAliasPipe = (metaPipe) => { return metaPipe.isAlias === true || isString(metaPipe.module); }; /** * Check if the meta pipe is a class pipe. * * @param metaPipe - The meta pipe to check. * @returns `true` if the meta pipe is a class pipe, otherwise `false`. */ const isClassPipe = (metaPipe) => { return metaPipe.isClass === true && isFunction(metaPipe.module) && isConstructor(metaPipe.module); }; /** * Check if the meta pipe is a factory pipe. * * @param metaPipe - The meta pipe to check. * @returns `true` if the meta pipe is a factory pipe, otherwise `false`. */ const isFactoryPipe = (metaPipe) => { return metaPipe.isFactory === true && isFunction(metaPipe.module); }; /** * Custom error for pipeline operations. */ class PipelineError extends Error { constructor(message) { super(message); this.name = 'PipelineError'; } } /** * Class representing a Pipeline. * * @template T - The type of the passable object in the pipeline. * @template R - The type of the return value from the pipeline execution. * * This class is responsible for managing and executing a series of operations * on a set of passable values through multiple configurable pipes. */ class Pipeline { /** The passable objects sent through the pipeline */ passable; /** The method name to call on each pipe */ method; /** Flag indicating whether the pipeline should run synchronously or asynchronously */ isSync; /** The default priority for the pipes in the pipeline */ _defaultPriority; /** The sorted metadata pipes that will be executed */ sortedMetaPipes; /** The pipeline hooks */ hooks; /** The resolver function used to resolve pipes before they are executed in the pipeline. */ resolver; /** * Create a pipeline instance. * * @param options - Optional Pipeline options. * @returns The pipeline instance. */ static create(options) { return new this(options); } /** * Initialize a new Pipeline instance. * * @param options - Optional Pipeline options. */ constructor(options) { this.isSync = false; this.method = 'handle'; this.sortedMetaPipes = []; this._defaultPriority = 10; this.resolver = options?.resolver; this.hooks = options?.hooks ?? {}; } /** * Set the default priority for pipes in the pipeline. * * @param value - The priority value to set. * @returns The current Pipeline instance. */ defaultPriority(value) { this._defaultPriority = value; return this; } /** * Set the passable objects being sent through the pipeline. * * @param passable - The object to pass through the pipeline. * @returns The current Pipeline instance. */ send(passable) { this.passable = passable; return this; } /** * Set the pipes for the pipeline. * * @param pipes - The pipes or MetaPipe instances. * @returns The current Pipeline instance. */ through(...pipes) { const priority = this._defaultPriority; const metaPipes = pipes.map(pipe => ((isString(pipe) || isFunction(pipe)) ? { module: pipe, priority, isAlias: isString(pipe) } : { priority, ...pipe })); this.sortedMetaPipes = Array .from(metaPipes.reduce((acc, pipe) => acc.set(pipe.module, pipe), new Map()).values()) .sort((a, b) => a.priority !== undefined && b.priority !== undefined ? a.priority - b.priority : 0) .reverse(); return this; } /** * Add additional pipes to the pipeline. * * @param {...MixedPipe} pipe - A single pipe or a list of pipes to add. * @returns The current Pipeline instance. */ pipe(...pipe) { return this.through(...this.sortedMetaPipes, ...pipe); } /** * Set the method to call on each pipe. * * @param method - The method name to use on each pipe. * @returns The current Pipeline instance. */ via(method) { this.method = method; return this; } /** * Configure the pipeline to run synchronously or asynchronously. * * @param value - Set true for sync, false for async (default is true). * @returns The current Pipeline instance. */ sync(value = true) { this.isSync = value; return this; } /** * Add a hook to the pipeline. * * @param name - The name of the hook. * @param listener - The hook listener function. * @returns The current Pipeline instance. */ on(name, listener) { this.hooks[name] ??= []; this.hooks[name] = this.hooks[name].concat(listener); return this; } /** * Run the pipeline with a final destination callback. * * @param destination - The final function to execute. * @returns The result of the pipeline, either synchronously or as a Promise. */ then(destination) { if (this.passable === undefined) { throw new PipelineError('No passable object has been set for this pipeline.'); } return this .sortedMetaPipes .reduce(this.isSync ? this.reducer() : this.asyncReducer(), destination.bind(destination))(this.passable); } /** * Run the pipeline and return the result. * * @returns The result of the pipeline, either synchronously or as a Promise. */ thenReturn() { return this.then((passable) => passable); } /** * Get the asynchronous reducer to iterate over the pipes. * * @returns The asynchronous reducer callback. */ asyncReducer() { return (previousPipeExecutor, currentPipe) => { return async (passable) => { return await this.executeAsyncPipe(currentPipe, passable, previousPipeExecutor); }; }; } /** * Get the synchronous reducer to iterate over the pipes. * * @returns The synchronous reducer callback. */ reducer() { return (previousPipeExecutor, currentPipe) => { return (passable) => { return this.executePipe(currentPipe, passable, previousPipeExecutor); }; }; } /** * Resolve and execute async pipe. * * @param currentPipe - The current pipe to execute (class or service alias string). * @param passable - The passable object to send through the pipe. * @param previousPipeExecutor - The previous pipe executor in the sequence. * @returns The result of the pipe execution. * @throws PipelineError If the pipe cannot be resolved or the method is missing. */ async executeAsyncPipe(currentPipe, passable, previousPipeExecutor) { const instance = this.resolvePipe(currentPipe); await this.executeAsyncHooks('onProcessingPipe', currentPipe, instance, passable); const result = await instance[this.method](passable, previousPipeExecutor, ...(currentPipe.params ?? [])); await this.executeAsyncHooks('onPipeProcessed', currentPipe, instance, passable); return result; } /** * Resolve and execute a pipe. * * @param currentPipe - The current pipe to execute (class or service alias string). * @param passable - The passable object to send through the pipe. * @param previousPipeExecutor - The previous pipe executor in the sequence. * @returns The result of the pipe execution. * @throws PipelineError If the pipe cannot be resolved or the method is missing. */ executePipe(currentPipe, passable, previousPipeExecutor) { const instance = this.resolvePipe(currentPipe); this.executeHooks('onProcessingPipe', currentPipe, instance, passable); const result = instance[this.method](passable, previousPipeExecutor, ...(currentPipe.params ?? [])); this.executeHooks('onPipeProcessed', currentPipe, instance, passable); return result; } /** * Resolve pipe. * * @param currentPipe - The current pipe to execute (class or service alias string). * @returns The resolved pipe instance. * @throws PipelineError If the pipe cannot be resolved or the method is missing. */ resolvePipe(currentPipe) { let instance = (isFunction(this.resolver) ? this.resolver(currentPipe) : undefined); if (instance === undefined) { instance = this.createInstanceFromPipe(currentPipe); if (instance === undefined) { throw new PipelineError(`Cannot resolve this pipe ${String(currentPipe)}.`); } } else if (isFunction(instance)) { instance = { [this.method]: instance }; } this.validatePipeMethod(instance, currentPipe); return instance; } /** * Create an instance from the provided pipe. * * @param currentPipe - The pipe function to create an instance from. * @returns The created instance or an object with the method. */ createInstanceFromPipe(currentPipe) { if (isFunction(currentPipe.module)) { if (isClassPipe(currentPipe)) { return new currentPipe.module.prototype.constructor(...[]); } else if (isFactoryPipe(currentPipe)) { return { [this.method]: currentPipe.module(...[]) }; } else if (isFunctionPipe(currentPipe)) { return { [this.method]: currentPipe.module }; } } } /** * Validate that the required method exists on the instance. * * @param instance - The instance to validate. * @param currentPipe - The current pipe being executed. * @throws {PipelineError} If the method does not exist on the instance. */ validatePipeMethod(instance, currentPipe) { if (!isFunction(instance[this.method])) { throw new PipelineError(`No method with this name(${this.method}) exists in this constructor(${currentPipe.module.constructor.name})`); } } /** * Execute lifecycle async hooks. * * @param name - The hook's name. * @param pipe - The current pipe instance. */ async executeAsyncHooks(name, pipe, instance, passable) { if (Array.isArray(this.hooks[name])) { for (const listener of this.hooks[name]) { await listener({ passable, instance, pipe, pipes: this.sortedMetaPipes }); } } } /** * Execute lifecycle hooks. * * @param name - The hook's name. * @param pipe - The current pipe instance. */ executeHooks(name, pipe, instance, passable) { if (Array.isArray(this.hooks[name])) { for (const listener of this.hooks[name]) { listener({ passable, instance, pipe, pipes: this.sortedMetaPipes }); } } } } export { Pipeline, PipelineError, defineMiddleware, isAliasPipe, isClassPipe, isConstructor, isFactoryPipe, isFunction, isFunctionPipe, isString };