UNPKG

@noony-serverless/core

Version:

A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript

330 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createConsolidatedValidationMiddleware = exports.ConsolidatedValidationMiddleware = void 0; const zod_1 = require("zod"); const core_1 = require("../core"); /** * Consolidated ValidationMiddleware that combines body validation, header validation, and query parameter validation. * * This middleware replaces the need for separate: * - BodyValidationMiddleware * - ValidationMiddleware * - HeaderVariablesMiddleware * * @example * Complete validation setup: * ```typescript * const createUserSchema = z.object({ * name: z.string().min(1).max(100), * email: z.string().email(), * age: z.number().min(18).max(120) * }); * * const handler = new Handler() * .use(new ConsolidatedValidationMiddleware({ * body: { * schema: createUserSchema, * required: true, * maxSize: 1024 * 1024 // 1MB * }, * headers: { * required: ['authorization', 'content-type'], * optional: ['x-trace-id', 'user-agent'], * patterns: { * 'x-trace-id': /^[a-f0-9-]{36}$/ * } * }, * query: { * allowedParams: ['page', 'limit', 'sort'], * requiredParams: ['page'], * parseTypes: { * page: 'number', * limit: 'number' * } * } * })) * .handle(async (context) => { * // context.req.validatedBody is typed as z.infer<typeof createUserSchema> * const { name, email, age } = context.req.validatedBody!; * return { message: `Creating user ${name}` }; * }); * ``` */ class ConsolidatedValidationMiddleware { config; constructor(config = {}) { this.config = { body: { required: false, allowEmpty: true }, headers: { caseSensitive: false }, query: { maxParams: 100 }, ...config, }; } async before(context) { // Skip validation if custom skip function returns true if (this.config.skipValidation && this.config.skipValidation(context)) { return; } // 1. Validate headers first (early failure) if (this.config.headers) { await this.validateHeaders(context); } // 2. Validate query parameters if (this.config.query) { await this.validateQueryParameters(context); } // 3. Validate request body (most expensive, do last) if (this.config.body) { await this.validateBody(context); } } async validateHeaders(context) { const headerConfig = this.config.headers; const headers = context.req.headers || {}; // Normalize headers for case-insensitive comparison if needed const normalizedHeaders = headerConfig.caseSensitive ? headers : this.normalizeHeaders(headers); // Check required headers if (headerConfig.required) { for (const requiredHeader of headerConfig.required) { const headerKey = headerConfig.caseSensitive ? requiredHeader : requiredHeader.toLowerCase(); if (!normalizedHeaders[headerKey]) { throw new core_1.ValidationError(`Missing required header: ${requiredHeader}`, `Header '${requiredHeader}' is required but not provided`); } } } // Validate header patterns if (headerConfig.patterns) { for (const [headerName, pattern] of Object.entries(headerConfig.patterns)) { const headerKey = headerConfig.caseSensitive ? headerName : headerName.toLowerCase(); const headerValue = normalizedHeaders[headerKey]; if (headerValue && typeof headerValue === 'string' && !pattern.test(headerValue)) { throw new core_1.ValidationError(`Invalid header format: ${headerName}`, `Header '${headerName}' value '${headerValue}' does not match required pattern`); } } } // Custom header validators if (headerConfig.customValidators) { for (const [headerName, validator] of Object.entries(headerConfig.customValidators)) { const headerKey = headerConfig.caseSensitive ? headerName : headerName.toLowerCase(); const headerValue = normalizedHeaders[headerKey]; if (headerValue && typeof headerValue === 'string' && !validator(headerValue)) { throw new core_1.ValidationError(`Invalid header value: ${headerName}`, `Header '${headerName}' failed custom validation`); } } } } async validateQueryParameters(context) { const queryConfig = this.config.query; const query = context.req.query || {}; // Check max parameters limit if (queryConfig.maxParams && Object.keys(query).length > queryConfig.maxParams) { throw new core_1.ValidationError('Too many query parameters', `Maximum ${queryConfig.maxParams} query parameters allowed`); } // Check allowed parameters if (queryConfig.allowedParams) { for (const paramName of Object.keys(query)) { if (!queryConfig.allowedParams.includes(paramName)) { throw new core_1.ValidationError(`Invalid query parameter: ${paramName}`, `Parameter '${paramName}' is not allowed`); } } } // Check required parameters if (queryConfig.requiredParams) { for (const requiredParam of queryConfig.requiredParams) { if (!query[requiredParam]) { throw new core_1.ValidationError(`Missing required query parameter: ${requiredParam}`, `Query parameter '${requiredParam}' is required`); } } } // Validate parameter patterns if (queryConfig.patterns) { for (const [paramName, pattern] of Object.entries(queryConfig.patterns)) { const paramValue = query[paramName]; if (paramValue && typeof paramValue === 'string' && !pattern.test(paramValue)) { throw new core_1.ValidationError(`Invalid query parameter format: ${paramName}`, `Parameter '${paramName}' value '${paramValue}' does not match required pattern`); } } } // Parse and type convert parameters if (queryConfig.parseTypes) { for (const [paramName, type] of Object.entries(queryConfig.parseTypes)) { const paramValue = query[paramName]; if (paramValue) { try { query[paramName] = this.parseQueryParamType(paramValue, type); } catch (error) { throw new core_1.ValidationError(`Invalid query parameter type: ${paramName}`, `Parameter '${paramName}' could not be parsed as ${type}: ${error}`); } } } } } async validateBody(context) { const bodyConfig = this.config.body; const body = context.req.body || context.req.parsedBody; // Check if body is required if (bodyConfig.required && (!body || (bodyConfig.allowEmpty === false && this.isEmpty(body)))) { throw new core_1.ValidationError('Request body is required', 'Request body must be provided and non-empty'); } // Skip validation if body is empty and empty is allowed if (!body || (this.isEmpty(body) && bodyConfig.allowEmpty !== false)) { return; } // Check body size limit if (bodyConfig.maxSize) { const bodySize = this.getBodySize(body); if (bodySize > bodyConfig.maxSize) { throw new core_1.ValidationError('Request body too large', `Body size ${bodySize} bytes exceeds limit of ${bodyConfig.maxSize} bytes`); } } // Use custom validator if provided if (bodyConfig.customValidator) { try { const validatedBody = await bodyConfig.customValidator(body); context.req.validatedBody = validatedBody; return; } catch (error) { throw new core_1.ValidationError('Custom body validation failed', error instanceof Error ? error.message : 'Custom validation error'); } } // Use Zod schema validation if provided if (bodyConfig.schema) { try { const validatedBody = await bodyConfig.schema.parseAsync(body); context.req.validatedBody = validatedBody; } catch (error) { if (error instanceof zod_1.ZodError) { const zodError = error; const errorDetails = zodError.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message, code: issue.code, })); throw new core_1.ValidationError('Request body validation failed', `Validation errors: ${errorDetails.map((e) => `${e.path}: ${e.message}`).join(', ')}`); } else { throw new core_1.ValidationError('Body validation error', error instanceof Error ? error.message : 'Unknown validation error'); } } } } normalizeHeaders(headers) { const normalized = {}; for (const [key, value] of Object.entries(headers)) { normalized[key.toLowerCase()] = value; } return normalized; } parseQueryParamType(value, type) { if (Array.isArray(value) && type !== 'array') { value = value[0]; // Take first value for non-array types } switch (type) { case 'string': return typeof value === 'string' ? value : String(value); case 'number': if (typeof value === 'string') { const parsed = Number(value); if (isNaN(parsed)) { throw new Error(`Cannot parse '${value}' as number`); } return parsed; } throw new Error(`Expected string value for number parsing, got ${typeof value}`); case 'boolean': if (typeof value === 'string') { if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; throw new Error(`Cannot parse '${value}' as boolean (expected 'true' or 'false')`); } throw new Error(`Expected string value for boolean parsing, got ${typeof value}`); case 'array': return Array.isArray(value) ? value : [value]; default: throw new Error(`Unknown type: ${type}`); } } isEmpty(value) { if (value === null || value === undefined) return true; if (typeof value === 'string' && value.trim() === '') return true; if (Array.isArray(value) && value.length === 0) return true; if (typeof value === 'object' && Object.keys(value).length === 0) return true; return false; } getBodySize(body) { if (typeof body === 'string') { return Buffer.byteLength(body, 'utf8'); } if (Buffer.isBuffer(body)) { return body.length; } if (typeof body === 'object') { return Buffer.byteLength(JSON.stringify(body), 'utf8'); } return 0; } } exports.ConsolidatedValidationMiddleware = ConsolidatedValidationMiddleware; /** * Factory functions for creating ConsolidatedValidationMiddleware with common configurations */ exports.createConsolidatedValidationMiddleware = { /** * Body-only validation with Zod schema */ bodyOnly: (schema) => new ConsolidatedValidationMiddleware({ body: { schema, required: true } }), /** * Headers-only validation */ headersOnly: (required, optional = []) => new ConsolidatedValidationMiddleware({ headers: { required, optional } }), /** * Query-only validation */ queryOnly: (config) => new ConsolidatedValidationMiddleware({ query: config }), /** * Complete validation setup */ complete: (config) => new ConsolidatedValidationMiddleware(config), /** * API validation with common patterns */ apiValidation: (schema) => new ConsolidatedValidationMiddleware({ body: { schema, required: true }, headers: { required: ['content-type'], patterns: { 'content-type': /^application\/json/i, }, }, query: { maxParams: 50, allowedParams: ['page', 'limit', 'sort', 'filter'], }, }), }; //# sourceMappingURL=ConsolidatedValidationMiddleware.js.map