@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
448 lines (381 loc) • 11.8 kB
JavaScript
/**
* API Response optimization utilities
* Provides compression, pagination, and response formatting optimizations
*/
/**
* Response compression and optimization
*/
export const ResponseOptimizer = {
/**
* Optimize GraphQL response by removing null/undefined fields
*/
cleanResponse: (data) => {
if (data === null || data === undefined) {
return data;
}
if (Array.isArray(data)) {
return data.map(item => ResponseOptimizer.cleanResponse(item));
}
if (typeof data === 'object') {
const cleaned = {};
for (const [key, value] of Object.entries(data)) {
if (value !== null && value !== undefined) {
cleaned[key] = ResponseOptimizer.cleanResponse(value);
}
}
return cleaned;
}
return data;
},
/**
* Compress large arrays by limiting nested object fields
*/
compressArray: (array, maxFields = 10) => {
if (!Array.isArray(array) || array.length === 0) {
return array;
}
return array.map(item => {
if (typeof item === 'object' && item !== null) {
const keys = Object.keys(item);
if (keys.length > maxFields) {
// Keep most important fields
const importantFields = ['id', '_id', 'name', 'title', 'email', 'status', 'createdAt'];
const compressed = {};
// Add important fields first
importantFields.forEach(field => {
if (item[field] !== undefined) {
compressed[field] = item[field];
}
});
// Add remaining fields up to limit
let count = Object.keys(compressed).length;
for (const key of keys) {
if (count >= maxFields) break;
if (!compressed.hasOwnProperty(key)) {
compressed[key] = item[key];
count++;
}
}
return compressed;
}
}
return item;
});
},
/**
* Create paginated response with metadata
*/
paginate: (data, page, limit, total) => {
const totalPages = Math.ceil(total / limit);
const hasNextPage = page < totalPages;
const hasPreviousPage = page > 1;
return {
data,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: parseInt(total),
totalPages,
hasNextPage,
hasPreviousPage,
nextPage: hasNextPage ? page + 1 : null,
previousPage: hasPreviousPage ? page - 1 : null
}
};
},
/**
* Optimize nested lookups by batching
*/
batchNestedData: async (items, lookupField, lookupFunction, batchSize = 50) => {
if (!items || items.length === 0) {
return items;
}
// Extract unique IDs for lookup
const ids = [...new Set(items.map(item => item[lookupField]).filter(Boolean))];
// Batch lookup in chunks
const lookupResults = new Map();
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const results = await lookupFunction(batch);
results.forEach(result => {
lookupResults.set(result._id.toString(), result);
});
}
// Attach lookup results to items
return items.map(item => ({
...item,
[lookupField.replace('Id', '')]: lookupResults.get(item[lookupField]?.toString())
}));
}
};
/**
* Field selection optimization for GraphQL
*/
export const FieldSelector = {
/**
* Parse GraphQL field selection info
*/
parseSelections: (info) => {
const selections = info.fieldNodes[0].selectionSet?.selections || [];
const fields = {};
selections.forEach(selection => {
if (selection.kind === 'Field') {
fields[selection.name.value] = selection.selectionSet
? FieldSelector.parseNestedSelections(selection.selectionSet)
: true;
}
});
return fields;
},
/**
* Parse nested field selections
*/
parseNestedSelections: (selectionSet) => {
const fields = {};
selectionSet.selections.forEach(selection => {
if (selection.kind === 'Field') {
fields[selection.name.value] = selection.selectionSet
? FieldSelector.parseNestedSelections(selection.selectionSet)
: true;
}
});
return fields;
},
/**
* Create MongoDB projection from GraphQL selections
*/
createProjection: (selections, excludeFields = []) => {
const projection = {};
Object.keys(selections).forEach(field => {
if (!excludeFields.includes(field)) {
projection[field] = 1;
}
});
// Always include _id unless explicitly excluded
if (!excludeFields.includes('_id') && !excludeFields.includes('id')) {
projection._id = 1;
}
return projection;
},
/**
* Optimize lookup projections based on selected fields
*/
optimizeLookups: (selections) => {
const lookups = {};
Object.entries(selections).forEach(([field, subFields]) => {
if (typeof subFields === 'object' && subFields !== null) {
// This is a nested field (likely a lookup)
lookups[field] = Object.keys(subFields);
}
});
return lookups;
}
};
/**
* Response caching with ETags and conditional requests
*/
export const ResponseCaching = {
/**
* Generate ETag for response data
*/
generateETag: (data) => {
const crypto = require('crypto');
const hash = crypto.createHash('md5');
hash.update(JSON.stringify(data));
return `"${hash.digest('hex')}"`;
},
/**
* Check if response has been modified
*/
isModified: (req, etag, lastModified) => {
const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];
// Check ETag
if (ifNoneMatch && ifNoneMatch === etag) {
return false;
}
// Check last modified date
if (ifModifiedSince && lastModified) {
const clientDate = new Date(ifModifiedSince);
const serverDate = new Date(lastModified);
return serverDate > clientDate;
}
return true;
},
/**
* Set cache headers
*/
setCacheHeaders: (res, etag, lastModified, maxAge = 300) => {
if (etag) {
res.set('ETag', etag);
}
if (lastModified) {
res.set('Last-Modified', new Date(lastModified).toUTCString());
}
res.set('Cache-Control', `public, max-age=${maxAge}`);
}
};
/**
* Data transformation utilities
*/
export const DataTransformer = {
/**
* Transform MongoDB documents for API response
*/
transformDocument: (doc, transformations = {}) => {
if (!doc) return doc;
const transformed = { ...doc };
// Convert _id to id but keep _id for GraphQL schema compatibility
if (transformed._id) {
transformed.id = transformed._id.toString();
// Keep _id as string for GraphQL schema compatibility
transformed._id = transformed._id.toString();
}
// Apply custom transformations
Object.entries(transformations).forEach(([field, transformer]) => {
if (transformed[field] !== undefined) {
transformed[field] = transformer(transformed[field]);
}
});
// Format dates
['createdAt', 'updatedAt', 'lastLogin', 'scheduledDate'].forEach(field => {
if (transformed[field] instanceof Date) {
transformed[field] = transformed[field].toISOString();
}
});
return transformed;
},
/**
* Transform array of documents
*/
transformArray: (docs, transformations = {}) => {
if (!Array.isArray(docs)) return [];
return docs.map(doc => DataTransformer.transformDocument(doc, transformations));
},
/**
* Sanitize sensitive data
*/
sanitize: (data, sensitiveFields = ['password', 'token', 'secret']) => {
if (!data || typeof data !== 'object') return data;
const sanitized = { ...data };
sensitiveFields.forEach(field => {
if (sanitized[field]) {
delete sanitized[field];
}
});
return sanitized;
}
};
/**
* Error response optimization
*/
export const ErrorOptimizer = {
/**
* Format GraphQL errors for production
*/
formatError: (error, isDevelopment = false) => {
const formatted = {
message: error.message,
code: error.extensions?.code || 'INTERNAL_ERROR',
timestamp: new Date().toISOString()
};
if (isDevelopment) {
formatted.stack = error.stack;
formatted.path = error.path;
formatted.locations = error.locations;
}
// Add specific error details based on type
if (error.extensions?.code === 'VALIDATION_ERROR') {
formatted.details = error.extensions.details;
}
return formatted;
},
/**
* Create standardized API error response
*/
createErrorResponse: (message, code = 'INTERNAL_ERROR', status = 500, details = null) => {
return {
error: {
message,
code,
status,
timestamp: new Date().toISOString(),
...(details && { details })
}
};
}
};
/**
* Performance monitoring for responses
*/
export const ResponseMonitor = {
/**
* Track response times and sizes
*/
trackResponse: (operationName, responseTime, responseSize) => {
// In a real implementation, this would send to monitoring service
console.log(`GraphQL Operation: ${operationName}`, {
responseTime: `${responseTime}ms`,
responseSize: `${(responseSize / 1024).toFixed(2)}KB`,
timestamp: new Date().toISOString()
});
// Alert on slow responses
if (responseTime > 1000) {
console.warn(`Slow GraphQL operation detected: ${operationName} took ${responseTime}ms`);
}
// Alert on large responses
if (responseSize > 1024 * 1024) { // 1MB
console.warn(`Large GraphQL response detected: ${operationName} returned ${(responseSize / 1024 / 1024).toFixed(2)}MB`);
}
},
/**
* Middleware to track all GraphQL operations
*/
createTrackingMiddleware: () => {
return (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
res.send = function (data) {
const endTime = Date.now();
const responseTime = endTime - startTime;
const responseSize = Buffer.byteLength(data);
const operationName = req.body?.operationName || 'Unknown';
ResponseMonitor.trackResponse(operationName, responseTime, responseSize);
return originalSend.call(this, data);
};
next();
};
}
};
/**
* Batch processing utilities
*/
export const BatchProcessor = {
/**
* Process items in batches to avoid memory issues
*/
processBatches: async (items, batchSize, processor) => {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await processor(batch);
results.push(...batchResults);
}
return results;
},
/**
* Create batched resolver
*/
createBatchedResolver: (resolver, batchSize = 100) => {
return async (parent, args, context, info) => {
if (args.ids && args.ids.length > batchSize) {
return BatchProcessor.processBatches(
args.ids,
batchSize,
(batch) => resolver(parent, { ...args, ids: batch }, context, info)
);
}
return resolver(parent, args, context, info);
};
}
};