Skip to main content
Glama
mutation-validator.tsβ€’6.38 kB
/** * Data quality validator for workflow mutations * Ensures mutation data meets quality standards before tracking */ import { createHash } from 'crypto'; import { WorkflowMutationData, MutationDataQualityResult, MutationTrackingOptions, } from './mutation-types.js'; /** * Default options for mutation tracking */ export const DEFAULT_MUTATION_TRACKING_OPTIONS: Required<MutationTrackingOptions> = { enabled: true, maxWorkflowSizeKb: 500, validateQuality: true, sanitize: true, }; /** * Validates workflow mutation data quality */ export class MutationValidator { private options: Required<MutationTrackingOptions>; constructor(options: MutationTrackingOptions = {}) { this.options = { ...DEFAULT_MUTATION_TRACKING_OPTIONS, ...options }; } /** * Validate mutation data quality */ validate(data: WorkflowMutationData): MutationDataQualityResult { const errors: string[] = []; const warnings: string[] = []; // Check workflow structure if (!this.isValidWorkflow(data.workflowBefore)) { errors.push('Invalid workflow_before structure'); } if (!this.isValidWorkflow(data.workflowAfter)) { errors.push('Invalid workflow_after structure'); } // Check workflow size const beforeSizeKb = this.getWorkflowSizeKb(data.workflowBefore); const afterSizeKb = this.getWorkflowSizeKb(data.workflowAfter); if (beforeSizeKb > this.options.maxWorkflowSizeKb) { errors.push( `workflow_before size (${beforeSizeKb}KB) exceeds maximum (${this.options.maxWorkflowSizeKb}KB)` ); } if (afterSizeKb > this.options.maxWorkflowSizeKb) { errors.push( `workflow_after size (${afterSizeKb}KB) exceeds maximum (${this.options.maxWorkflowSizeKb}KB)` ); } // Check for meaningful change if (!this.hasMeaningfulChange(data.workflowBefore, data.workflowAfter)) { warnings.push('No meaningful change detected between before and after workflows'); } // Check intent quality if (!data.userIntent || data.userIntent.trim().length === 0) { warnings.push('User intent is empty'); } else if (data.userIntent.trim().length < 5) { warnings.push('User intent is too short (less than 5 characters)'); } else if (data.userIntent.length > 1000) { warnings.push('User intent is very long (over 1000 characters)'); } // Check operations if (!data.operations || data.operations.length === 0) { errors.push('No operations provided'); } // Check validation data consistency if (data.validationBefore && data.validationAfter) { if (typeof data.validationBefore.valid !== 'boolean') { warnings.push('Invalid validation_before structure'); } if (typeof data.validationAfter.valid !== 'boolean') { warnings.push('Invalid validation_after structure'); } } // Check duration sanity if (data.durationMs !== undefined) { if (data.durationMs < 0) { errors.push('Duration cannot be negative'); } if (data.durationMs > 300000) { // 5 minutes warnings.push('Duration is very long (over 5 minutes)'); } } return { valid: errors.length === 0, errors, warnings, }; } /** * Check if workflow has valid structure */ private isValidWorkflow(workflow: any): boolean { if (!workflow || typeof workflow !== 'object') { return false; } // Must have nodes array if (!Array.isArray(workflow.nodes)) { return false; } // Must have connections object if (!workflow.connections || typeof workflow.connections !== 'object') { return false; } return true; } /** * Get workflow size in KB */ private getWorkflowSizeKb(workflow: any): number { try { const json = JSON.stringify(workflow); return json.length / 1024; } catch { return 0; } } /** * Check if there's meaningful change between workflows */ private hasMeaningfulChange(workflowBefore: any, workflowAfter: any): boolean { try { // Compare hashes const hashBefore = this.hashWorkflow(workflowBefore); const hashAfter = this.hashWorkflow(workflowAfter); return hashBefore !== hashAfter; } catch { return false; } } /** * Hash workflow for comparison */ hashWorkflow(workflow: any): string { try { const json = JSON.stringify(workflow); return createHash('sha256').update(json).digest('hex').substring(0, 16); } catch { return ''; } } /** * Check if mutation should be excluded from tracking */ shouldExclude(data: WorkflowMutationData): boolean { // Exclude if not successful and no error message if (!data.mutationSuccess && !data.mutationError) { return true; } // Exclude if workflows are identical if (!this.hasMeaningfulChange(data.workflowBefore, data.workflowAfter)) { return true; } // Exclude if workflow size exceeds limits const beforeSizeKb = this.getWorkflowSizeKb(data.workflowBefore); const afterSizeKb = this.getWorkflowSizeKb(data.workflowAfter); if ( beforeSizeKb > this.options.maxWorkflowSizeKb || afterSizeKb > this.options.maxWorkflowSizeKb ) { return true; } return false; } /** * Check for duplicate mutation (same hash + operations) */ isDuplicate( workflowBefore: any, workflowAfter: any, operations: any[], recentMutations: Array<{ hashBefore: string; hashAfter: string; operations: any[] }> ): boolean { const hashBefore = this.hashWorkflow(workflowBefore); const hashAfter = this.hashWorkflow(workflowAfter); const operationsHash = this.hashOperations(operations); return recentMutations.some( (m) => m.hashBefore === hashBefore && m.hashAfter === hashAfter && this.hashOperations(m.operations) === operationsHash ); } /** * Hash operations for deduplication */ private hashOperations(operations: any[]): string { try { const json = JSON.stringify(operations); return createHash('sha256').update(json).digest('hex').substring(0, 16); } catch { return ''; } } } /** * Singleton instance for easy access */ export const mutationValidator = new MutationValidator();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server