UNPKG

@energica-city/shared-amplify-utils

Version:

Shared utilities for AWS Amplify projects

247 lines 9.2 kB
// Future Enhancement: Response Transformation Support // Planned features for response processing: // - useResponseTransformer() method for response-only transformations // - useFull() method for combined request/response middleware // - ResponseTransformer<TInput, TOutput> type for type-safe response modifications // These features will provide cleaner patterns for response processing (timestamps, compression, headers, etc.) /** * Generic middleware chain implementation * * Provides Express-style middleware functionality for AWS Lambda handlers. * Middleware executes in an "onion model" where each middleware wraps the next one in the chain. * * **Execution Flow:** * ``` * Middleware 1 (before) → * Middleware 2 (before) → * Final Handler → * Middleware 2 (after) ← * Middleware 1 (after) ← * ``` * * **Input Mutation:** * - Middleware can mutate the input object directly * - Changes are visible to all subsequent middleware and the final handler * - Consider input immutability for complex applications * * **Error Handling:** * - Errors thrown by middleware are enhanced with chain context * - Execution stops at the first error (no subsequent middleware execute) * - Configure `onError` handler for centralized error processing * * **Performance:** * - Each execution creates a closure chain - consider reusing chains for high-frequency operations * - Debug logging adds overhead - disable in production * * @template TInput - Type of input data passed through the chain * @template TOutput - Type of output returned by the chain * * @example * ```typescript * const chain = new MiddlewareChain<MyInput, MyOutput>({ * enableDebugLogging: true, * onError: (error, middlewareName) => { * console.error(`Middleware ${middlewareName} failed:`, error); * } * }); * * chain * .use('auth', authMiddleware) * .use('logging', loggingMiddleware) * .use('validation', validationMiddleware); * * const result = await chain.execute(input, finalHandler); * ``` */ export class MiddlewareChain { middlewares = []; config; constructor(config = {}) { this.config = config; } /** * Create a middleware chain specifically for AWS Lambda handlers * * This is a convenience method that creates a chain with the standard * Lambda input structure: `{ event: TEvent; context: TContext }` * * @template TEvent - Type of the Lambda event * @template TContext - Type of the Lambda context * @template TReturn - Type of the Lambda return value * @param config - Configuration options for the chain * @returns A new middleware chain configured for Lambda handlers * * @example * ```typescript * const chain = MiddlewareChain.createLambdaChain<APIGatewayProxyEvent, Context, APIGatewayProxyResult>(); * ``` */ static createLambdaChain(config = {}) { return new MiddlewareChain(config); } /** * Add middleware to the chain * * Middleware functions are executed in the order they are added. * Each middleware receives the input and a `next` function to continue * to the next middleware or final handler. * * @param name - Descriptive name for the middleware (used in error messages and logging) * @param middleware - The middleware function to add * @returns This chain instance for method chaining * * @example * ```typescript * chain * .use('authentication', authMiddleware) * .use('logging', loggingMiddleware) * .use('validation', validationMiddleware); * ``` */ use(name, middleware) { this.middlewares.push({ name, middleware }); return this; } /** * Execute the middleware chain with the given input and final handler * * This method runs all middleware in the chain, followed by the final handler. * If any middleware throws an error, execution stops and the error is enhanced * with chain context before being re-thrown. * * @param input - The input data to pass through the middleware chain * @param finalHandler - The final handler function that processes the input * @returns Promise resolving to the output from the final handler * * @example * ```typescript * const result = await chain.execute( * { userId: '123', data: { name: 'John' } }, * async (input) => { * return await fetchUserData(input.userId); * } * ); * ``` */ async execute(input, finalHandler) { if (this.middlewares.length === 0) { return finalHandler(input); } let index = 0; const executeNext = async (modifiedInput) => { if (index >= this.middlewares.length) { // We're now in the final handler - any errors should not be attributed to middleware return finalHandler(modifiedInput ?? input); } const { name, middleware } = this.middlewares[index]; const currentIndex = index; index++; try { return await middleware(modifiedInput ?? input, executeNext); } catch (error) { // Only enhance error if it doesn't already have middleware context // and it came from middleware code (not bubbled up from handler) const isAlreadyMiddlewareError = error instanceof Error && 'middlewareName' in error && typeof error.middlewareName === 'string'; const isFromErrorLibrary = error instanceof Error && '__fromErrorLibrary' in error && error .__fromErrorLibrary === true; // If it's already been processed by error library or is already a middleware error, // don't add middleware context (it came from handler/downstream) if (isAlreadyMiddlewareError || isFromErrorLibrary) { throw error; } // This is a genuine middleware error - enhance it const enhancedError = error instanceof Error ? error : new Error(String(error)); enhancedError.middlewareName = name; enhancedError.middlewareIndex = currentIndex; enhancedError.totalMiddlewares = this.middlewares.length; enhancedError.middlewareChain = this.middlewares.map(m => m.name); // Store original error if we created a new Error object if (!(error instanceof Error)) { enhancedError.originalError = error; } if (this.config.onError) { this.config.onError(enhancedError, name); } throw enhancedError; } }; return executeNext(); } /** * Get the number of middlewares in the chain * * @returns The count of middleware functions currently in the chain * * @example * ```typescript * const chain = new MiddlewareChain(); * console.log(chain.length); // 0 * * chain.use('auth', authMiddleware); * chain.use('logging', loggingMiddleware); * console.log(chain.length); // 2 * ``` */ get length() { return this.middlewares.length; } /** * Clear all middlewares from the chain * * Removes all middleware functions, resetting the chain to empty state. * Useful for reusing chain instances or cleaning up during testing. * * @returns This chain instance for method chaining * * @example * ```typescript * const chain = new MiddlewareChain(); * chain.use('auth', authMiddleware); * chain.use('logging', loggingMiddleware); * * chain.clear(); // Chain is now empty * console.log(chain.length); // 0 * ``` */ clear() { this.middlewares = []; return this; } } /** * Wrap a Lambda handler with middleware chain functionality * * This function creates a new Lambda handler that executes the middleware chain * before calling the original handler. The middleware chain receives the Lambda * event and context as input. * * @template TEvent - Type of the Lambda event * @template TContext - Type of the Lambda context * @template TReturn - Type of the Lambda return value * @param chain - The middleware chain to execute * @param handler - The original Lambda handler function * @returns A new Lambda handler function that includes middleware execution * * @example * ```typescript * const wrappedHandler = wrapLambdaHandler( * chain, * async (event, context) => { * return { statusCode: 200, body: 'Hello World' }; * } * ); * ``` */ export function wrapLambdaHandler(chain, handler) { return async (event, context) => { return await chain.execute({ event, context }, async (input) => { return await handler(input.event, input.context); }); }; } //# sourceMappingURL=middlewareChain.js.map