@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
330 lines • 13.5 kB
JavaScript
;
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