@energica-city/shared-amplify-utils
Version:
Shared utilities for AWS Amplify projects
247 lines • 9.2 kB
JavaScript
// 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