UNPKG

@energica-city/shared-amplify-utils

Version:

Shared utilities for AWS Amplify projects

816 lines (650 loc) โ€ข 21.5 kB
# AWS Amplify Shared Utilities A TypeScript package providing standardized utilities for AWS Amplify projects with error handling, structured logging, type-safe database operations, and Lambda middleware. ## ๐ŸŽฏ Features - **Error Handling**: Standardized error throwing with structured logging and context - **Logging**: Environment-aware logging with AWS CloudWatch integration and Lambda detection - **Query Factory**: Type-safe CRUD operations for Amplify Data models - **Middleware System**: Composable middleware chain for Lambda functions and GraphQL resolvers - **REST APIs**: Complete REST API middleware with validation and error handling - **WebSocket Support**: Complete WebSocket API middleware with error handling and validation - **Request Validation**: Yup-based validation with built-in patterns ## ๐Ÿ“ฆ Installation ```bash npm install @your-org/amplify-shared-utilities ``` ## ๐Ÿ—๏ธ Architecture Overview This package is organized into four core modules, each with specific responsibilities: ### 1. Error Module (`/error`) **Purpose**: Centralized error handling with structured context and WebSocket-specific error types. **Key Components**: - `error.ts`: Core error utilities (`throwError`, `extractErrorMessage`, `createErrorContext`) - `websocket.ts`: WebSocket-specific error handling with predefined error codes - `index.ts`: Main exports for both REST and WebSocket error handling **Error Library Integration**: The middleware system automatically detects errors thrown by `throwError()` and preserves their context through the middleware chain. ### 2. Logging Module (`/log`) **Purpose**: Environment-aware structured logging with CloudWatch integration. **Key Features**: - Automatic environment detection (development, production, aws-lambda) - Structured JSON logging for CloudWatch - Context-aware logging with request IDs and user context - Lambda context capture (request ID, function name, X-Ray trace) ### 3. Query Factory (`/queries`) **Purpose**: Type-safe CRUD operations for Amplify Data models. **Key Components**: - `QueryFactory.ts`: Main factory for creating type-safe model operations - `ClientManager.ts`: Singleton client management with connection pooling - `initialize.ts`: One-time setup for Amplify client configuration - `types.ts`: Comprehensive TypeScript types for all operations ### 4. Middleware System (`/middleware`) **Purpose**: Composable middleware chains for different AWS services. **Sub-modules**: - **Core**: `middlewareChain.ts` - Generic middleware chain implementation - **REST**: Complete REST API middleware with validation and error handling - **GraphQL**: GraphQL resolver middleware with error handling - **WebSocket**: Complete WebSocket API middleware system - **Utils**: Validation patterns and sanitization utilities ## ๐Ÿšจ Error Handling ### Core Error Utilities ```typescript import { throwError, extractErrorMessage, createErrorContext, } from '@your-org/amplify-shared-utilities'; // Simple error with automatic context throwError('User not found'); // Error with structured context throwError( 'Database operation failed', createErrorContext({ userId: '123', operation: 'create', database: 'users', }), ); // Wrap existing error with context try { await databaseOperation(); } catch (error) { throwError('Database operation failed', error); } // Extract consistent error messages const message = extractErrorMessage(error); ``` ### WebSocket Error Handling ```typescript import { WebSocketErrors, WebSocketErrorCodes, throwWebSocketError, isWebSocketError, } from '@your-org/amplify-shared-utilities'; // Use predefined WebSocket error codes throwWebSocketError(WebSocketErrorCodes.INVALID_MESSAGE_FORMAT, { message: 'Invalid JSON format', receivedData: rawMessage, }); // Check if error is WebSocket-specific if (isWebSocketError(error)) { const { code, message, context } = extractWebSocketErrorInfo(error); // Handle WebSocket error specifically } ``` ### Error Context in Middleware The middleware system automatically preserves error context: ```typescript // Error thrown in middleware preserves all context const middleware = async (input, next) => { try { return await next(); } catch (error) { // Error automatically includes: // - middlewareName: 'auth' // - middlewareChain: ['auth', 'validation', 'handler'] // - All original error context from throwError() throw error; } }; ``` ## ๐Ÿ“ Logging ### Environment-Aware Logging ```typescript import { logger, LogLevel } from '@your-org/amplify-shared-utilities'; // Set context for all subsequent logs logger.setContext({ userId: '123', operation: 'create', requestId: 'req-456', }); // Log levels with automatic environment detection logger.error('Critical error', { errorCode: 500 }); logger.warn('Deprecated API usage'); logger.info('Operation completed', { result: data }); logger.debug('Debug info', { state: complexObject }); // Environment detection logger.getEnvironment(); // 'production', 'development', 'aws-lambda' logger.isStructuredLoggingEnabled(); // true in production/Lambda // Clear context when done logger.clearContext(); ``` ### Lambda Integration Automatically captures Lambda context in structured logs: ```typescript // In Lambda environment, logs include: { "timestamp": "2024-01-01T00:00:00.000Z", "level": "info", "message": "Operation completed", "awsRequestId": "req-123", "functionName": "myFunction", "xrayTraceId": "1-abc123-def456", "environment": "aws-lambda" } ``` ## ๐Ÿ—ƒ๏ธ Query Factory ### Initialization ```typescript import { QueryFactory, initializeQueries, } from '@your-org/amplify-shared-utilities'; import { MainTypes } from './schema'; import outputs from './amplify_outputs.json'; // Initialize once at startup await initializeQueries<MainTypes>(outputs); // Create type-safe QueryFactory for any model const UserQueries = await QueryFactory<'User', MainTypes>({ name: 'User', }); ``` ### CRUD Operations ```typescript // Create with type safety const user = await UserQueries.create({ input: { username: 'john', email: 'john@example.com', validated: false, }, }); // Get with automatic error handling const foundUser = await UserQueries.get({ input: { userId: user.id }, }); // List with filtering and pagination const users = await UserQueries.list({ filter: { validated: { eq: true } }, limit: 10, nextToken: 'next-page-token', }); // Update with partial data await UserQueries.update({ input: { userId: user.id, validated: true, lastLoginAt: new Date().toISOString(), }, }); // Delete with validation await UserQueries.delete({ input: { userId: user.id }, }); ``` ### Client Management ```typescript import { ClientManager } from '@your-org/amplify-shared-utilities'; // Get singleton client manager const manager = ClientManager.getInstance('default'); // Get initialized client const client = await manager.getClient<YourTypes>(); // Client is automatically configured with: // - Amplify Data client // - Authentication // - Environment-specific settings ``` ## ๐Ÿ”ง Middleware System ### Core Middleware Chain ```typescript import { MiddlewareChain } from '@your-org/amplify-shared-utilities'; // Create generic middleware chain const chain = new MiddlewareChain<MyInput, MyOutput>({ enableDebugLogging: true, onError: (error, middlewareName) => { console.error(`Middleware ${middlewareName} failed:`, error); }, }); // Add middleware in execution order chain .use('auth', authMiddleware) .use('logging', loggingMiddleware) .use('validation', validationMiddleware); // Execute with final handler const result = await chain.execute(input, async input => { return await businessLogic(input); }); ``` ### REST API Gateway Lambda Functions ```typescript import { createRestChain, wrapRestHandler, createRestErrorHandler, createRestRequestLogger, createRestRequestValidator, createRestModelInitializer, getValidatedBody, getModelsFromInput, getModelFromInput, ValidationPatterns, } from '@your-org/amplify-shared-utilities'; import * as yup from 'yup'; // Create REST middleware chain const chain = createRestChain<MainTypes>({ enableDebugLogging: true, }); // Add REST-specific middleware chain.use( 'errorHandler', createRestErrorHandler({ includeStackTrace: process.env.NODE_ENV === 'development', defaultContext: { service: 'message-api', version: '1.0.0' }, }), ); chain.use( 'requestLogger', createRestRequestLogger({ logEvent: true, logResponse: true, logTiming: true, logRequestBody: true, }), ); chain.use( 'modelInitializer', createRestModelInitializer({ schema: MainSchema, amplifyOutputs: outputs, entities: ['Message'], timeout: 5000, }), ); // Add validation middleware chain.use( 'validator', createRestRequestValidator({ bodySchema: yup.object({ content: yup.string().required(), type: yup.string().oneOf(['info', 'warning', 'error']).required(), targetId: yup.string().required(), }), stripUnknown: true, abortEarly: false, }), ); // Business logic handler const messageHandler = async input => { const MessageModel = getModelFromInput(input, 'Message'); const validatedBody = getValidatedBody(input.event); const message = await MessageModel.create({ input: validatedBody, }); return { statusCode: 201, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, data: message, }), }; }; // Wrap handler - single endpoint pattern export const handler = wrapRestHandler(chain, messageHandler); ``` ### REST Middleware Components #### Error Handler (`createRestErrorHandler`) ```typescript // Environment-aware error responses const errorHandler = createRestErrorHandler({ includeStackTrace: process.env.NODE_ENV === 'development', defaultContext: { service: 'api' }, forceStructuredLogging: true, }); // Automatically handles: // - Development: Full error details with stack traces // - Production: Sanitized error messages // - Request ID tracking // - Structured error context ``` #### Request Validator (`createRestRequestValidator`) ```typescript // Validate request components const validator = createRestRequestValidator({ bodySchema: yup.object({ name: yup.string().required(), email: yup.string().email().required(), }), queryParametersSchema: yup.object({ page: yup.number().positive().optional(), limit: yup.number().positive().max(100).optional(), }), pathParametersSchema: yup.object({ id: yup.string().uuid().required(), }), headersSchema: yup.object({ authorization: yup.string().required(), }), stripUnknown: true, abortEarly: false, validateOnlyOnMethods: ['POST', 'PUT', 'PATCH'], }); // Access validated data in handlers const validatedBody = getValidatedBody(event); const validatedQuery = getValidatedQuery(event); const validatedPath = getValidatedPath(event); const validatedHeaders = getValidatedHeaders(event); ``` #### Model Initializer (`createRestModelInitializer`) ```typescript // Initialize Amplify Data models const modelInitializer = createRestModelInitializer({ schema: MainSchema, amplifyOutputs: outputs, entities: ['User', 'Post', 'Comment'], clientKey: 'default', timeout: 5000, }); // Access models in handlers const models = getModelsFromInput(input); const UserModel = getModelFromInput(input, 'User'); const PostModel = getModelFromInput(input, 'Post'); ``` #### Request Logger (`createRestRequestLogger`) ```typescript // Structured request/response logging const logger = createRestRequestLogger({ logEvent: true, logResponse: true, logTiming: true, maxDepth: 3, excludeEventFields: ['body', 'headers'], excludeResponseFields: ['body'], defaultContext: { service: 'api' }, logLevel: 'info', logRequestBody: true, forceStructuredLogging: true, }); ``` ### Single Endpoint Pattern The REST middleware is optimized for AWS API Gateway's single Lambda per resource pattern: ```typescript // Perfect for CDK REST API deployment // One Lambda function per resource endpoint // Each function handles one HTTP method (typically POST) // API Gateway handles CORS and method routing const chain = createRestChain<MessageTypes>(); chain.use('errorHandler', createRestErrorHandler()); chain.use('logger', createRestRequestLogger()); chain.use('validator', createRestRequestValidator({ bodySchema })); const handler = wrapRestHandler(chain, businessLogicHandler); ``` ### GraphQL Resolvers ```typescript import { MiddlewareChain, wrapGraphQLResolver, createGraphQLErrorHandler, } from '@your-org/amplify-shared-utilities'; // Create GraphQL-specific middleware chain const chain = MiddlewareChain.createGraphQLChain< { id: string }, unknown, { id: string; name: string } >(); chain.use( 'graphQLErrorHandler', createGraphQLErrorHandler({ includeStackTrace: process.env.NODE_ENV === 'development', defaultContext: { service: 'product-api', version: '1.0.0' }, }), ); const mainHandler = async (source: any, args: any, context: any, info: any) => { const { productId } = args; return await getProductById(productId); }; export const handler = wrapGraphQLResolver(chain, mainHandler); ``` ### WebSocket APIs ```typescript import { createWebSocketChain, wrapWebSocketHandler, createWebSocketErrorHandler, createWebSocketRequestLogger, createWebSocketRequestValidator, createWebSocketModelInitializer, getModelFromInput, WebSocketErrorCodes, } from '@your-org/amplify-shared-utilities'; import * as yup from 'yup'; // Create WebSocket middleware chain const chain = createWebSocketChain<MainTypes>({ enableDebugLogging: true, }); // Add WebSocket-specific middleware chain.use( 'errorHandler', createWebSocketErrorHandler({ includeStackTrace: process.env.NODE_ENV === 'development', }), ); chain.use( 'requestLogger', createWebSocketRequestLogger({ logLevel: 'info', includeBody: true, }), ); chain.use( 'modelInitializer', createWebSocketModelInitializer({ models: ['User', 'Message'], }), ); chain.use( 'validator', createWebSocketRequestValidator({ messageSchema: yup.object({ action: yup.string().required(), data: yup.object().required(), }), }), ); // WebSocket handler with model access const webSocketHandler = async input => { const { event, models } = input; // Access type-safe models const UserModel = getModelFromInput(input, 'User'); const MessageModel = getModelFromInput(input, 'Message'); // Handle different WebSocket events switch (event.requestContext.routeKey) { case '$connect': return { statusCode: 200 }; case '$disconnect': return { statusCode: 200 }; case 'sendMessage': const message = await MessageModel.create({ input: { content: event.body.data.content, userId: event.requestContext.authorizer.userId, }, }); return { statusCode: 200, body: JSON.stringify(message) }; default: throw new Error(`Unknown route: ${event.requestContext.routeKey}`); } }; export const handler = wrapWebSocketHandler(chain, webSocketHandler); ``` ### Built-in Validation Patterns ```typescript import { ValidationPatterns } from '@your-org/amplify-shared-utilities'; // Common validation patterns ValidationPatterns.idParam(); // UUID path parameter ValidationPatterns.email(); // Email validation ValidationPatterns.pagination(); // Page/limit query params ValidationPatterns.dateRange(); // Start/end date validation ``` ## ๐Ÿงช Testing Patterns ### Middleware Testing ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { MiddlewareChain } from '@your-org/amplify-shared-utilities'; // Mock dependencies vi.mock('../log', () => ({ logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn(), }, })); describe('Middleware Chain', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should execute middleware in order', async () => { const chain = new MiddlewareChain(); const executionOrder: string[] = []; const middleware1 = vi.fn().mockImplementation(async (input, next) => { executionOrder.push('before-1'); const result = await next(); executionOrder.push('after-1'); return result; }); const middleware2 = vi.fn().mockImplementation(async (input, next) => { executionOrder.push('before-2'); const result = await next(); executionOrder.push('after-2'); return result; }); chain.use('first', middleware1); chain.use('second', middleware2); const result = await chain.execute('input', async () => { executionOrder.push('handler'); return 'success'; }); expect(executionOrder).toEqual([ 'before-1', 'before-2', 'handler', 'after-2', 'after-1', ]); expect(result).toBe('success'); }); it('should enhance errors with middleware context', async () => { const chain = new MiddlewareChain(); const middleware = vi.fn().mockRejectedValue(new Error('Test error')); chain.use('test', middleware); try { await chain.execute('input', async () => 'success'); expect.fail('Should have thrown'); } catch (error: any) { expect(error.middlewareName).toBe('test'); expect(error.middlewareChain).toEqual(['test']); expect(error.message).toBe('Test error'); } }); }); ``` ### Error Library Integration Testing ```typescript import { throwError, createErrorContext, } from '@your-org/amplify-shared-utilities'; it('should preserve error library context through middleware', async () => { const chain = new MiddlewareChain(); const middleware = vi.fn().mockImplementation(async () => { throwError( 'Database connection failed', createErrorContext({ operation: 'getUser', userId: '123', database: 'users', }), ); }); chain.use('database', middleware); try { await chain.execute('input', async () => 'success'); expect.fail('Should have thrown'); } catch (error: any) { expect(error.__fromErrorLibrary).toBe(true); expect(error.operation).toBe('getUser'); expect(error.userId).toBe('123'); expect(error.database).toBe('users'); expect(error.middlewareName).toBe('database'); } }); ``` ## โš™๏ธ Configuration ### Environment Variables - `LOG_LEVEL`: Log level (0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) - `STRUCTURED_LOGGING`: Force JSON logging ('true'/'false') - `NODE_ENV`: Environment detection - `environment`: Explicit environment setting ('prod' for production) ### AWS Lambda Auto-Detection Automatically detects Lambda environment and: - Enables structured JSON logging for CloudWatch - Captures Lambda context (request ID, function name, X-Ray trace) - Optimizes logging format for CloudWatch Logs Insights - Detects production environment from function name patterns ## ๐Ÿ“‹ Requirements - Node.js 22+ - TypeScript 5.6+ - AWS Amplify 6.0+ - Yup 1.6+ (for validation) ## ๐Ÿ”„ Development Workflow ### Adding New Features 1. **Follow the module structure**: Each module has its own directory with `index.ts` exports 2. **Add comprehensive tests**: Co-locate tests with source files, use Vitest 3. **Update exports**: Ensure all new functionality is exported from module `index.ts` 4. **Update main exports**: Add to root `index.ts` if needed 5. **Follow TypeScript standards**: No `any` types, use strict mode ### Testing Guidelines - Use Vitest for all testing - Mock external dependencies with `vi.mock` - Test both success and error scenarios - Verify error context preservation - Test middleware chain execution order - Use `expect().rejects.toThrow()` for error cases ### Code Quality - Run `npm run lint:check` before committing - Follow ESLint rules (no `any`, max complexity 15, etc.) - Use Prettier for formatting - Maintain backward compatibility - Add JSDoc comments for public APIs ## ๐Ÿš€ Performance Considerations - **Middleware Chains**: Each execution creates a closure chain - reuse chains for high-frequency operations - **Client Management**: ClientManager uses singleton pattern for connection reuse - **Logging**: Debug logging adds overhead - disable in production - **Error Context**: Large context objects are filtered to remove undefined values - **Validation**: Use `stripUnknown: true` in validation to reduce payload size ## ๐Ÿ”ง Troubleshooting ### Common Issues 1. **"Model not found" errors**: Ensure `initializeQueries()` is called before using QueryFactory 2. **Middleware not executing**: Check middleware order and ensure `next()` is called 3. **Error context missing**: Use `throwError()` instead of `new Error()` for automatic context 4. **Logging not structured**: Check environment detection and `STRUCTURED_LOGGING` variable ### Debug Mode Enable debug logging for middleware chains: ```typescript const chain = new MiddlewareChain({ enableDebugLogging: true, onError: (error, middlewareName) => { console.error(`Middleware ${middlewareName} failed:`, error); }, }); ``` This comprehensive documentation provides detailed instructions for an AI to understand and work with this codebase, including architecture patterns, testing approaches, and development workflows.