@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
430 lines • 15.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createProcessingMiddleware = exports.ProcessingMiddleware = void 0;
const core_1 = require("../core");
/**
* Consolidated ProcessingMiddleware that combines body parsing, query processing, and attribute extraction.
*
* This middleware replaces the need for separate:
* - BodyParserMiddleware
* - QueryParametersMiddleware
* - HttpAttributesMiddleware
*
* @template TBody - The type of the request body payload (preserves type chain)
* @template TUser - The type of the authenticated user (preserves type chain)
*
* @example
* Complete processing setup:
* ```typescript
* const handler = new Handler()
* .use(new ProcessingMiddleware({
* parser: {
* maxSize: 1024 * 1024, // 1MB
* supportPubSub: true,
* enableAsyncParsing: true
* },
* query: {
* parseArrays: true,
* parseNumbers: true,
* parseBooleans: true,
* maxKeys: 100
* },
* attributes: {
* extractIP: true,
* extractUserAgent: true,
* extractTimestamp: true,
* trustProxy: true
* }
* }))
* .handle(async (context) => {
* // context.req.parsedBody contains parsed JSON
* // context.req.query contains processed query parameters
* // context.req.ip, context.req.userAgent, etc. are extracted
* return { message: 'Processing complete' };
* });
* ```
*
* @example
* Parser-only for API endpoints:
* ```typescript
* const handler = new Handler()
* .use(new ProcessingMiddleware({
* parser: {
* maxSize: 512 * 1024, // 512KB
* supportPubSub: false
* }
* }));
* ```
*/
class ProcessingMiddleware {
config;
constructor(config = {}) {
this.config = {
parser: {
maxSize: 1024 * 1024, // 1MB default
supportPubSub: true,
allowEmptyBody: true,
enableAsyncParsing: true,
asyncThreshold: 10000, // 10KB
},
query: {
parseArrays: false,
parseNumbers: false,
parseBooleans: false,
maxKeys: 1000,
delimiter: '&',
arrayDelimiter: ',',
},
attributes: {
extractIP: true,
extractUserAgent: true,
extractTimestamp: false,
trustProxy: false,
},
...config,
};
}
async before(context) {
// Skip processing if custom skip function returns true
if (this.config.skipProcessing && this.config.skipProcessing(context)) {
return;
}
// 1. Extract HTTP attributes first (lightweight)
if (this.config.attributes) {
await this.extractAttributes(context);
}
// 2. Process query parameters (moderate cost)
if (this.config.query) {
await this.processQueryParameters(context);
}
// 3. Parse body (most expensive, do last)
if (this.config.parser) {
await this.parseBody(context);
}
}
async extractAttributes(context) {
const attributesConfig = this.config.attributes;
const req = context.req;
// Extract IP address
if (attributesConfig.extractIP) {
req.ip = this.extractIPAddress(req, attributesConfig.trustProxy || false);
}
// Extract User-Agent
if (attributesConfig.extractUserAgent) {
req.userAgent = this.extractUserAgent(req);
}
// Extract timestamp
if (attributesConfig.extractTimestamp) {
req.timestamp =
new Date().toISOString();
}
// Extract content length
if (attributesConfig.extractContentLength) {
req.contentLength =
this.extractContentLength(req);
}
// Extract Accept-Language
if (attributesConfig.extractAcceptLanguage) {
req.acceptLanguage =
this.extractAcceptLanguage(req);
}
// Extract Referer
if (attributesConfig.extractReferer) {
req.referer =
this.extractReferer(req);
}
// Apply custom extractors
if (attributesConfig.customExtractors) {
for (const [key, extractor] of Object.entries(attributesConfig.customExtractors)) {
try {
req[key] = extractor(req);
}
catch (error) {
console.warn(`Custom extractor '${key}' failed:`, error);
}
}
}
}
async processQueryParameters(context) {
const queryConfig = this.config.query;
const query = context.req.query || {};
// Check max keys limit
if (queryConfig.maxKeys &&
Object.keys(query).length > queryConfig.maxKeys) {
throw new core_1.ValidationError('Too many query parameters', `Maximum ${queryConfig.maxKeys} query parameters allowed`);
}
// Apply custom parser first if provided
if (queryConfig.customParser) {
try {
context.req.query = queryConfig.customParser(query);
return;
}
catch (error) {
throw new core_1.ValidationError('Query parameter parsing failed', error instanceof Error ? error.message : 'Custom parser error');
}
}
// Process each query parameter
const processedQuery = {};
for (const [key, value] of Object.entries(query)) {
processedQuery[key] = this.processQueryValue(value, queryConfig.parseArrays || false, queryConfig.parseNumbers || false, queryConfig.parseBooleans || false, queryConfig.arrayDelimiter || ',');
}
context.req.query = processedQuery;
}
async parseBody(context) {
const parserConfig = this.config.parser;
const body = context.req.body;
// Skip if no body or body is already parsed
if (!body || context.req.parsedBody) {
if (parserConfig.allowEmptyBody) {
return;
}
throw new core_1.ValidationError('Request body is required');
}
// Apply custom parser if provided
if (parserConfig.customParser) {
try {
context.req.parsedBody = await parserConfig.customParser(body);
return;
}
catch (error) {
throw new core_1.ValidationError('Custom body parsing failed', error instanceof Error ? error.message : 'Custom parser error');
}
}
// Handle different body types
if (typeof body === 'string') {
await this.parseStringBody(context, body, parserConfig);
}
else if (typeof body === 'object' && body !== null) {
// Handle PubSub messages if enabled
if (parserConfig.supportPubSub && this.isPubSubMessage(body)) {
await this.parsePubSubMessage(context, body, parserConfig);
}
else {
// Already an object, store as is
context.req.parsedBody = body;
}
}
else {
context.req.parsedBody = body;
}
}
async parseStringBody(context, body, config) {
// Check size limit
if (config.maxSize && Buffer.byteLength(body, 'utf8') > config.maxSize) {
throw new core_1.TooLargeError(`Request body size exceeds limit of ${config.maxSize} bytes`);
}
try {
// Use async parsing for large payloads if enabled
if (config.enableAsyncParsing &&
body.length > (config.asyncThreshold || 10000)) {
context.req.parsedBody = await this.parseJsonAsync(body);
}
else {
context.req.parsedBody = JSON.parse(body);
}
}
catch (error) {
throw new core_1.ValidationError('Invalid JSON body', error instanceof Error ? error.message : 'JSON parsing failed');
}
}
async parsePubSubMessage(context, body, config) {
try {
const encodedData = body.message.data;
// Validate base64 format
this.validateBase64Format(encodedData);
// Check size limit before decoding
if (config.maxSize && encodedData.length > config.maxSize * 1.33) {
// Account for base64 overhead
throw new core_1.TooLargeError(`PubSub message size exceeds limit of ${config.maxSize} bytes`);
}
// Decode base64 data
const decodedData = Buffer.from(encodedData, 'base64').toString('utf-8');
// Parse JSON content
context.req.parsedBody =
config.enableAsyncParsing &&
decodedData.length > (config.asyncThreshold || 10000)
? await this.parseJsonAsync(decodedData)
: JSON.parse(decodedData);
// Store PubSub metadata
context.req.pubsubMetadata = {
publishTime: body.message.publishTime,
messageId: body.message.messageId,
attributes: body.message.attributes,
};
}
catch (error) {
throw new core_1.ValidationError('PubSub message parsing failed', error instanceof Error ? error.message : 'PubSub parsing error');
}
}
processQueryValue(value, parseArrays, parseNumbers, parseBooleans, arrayDelimiter) {
if (typeof value !== 'string') {
return value; // Return as-is if not string
}
// Handle arrays
if (parseArrays && value.includes(arrayDelimiter)) {
return value
.split(arrayDelimiter)
.map((item) => this.processQueryValue(item.trim(), false, parseNumbers, parseBooleans, arrayDelimiter));
}
// Handle booleans
if (parseBooleans) {
const lowerValue = value.toLowerCase();
if (lowerValue === 'true')
return true;
if (lowerValue === 'false')
return false;
}
// Handle numbers
if (parseNumbers && /^-?\d+(\.\d+)?$/.test(value)) {
const numValue = Number(value);
if (!isNaN(numValue))
return numValue;
}
return value; // Return as string if no parsing applied
}
extractIPAddress(req, trustProxy) {
if (trustProxy) {
// Check X-Forwarded-For headers
const xForwardedFor = req.headers?.['x-forwarded-for'] || req.headers?.['X-Forwarded-For'];
if (xForwardedFor) {
const ips = Array.isArray(xForwardedFor)
? xForwardedFor[0]
: xForwardedFor;
return typeof ips === 'string' ? ips.split(',')[0].trim() : ips;
}
const xRealIP = req.headers?.['x-real-ip'] || req.headers?.['X-Real-IP'];
if (xRealIP && typeof xRealIP === 'string') {
return xRealIP;
}
}
// Fallback to direct IP sources
return (req.ip ||
req
.connection?.remoteAddress ||
req.socket
?.remoteAddress ||
req.requestContext?.identity?.sourceIp ||
'unknown');
}
extractUserAgent(req) {
const userAgent = req.headers?.['user-agent'] ||
req.headers?.['User-Agent'] ||
req.get?.('user-agent') ||
'unknown';
return Array.isArray(userAgent) ? userAgent[0] : userAgent;
}
extractContentLength(req) {
const contentLength = req.headers?.['content-length'] || req.headers?.['Content-Length'];
const lengthValue = Array.isArray(contentLength)
? contentLength[0]
: contentLength;
return lengthValue ? parseInt(lengthValue, 10) : undefined;
}
extractAcceptLanguage(req) {
const acceptLanguage = req.headers?.['accept-language'] || req.headers?.['Accept-Language'];
return Array.isArray(acceptLanguage) ? acceptLanguage[0] : acceptLanguage;
}
extractReferer(req) {
const referer = req.headers?.referer ||
req.headers?.Referer ||
req.headers?.referrer ||
req.headers?.Referrer;
return Array.isArray(referer) ? referer[0] : referer;
}
isPubSubMessage(body) {
return (!!body &&
typeof body === 'object' &&
'message' in body &&
typeof body.message === 'object' &&
'data' in body.message &&
typeof body.message.data === 'string');
}
validateBase64Format(base64Data) {
// Basic base64 format validation
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (!base64Regex.test(base64Data)) {
throw new core_1.ValidationError('Invalid base64 format in PubSub message');
}
if (base64Data.length % 4 !== 0) {
throw new core_1.ValidationError('Invalid base64 length - must be multiple of 4');
}
}
async parseJsonAsync(jsonString) {
// Use setImmediate to make JSON parsing non-blocking for large payloads
return new Promise((resolve, reject) => {
setImmediate(() => {
try {
const result = JSON.parse(jsonString);
resolve(result);
}
catch (error) {
reject(new core_1.ValidationError('Invalid JSON body', error instanceof Error ? error.message : 'JSON parsing failed'));
}
});
});
}
}
exports.ProcessingMiddleware = ProcessingMiddleware;
/**
* Factory functions for creating ProcessingMiddleware with common configurations
*/
exports.createProcessingMiddleware = {
/**
* API processing with JSON parsing and basic attributes
*/
api: () => new ProcessingMiddleware({
parser: { maxSize: 1024 * 1024, supportPubSub: false },
query: { parseNumbers: true, parseBooleans: true, maxKeys: 100 },
attributes: { extractIP: true, extractUserAgent: true },
}),
/**
* PubSub processing with base64 decoding
*/
pubsub: () => new ProcessingMiddleware({
parser: {
maxSize: 2 * 1024 * 1024,
supportPubSub: true,
enableAsyncParsing: true,
},
attributes: { extractIP: true, extractTimestamp: true },
}),
/**
* Lightweight processing for simple endpoints
*/
lightweight: () => new ProcessingMiddleware({
parser: {
maxSize: 64 * 1024,
supportPubSub: false,
enableAsyncParsing: false,
},
query: { parseNumbers: false, parseBooleans: false, maxKeys: 20 },
attributes: { extractIP: false, extractUserAgent: false },
}),
/**
* Full processing with all features enabled
*/
complete: () => new ProcessingMiddleware({
parser: {
maxSize: 5 * 1024 * 1024,
supportPubSub: true,
enableAsyncParsing: true,
asyncThreshold: 50000,
},
query: {
parseArrays: true,
parseNumbers: true,
parseBooleans: true,
maxKeys: 1000,
},
attributes: {
extractIP: true,
extractUserAgent: true,
extractTimestamp: true,
extractContentLength: true,
extractAcceptLanguage: true,
extractReferer: true,
trustProxy: true,
},
}),
};
//# sourceMappingURL=ProcessingMiddleware.js.map