Files
SkyArtShop/backend/middleware/apiOptimization.js.corrupt
Local Server c1da8eff42 webupdatev1
2026-01-04 17:52:37 -06:00

340 lines
7.9 KiB
Plaintext

/**
* API Response Optimization Middleware
* Implements response batching, field filtering, and pagination
*/
const logger = require("../config/logger");
/**
* Enable response compression for API endpoints
*/
const enableCompression = (req, res, next) => {
// Already handled by global compression middleware
next();
};
/**
* Add cache headers for GET requests
*/
const addCacheHeaders = (maxAge = 300) => {
return (req, res, next) => {
if (req.method === "GET" && !res.headersSent) {
try {
res.set({
"Cache-Control": `public, max-age=${maxAge}`,
Vary: "Accept-Encoding",
});
} catch (error) {
logger.warn("Failed to set cache headers", { error: error.message });
}
}
next();
};
};
/**
* Field filtering middleware
* Allows clients to request only specific fields: ?fields=id,name,price
* SAFEGUARD: Validates field names to prevent injection attacks
*/
const fieldFilter = (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = function (data) {
const fields = req.query.fields;
if (!fields || !data || res.headersSent) {
return originalJson(data);
}
try {
// SAFEGUARD: Validate field names (alphanumeric, underscore, dot only)
if (!/^[a-zA-Z0-9_.,\s]+$/.test(fields)) {
logger.warn("Invalid field filter attempted", { fields });
return originalJson(data);
}
const fieldList = fields.split(",").map((f) => f.trim()).filter(Boolean);
// SAFEGUARD: Limit number of fields
if (fieldList.length > 50) {
logger.warn("Too many fields requested", { count: fieldList.length });
return originalJson(data);
}
const filterObject = (obj) => {
if (!obj || typeof obj !== "object") return obj;
const filtered = {};
fieldList.forEach((field) => {
if (field in obj) {
filtered[field] = obj[field];
}
});
return filtered;
};
if (Array.isArray(data)) {
data = data.map(filterObject);
} else if (data.success !== undefined && data.data) {
// Handle wrapped responses
if (Array.isArray(data.data)) {
data.data = data.data.map(filterObject);
} else {
data.data = filterObject(data.data);
}
} else {
data = filterObject(data);
}
return originalJson(data);
} catch (error) {
logger.error("Field filter error", { error: error.message });
return originalJson(data);
}
};
next();
};
/**
* Pagination middleware
* Adds pagination support: ?page=1&limit=20
*/
const paginate = (defaultLimit = 20, maxLimit = 100) => {
return (req, res, next) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(
maxLimit,
Math.max(1, parseInt(req.query.limit) || defaultLimit)
);
const offset = (page - 1) * limit;
req.pagination = {
page,
limit,
offset,
maxLimit,
};
// Helper to add pagination info to response
res.paginate = (data, total) => {
const totalPages = Math.ceil(total / limit);
return res.json({
success: true,
data,
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
});
};
next();
};
};
/**
* Response time tracking
*/
const trackResponseTime = (req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
// Log slow requests
if (duration > 1000) {
logger.warn("Slow API request", {
method: req.method,
path: req.path,
duration: `${duration}ms`,
status: res.statusCode,
});
}
// Add response time header only if headers haven't been sent
if (!res.headersSent) {
res.set("X-Response-Time", `${duration}ms`);
}
});
next();
};
/**
* ETag generation for GET requests
* SAFEGUARD: Checks headersSent before setting headers
*/
const generateETag = (req, res, next) => {
if (req.method !== "GET") {
return next();
}
const originalJson = res.json.bind(res);
res.json = function (data) {
try {
// SAFEGUARD: Don't process if headers already sent
if (res.headersSent) {
return originalJson(data);
}
// Generate simple ETag from stringified data
const dataStr = JSON.stringify(data);
const etag = `W/"${Buffer.from(dataStr).length.toString(16)}"`;
// Check if client has cached version
if (req.headers["if-none-match"] === etag) {
res.status(304).end();
return;
}
res.set("ETag", etag);
return originalJson(data);
} catch (error) {
logger.error("ETag generation error", { error: error.message });
return originalJson(data);
}
};
next();
};
/**
* JSON response size optimization
* Removes null values and compacts responses
*/
const optimizeJSON = (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = function (data) {
if (data && typeof data === "object") {
data = removeNulls(data);
}
return originalJson(data);
};
next();
};
function removeNulls(obj) {
if (Array.isArray(obj)) {
return obj.map(removeNulls);
}
if (obj !== null && typeof obj === "object") {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== null && value !== undefined) {
acc[key] = removeNulls(value);
}
return acc;
}, {});
}
return obj;
}
/**
* Batch request handler
* Allows multiple API calls in a single request
* POST /api/batch with body: { requests: [{ method, url, body }] }
* SAFEGUARD: Enhanced validation and error handling
*/
const batchHandler = async (req, res) => {
try {
const { requests } = req.body;
// SAFEGUARD: Validate requests array
if (!Array.isArray(requests) || requests.length === 0) {
return res.status(400).json({
success: false,
error: "Invalid batch request format",
});
}
// SAFEGUARD: Limit batch size
if (requests.length > 10) {
return res.status(400).json({
success: false,
error: "Maximum 10 requests per batch",
});
}
// SAFEGUARD: Validate each request structure
const isValid = requests.every(req =>
req && typeof req === 'object' &&
req.method && req.url &&
['GET', 'POST', 'PUT', 'DELETE'].includes(req.method.toUpperCase())
);
if (!isValid) {
return res.status(400).json({
success: false,
error: "Invalid request format in batch",
});
}
const results = await Promise.allSettled(
requests.map(async (request) => {
try {
// This would require implementation of internal request handling
// For now, return a placeholder
return {
status: 200,
data: { message: "Batch processing not fully implemented" },
};
} catch (error) {
return {
status: 500,
error: error.message,
};
}
})
);
// SAFEGUARD: Check if response already sent
if (res.headersSent) {
logger.warn("Response already sent in batch handler");
return;
}
res.json({
success: true,
results: results.map((result, index) => ({
...requests[index],
...result,
})),
});
} catch (error) {
logger.error("Batch handler error", { error: error.message, stack: error.stack });
if (!res.headersSent) {
res.status(500).json({
success: false,
error: "Batch processing failed",
});
}
res.json({
success: true,
results: results.map((result, index) => ({
...requests[index],
...result,
})),
});
};
module.exports = {
enableCompression,
addCacheHeaders,
fieldFilter,
paginate,
trackResponseTime,
generateETag,
optimizeJSON,
batchHandler,
};