UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

315 lines 12 kB
import { createHash } from 'crypto'; export class DiffEngine { diffCache = new Map(); maxCacheSize = 100; /** * Compare two handler schemas and detect breaking changes */ diffSchemas(oldSchema, newSchema) { const cacheKey = this.hash(oldSchema) + ':' + this.hash(newSchema); if (this.diffCache.has(cacheKey)) { return this.diffCache.get(cacheKey); } const breaking = []; const nonBreaking = []; // Compare request schemas this.compareRequest(oldSchema.request, newSchema.request, breaking, nonBreaking); // Compare response schemas this.compareResponse(oldSchema.response, newSchema.response, breaking, nonBreaking); const requiresTransformer = breaking.length > 0; const summary = this.generateSummary(breaking, nonBreaking); const diff = { breaking, nonBreaking, requiresTransformer, summary, hash: cacheKey, }; this.cacheDiff(cacheKey, diff); return diff; } /** * Compare request schemas */ compareRequest(oldReq, newReq, breaking, nonBreaking) { if (!oldReq && !newReq) return; // Compare params this.compareFields(oldReq?.params || {}, newReq?.params || {}, '/request/params', breaking, nonBreaking, 'request'); // Compare query this.compareFields(oldReq?.query || {}, newReq?.query || {}, '/request/query', breaking, nonBreaking, 'request'); // Compare body this.compareFields(oldReq?.body || {}, newReq?.body || {}, '/request/body', breaking, nonBreaking, 'request'); // Compare headers this.compareFields(oldReq?.headers || {}, newReq?.headers || {}, '/request/headers', breaking, nonBreaking, 'request'); } /** * Compare response schemas */ compareResponse(oldRes, newRes, breaking, nonBreaking) { if (!oldRes && !newRes) return; // Status code change if (oldRes?.status !== newRes?.status) { if (oldRes?.status && newRes?.status) { breaking.push({ type: 'breaking', operation: 'modify', path: '/response/status', description: `Response status changed from ${oldRes.status} to ${newRes.status}`, oldValue: oldRes.status, newValue: newRes.status, }); } } // Compare body this.compareFields(oldRes?.body || {}, newRes?.body || {}, '/response/body', breaking, nonBreaking, 'response'); // Compare headers this.compareFields(oldRes?.headers || {}, newRes?.headers || {}, '/response/headers', breaking, nonBreaking, 'response'); } /** * Compare field schemas */ compareFields(oldFields, newFields, basePath, breaking, nonBreaking, context) { const oldKeys = Object.keys(oldFields); const newKeys = Object.keys(newFields); const allKeys = new Set([...oldKeys, ...newKeys]); for (const key of allKeys) { const path = `${basePath}/${key}`; const oldField = oldFields[key]; const newField = newFields[key]; if (!oldField && newField) { // Field added if (context === 'request' && newField.required) { breaking.push({ type: 'breaking', operation: 'add', path, description: `Required ${context} field '${key}' added`, newValue: newField, }); } else { nonBreaking.push({ type: 'non-breaking', operation: 'add', path, description: `Optional ${context} field '${key}' added`, newValue: newField, }); } } else if (oldField && !newField) { // Field removed if (context === 'response' && oldField.required) { breaking.push({ type: 'breaking', operation: 'remove', path, description: `Required ${context} field '${key}' removed`, oldValue: oldField, }); } else { nonBreaking.push({ type: 'non-breaking', operation: 'remove', path, description: `Optional ${context} field '${key}' removed`, oldValue: oldField, }); } } else if (oldField && newField) { // Field modified this.compareField(oldField, newField, path, key, breaking, nonBreaking, context); } } } /** * Compare individual field schemas */ compareField(oldField, newField, path, fieldName, breaking, nonBreaking, context) { // Type change if (oldField.type !== newField.type) { breaking.push({ type: 'breaking', operation: 'modify', path, description: `Field '${fieldName}' type changed from ${oldField.type} to ${newField.type}`, oldValue: oldField.type, newValue: newField.type, }); } // Required flag change if (oldField.required !== newField.required) { if (context === 'request' && !oldField.required && newField.required) { breaking.push({ type: 'breaking', operation: 'modify', path, description: `Field '${fieldName}' is now required`, oldValue: oldField.required, newValue: newField.required, }); } else if (context === 'response' && oldField.required && !newField.required) { breaking.push({ type: 'breaking', operation: 'modify', path, description: `Field '${fieldName}' is no longer required`, oldValue: oldField.required, newValue: newField.required, }); } else { nonBreaking.push({ type: 'non-breaking', operation: 'modify', path, description: `Field '${fieldName}' required flag changed`, oldValue: oldField.required, newValue: newField.required, }); } } // Nullable flag change if (oldField.nullable !== newField.nullable) { if (!oldField.nullable && newField.nullable) { nonBreaking.push({ type: 'non-breaking', operation: 'modify', path, description: `Field '${fieldName}' is now nullable`, oldValue: oldField.nullable, newValue: newField.nullable, }); } else { breaking.push({ type: 'breaking', operation: 'modify', path, description: `Field '${fieldName}' is no longer nullable`, oldValue: oldField.nullable, newValue: newField.nullable, }); } } // Nested object comparison if (oldField.type === 'object' && newField.type === 'object') { if (oldField.properties && newField.properties) { this.compareFields(oldField.properties, newField.properties, `${path}/properties`, breaking, nonBreaking, context); } } // Array items comparison if (oldField.type === 'array' && newField.type === 'array') { if (oldField.items && newField.items) { this.compareField(oldField.items, newField.items, `${path}/items`, `${fieldName}[]`, breaking, nonBreaking, context); } } } /** * Generate human-readable summary */ generateSummary(breaking, nonBreaking) { const parts = []; if (breaking.length > 0) { parts.push(`${breaking.length} breaking change${breaking.length > 1 ? 's' : ''}`); } if (nonBreaking.length > 0) { parts.push(`${nonBreaking.length} non-breaking change${nonBreaking.length > 1 ? 's' : ''}`); } if (parts.length === 0) { return 'No changes detected'; } return parts.join(', '); } /** * Calculates the structural difference between two objects. * Returns a list of operations to transform obj1 into obj2. */ diff(obj1, obj2, path = '') { const ops = []; // Handle primitives if (obj1 === obj2) return ops; if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) { ops.push({ op: 'replace', path, value: obj2, oldValue: obj1 }); return ops; } // Handle Arrays if (Array.isArray(obj1) && Array.isArray(obj2)) { const len = Math.max(obj1.length, obj2.length); for (let i = 0; i < len; i++) { const currentPath = `${path}/${i}`; if (i >= obj1.length) { ops.push({ op: 'add', path: currentPath, value: obj2[i] }); } else if (i >= obj2.length) { ops.push({ op: 'remove', path: currentPath, oldValue: obj1[i] }); } else { ops.push(...this.diff(obj1[i], obj2[i], currentPath)); } } return ops; } // Handle Objects const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); const allKeys = new Set([...keys1, ...keys2]); for (const key of allKeys) { const currentPath = path ? `${path}/${key}` : `/${key}`; const o1 = obj1; const o2 = obj2; if (!Object.prototype.hasOwnProperty.call(obj1, key)) { ops.push({ op: 'add', path: currentPath, value: o2[key] }); } else if (!Object.prototype.hasOwnProperty.call(obj2, key)) { ops.push({ op: 'remove', path: currentPath, oldValue: o1[key] }); } else { ops.push(...this.diff(o1[key], o2[key], currentPath)); } } return ops; } /** * Calculates a content hash for an object or string. * Useful for detecting if a module's code has changed. */ hash(content) { const str = typeof content === 'string' ? content : JSON.stringify(content); return createHash('sha256').update(str).digest('hex'); } /** * Cache a diff result */ cacheDiff(key, diff) { if (this.diffCache.size >= this.maxCacheSize) { const firstKey = this.diffCache.keys().next().value; if (firstKey) { this.diffCache.delete(firstKey); } } this.diffCache.set(key, diff); } /** * Clear diff cache */ clearCache() { this.diffCache.clear(); } /** * Get cache statistics */ getCacheStats() { return { size: this.diffCache.size, maxSize: this.maxCacheSize, }; } } //# sourceMappingURL=diff-engine.js.map