@energica-city/shared-amplify-utils
Version:
Shared utilities for AWS Amplify projects
816 lines (650 loc) โข 21.5 kB
Markdown
# 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.