UNPKG

@energica-city/shared-amplify-utils

Version:

Shared utilities for AWS Amplify projects

210 lines 7.91 kB
import { logger } from '../../log'; import { WebSocketErrors } from '../../error'; import * as yup from 'yup'; import { buildWebSocketContext, parseJsonBody, isMessageEvent } from './utils'; /** * Symbol used to store validated message data on the WebSocket event * @internal */ const VALIDATED_MESSAGE_KEY = Symbol('validatedMessage'); /** * Retrieve validated message data from a WebSocket event * * This function extracts the validated message data that was stored by the * WebSocket request validator middleware. Returns undefined if no validation * was performed or if validation failed. * * @template T - Type of the validated message data * @param event - The WebSocket event that may contain validated data * @returns The validated message data, or undefined if not available * * @example * ```typescript * interface MessageData { * action: string; * payload: Record<string, unknown>; * } * * const validatedMessage = getValidatedMessage<MessageData>(event); * if (validatedMessage) { * console.log('Action:', validatedMessage.action); * } * ``` */ export function getValidatedMessage(event) { return event[VALIDATED_MESSAGE_KEY]; } /** * Extract detailed error information from Yup validation errors * * Converts Yup's nested validation errors into a standardized format * for consistent error reporting across the application. * * @param error - The Yup validation error to process * @returns Array of validation error details * @internal */ function extractErrors(error) { return error.inner.map(innerError => ({ field: innerError.path || 'unknown', message: innerError.message, value: innerError.value, type: innerError.type || 'validation', })); } /** * Determine if validation should be performed for the current request * * Checks various conditions to decide whether to validate the WebSocket message: * - Must be a MESSAGE event (not CONNECT/DISCONNECT) * - Must have a validation schema configured * - Must have a message body * - Route must be in the validation allowlist (if specified) * * @template TTypes - Available model types * @template TSelected - Selected model types for this request * @param input - The WebSocket input with models * @param bodySchema - The Yup schema for validation (undefined if no validation) * @param validateOnlyOnRoutes - Array of routes that require validation * @returns Object with validation decision and optional reason * @internal */ function shouldValidate(input, bodySchema, validateOnlyOnRoutes) { const { event } = input; if (!isMessageEvent(event)) { return { shouldValidate: false, reason: 'Not a MESSAGE event' }; } if (!bodySchema) { return { shouldValidate: false, reason: 'No validation schema' }; } const bodyStr = event.body; if (!bodyStr) { return { shouldValidate: false, reason: 'No message body' }; } if (validateOnlyOnRoutes.length > 0 && !validateOnlyOnRoutes.includes(event.requestContext ?.routeKey ?? '')) { return { shouldValidate: false, reason: `Route ${event.requestContext?.routeKey ?? 'unknown'} not in validation list`, }; } return { shouldValidate: true }; } /** * Create a WebSocket request validation middleware * * This middleware validates incoming WebSocket message bodies against a Yup schema. * It only validates MESSAGE events (not CONNECT/DISCONNECT) and can be configured * to validate only specific routes. * * **Validation Process:** * 1. Checks if validation should be performed (MESSAGE event, has schema, etc.) * 2. Parses the JSON message body * 3. Validates against the provided Yup schema * 4. Stores validated data on the event for later retrieval * 5. Continues to next middleware with original input * * **Error Handling:** * - JSON parsing errors: Returns BAD_REQUEST with context * - Validation errors: Returns BAD_REQUEST with validation details * - Other errors: Returns INTERNAL_SERVER_ERROR * * **Validated Data Access:** * Use `getValidatedMessage<T>(event)` to retrieve validated data in subsequent middleware. * * @template TTypes - Record of available Amplify model types * @template TSelected - Selected model types for this middleware chain * @template TOutput - Expected output type of the middleware chain * @param config - Configuration options for validation behavior * @returns Middleware function for WebSocket request validation * * @example * ```typescript * import * as yup from 'yup'; * * const messageSchema = yup.object({ * action: yup.string().required(), * payload: yup.object().required(), * timestamp: yup.number().optional(), * }); * * const validator = createWebSocketRequestValidator({ * bodySchema: messageSchema, * validateOnlyOnRoutes: ['sendMessage', 'updateStatus'], * stripUnknown: true, * errorMessage: 'Invalid message format', * }); * * chain.use('validation', validator); * ``` * * @example * ```typescript * // In your handler, retrieve validated data: * const handler = async (input) => { * const validatedMessage = getValidatedMessage<MessageData>(input.event); * if (validatedMessage) { * // Process validated message * return { statusCode: 200 }; * } * }; * ``` */ export function createWebSocketRequestValidator(config) { const { bodySchema, stripUnknown = true, abortEarly = false, errorMessage = 'Validation failed', errorContext = {}, validateOnlyOnRoutes = ['$default'], logValidationSkipped = false, } = config; return async (input, next) => { const { event } = input; const validationDecision = shouldValidate(input, bodySchema, validateOnlyOnRoutes); if (!validationDecision.shouldValidate) { if (logValidationSkipped) { const ctx = buildWebSocketContext(input); logger.debug('WebSocket validation skipped', { connectionId: ctx.connectionId, routeKey: event .requestContext?.routeKey, reason: validationDecision.reason, }); } return await next(input); } // Validate only this block; do NOT catch downstream handler errors try { const context = buildWebSocketContext(input); const bodyStr = event.body; const messageData = parseJsonBody(bodyStr, context); if (messageData === null) { const connId = event .requestContext?.connectionId; const routeKey = event .requestContext?.routeKey; const ctx = { ...(connId ? { connectionId: connId } : {}), ...(routeKey ? { routeKey } : {}), }; throw WebSocketErrors.badRequest('Invalid JSON in message body', ctx); } const validatedData = await bodySchema.validate(messageData, { stripUnknown, abortEarly, }); event[VALIDATED_MESSAGE_KEY] = validatedData; } catch (error) { const context = buildWebSocketContext(input, errorContext); if (error instanceof yup.ValidationError) { const validationErrors = extractErrors(error); throw WebSocketErrors.badRequest(errorMessage, { ...context, validationErrors, field: validationErrors[0]?.field, }); } // Re-throw unexpected errors during validation phase throw error; } // Run next outside of the validation try/catch so chain errors propagate return await next(input); }; } //# sourceMappingURL=WebSocketRequestValidator.js.map