Skip to main content
Glama

n8n-MCP

by 88-888
fixed-collection-validator.tsβ€’14.8 kB
/** * Generic utility for validating and fixing fixedCollection structures in n8n nodes * Prevents the "propertyValues[itemName] is not iterable" error */ // Type definitions for node configurations export type NodeConfigValue = string | number | boolean | null | undefined | NodeConfig | NodeConfigValue[]; export interface NodeConfig { [key: string]: NodeConfigValue; } export interface FixedCollectionPattern { nodeType: string; property: string; subProperty?: string; expectedStructure: string; invalidPatterns: string[]; } export interface FixedCollectionValidationResult { isValid: boolean; errors: Array<{ pattern: string; message: string; fix: string; }>; autofix?: NodeConfig | NodeConfigValue[]; } export class FixedCollectionValidator { /** * Type guard to check if value is a NodeConfig */ private static isNodeConfig(value: NodeConfigValue): value is NodeConfig { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Safely get nested property value */ private static getNestedValue(obj: NodeConfig, path: string): NodeConfigValue | undefined { const parts = path.split('.'); let current: NodeConfigValue = obj; for (const part of parts) { if (!this.isNodeConfig(current)) { return undefined; } current = current[part]; } return current; } /** * Known problematic patterns for various n8n nodes */ private static readonly KNOWN_PATTERNS: FixedCollectionPattern[] = [ // Conditional nodes (already fixed) { nodeType: 'switch', property: 'rules', expectedStructure: 'rules.values array', invalidPatterns: ['rules.conditions', 'rules.conditions.values'] }, { nodeType: 'if', property: 'conditions', expectedStructure: 'conditions array/object', invalidPatterns: ['conditions.values'] }, { nodeType: 'filter', property: 'conditions', expectedStructure: 'conditions array/object', invalidPatterns: ['conditions.values'] }, // New nodes identified by research { nodeType: 'summarize', property: 'fieldsToSummarize', subProperty: 'values', expectedStructure: 'fieldsToSummarize.values array', invalidPatterns: ['fieldsToSummarize.values.values'] }, { nodeType: 'comparedatasets', property: 'mergeByFields', subProperty: 'values', expectedStructure: 'mergeByFields.values array', invalidPatterns: ['mergeByFields.values.values'] }, { nodeType: 'sort', property: 'sortFieldsUi', subProperty: 'sortField', expectedStructure: 'sortFieldsUi.sortField array', invalidPatterns: ['sortFieldsUi.sortField.values'] }, { nodeType: 'aggregate', property: 'fieldsToAggregate', subProperty: 'fieldToAggregate', expectedStructure: 'fieldsToAggregate.fieldToAggregate array', invalidPatterns: ['fieldsToAggregate.fieldToAggregate.values'] }, { nodeType: 'set', property: 'fields', subProperty: 'values', expectedStructure: 'fields.values array', invalidPatterns: ['fields.values.values'] }, { nodeType: 'html', property: 'extractionValues', subProperty: 'values', expectedStructure: 'extractionValues.values array', invalidPatterns: ['extractionValues.values.values'] }, { nodeType: 'httprequest', property: 'body', subProperty: 'parameters', expectedStructure: 'body.parameters array', invalidPatterns: ['body.parameters.values'] }, { nodeType: 'airtable', property: 'sort', subProperty: 'sortField', expectedStructure: 'sort.sortField array', invalidPatterns: ['sort.sortField.values'] } ]; /** * Validate a node configuration for fixedCollection issues * Includes protection against circular references */ static validate( nodeType: string, config: NodeConfig ): FixedCollectionValidationResult { // Early return for non-object configs if (typeof config !== 'object' || config === null || Array.isArray(config)) { return { isValid: true, errors: [] }; } const normalizedNodeType = this.normalizeNodeType(nodeType); const pattern = this.getPatternForNode(normalizedNodeType); if (!pattern) { return { isValid: true, errors: [] }; } const result: FixedCollectionValidationResult = { isValid: true, errors: [] }; // Check for invalid patterns for (const invalidPattern of pattern.invalidPatterns) { if (this.hasInvalidStructure(config, invalidPattern)) { result.isValid = false; result.errors.push({ pattern: invalidPattern, message: `Invalid structure for nodes-base.${pattern.nodeType} node: found nested "${invalidPattern}" but expected "${pattern.expectedStructure}". This causes "propertyValues[itemName] is not iterable" error in n8n.`, fix: this.generateFixMessage(pattern) }); // Generate autofix if (!result.autofix) { result.autofix = this.generateAutofix(config, pattern); } } } return result; } /** * Apply autofix to a configuration */ static applyAutofix( config: NodeConfig, pattern: FixedCollectionPattern ): NodeConfig | NodeConfigValue[] { const fixedConfig = this.generateAutofix(config, pattern); // For If/Filter nodes, the autofix might return just the values array if (pattern.nodeType === 'if' || pattern.nodeType === 'filter') { const conditions = config.conditions; if (conditions && typeof conditions === 'object' && !Array.isArray(conditions) && 'values' in conditions) { const values = conditions.values; if (values !== undefined && values !== null && (Array.isArray(values) || typeof values === 'object')) { return values as NodeConfig | NodeConfigValue[]; } } } return fixedConfig; } /** * Normalize node type to handle various formats */ private static normalizeNodeType(nodeType: string): string { return nodeType .replace('n8n-nodes-base.', '') .replace('nodes-base.', '') .replace('@n8n/n8n-nodes-langchain.', '') .toLowerCase(); } /** * Get pattern configuration for a specific node type */ private static getPatternForNode(nodeType: string): FixedCollectionPattern | undefined { return this.KNOWN_PATTERNS.find(p => p.nodeType === nodeType); } /** * Check if configuration has an invalid structure * Includes circular reference protection */ private static hasInvalidStructure( config: NodeConfig, pattern: string ): boolean { const parts = pattern.split('.'); let current: NodeConfigValue = config; const visited = new WeakSet<object>(); for (const part of parts) { // Check for null/undefined if (current === null || current === undefined) { return false; } // Check if it's an object (but not an array for property access) if (typeof current !== 'object' || Array.isArray(current)) { return false; } // Check for circular reference if (visited.has(current)) { return false; // Circular reference detected, invalid structure } visited.add(current); // Check if property exists (using hasOwnProperty to avoid prototype pollution) if (!Object.prototype.hasOwnProperty.call(current, part)) { return false; } const nextValue = (current as NodeConfig)[part]; if (typeof nextValue !== 'object' || nextValue === null) { // If we have more parts to traverse but current value is not an object, invalid structure if (parts.indexOf(part) < parts.length - 1) { return false; } } current = nextValue as NodeConfig; } return true; } /** * Generate a fix message for the specific pattern */ private static generateFixMessage(pattern: FixedCollectionPattern): string { switch (pattern.nodeType) { case 'switch': return 'Use: { "rules": { "values": [{ "conditions": {...}, "outputKey": "output1" }] } }'; case 'if': case 'filter': return 'Use: { "conditions": {...} } or { "conditions": [...] } directly, not nested under "values"'; case 'summarize': return 'Use: { "fieldsToSummarize": { "values": [...] } } not nested values.values'; case 'comparedatasets': return 'Use: { "mergeByFields": { "values": [...] } } not nested values.values'; case 'sort': return 'Use: { "sortFieldsUi": { "sortField": [...] } } not sortField.values'; case 'aggregate': return 'Use: { "fieldsToAggregate": { "fieldToAggregate": [...] } } not fieldToAggregate.values'; case 'set': return 'Use: { "fields": { "values": [...] } } not nested values.values'; case 'html': return 'Use: { "extractionValues": { "values": [...] } } not nested values.values'; case 'httprequest': return 'Use: { "body": { "parameters": [...] } } not parameters.values'; case 'airtable': return 'Use: { "sort": { "sortField": [...] } } not sortField.values'; default: return `Use ${pattern.expectedStructure} structure`; } } /** * Generate autofix for invalid structures */ private static generateAutofix( config: NodeConfig, pattern: FixedCollectionPattern ): NodeConfig | NodeConfigValue[] { const fixedConfig = { ...config }; switch (pattern.nodeType) { case 'switch': { const rules = config.rules; if (this.isNodeConfig(rules)) { const conditions = rules.conditions; if (this.isNodeConfig(conditions) && 'values' in conditions) { const values = conditions.values; fixedConfig.rules = { values: Array.isArray(values) ? values.map((condition, index) => ({ conditions: condition, outputKey: `output${index + 1}` })) : [{ conditions: values, outputKey: 'output1' }] }; } else if (conditions) { fixedConfig.rules = { values: [{ conditions: conditions, outputKey: 'output1' }] }; } } break; } case 'if': case 'filter': { const conditions = config.conditions; if (this.isNodeConfig(conditions) && 'values' in conditions) { const values = conditions.values; if (values !== undefined && values !== null && (Array.isArray(values) || typeof values === 'object')) { return values as NodeConfig | NodeConfigValue[]; } } break; } case 'summarize': { const fieldsToSummarize = config.fieldsToSummarize; if (this.isNodeConfig(fieldsToSummarize)) { const values = fieldsToSummarize.values; if (this.isNodeConfig(values) && 'values' in values) { fixedConfig.fieldsToSummarize = { values: values.values }; } } break; } case 'comparedatasets': { const mergeByFields = config.mergeByFields; if (this.isNodeConfig(mergeByFields)) { const values = mergeByFields.values; if (this.isNodeConfig(values) && 'values' in values) { fixedConfig.mergeByFields = { values: values.values }; } } break; } case 'sort': { const sortFieldsUi = config.sortFieldsUi; if (this.isNodeConfig(sortFieldsUi)) { const sortField = sortFieldsUi.sortField; if (this.isNodeConfig(sortField) && 'values' in sortField) { fixedConfig.sortFieldsUi = { sortField: sortField.values }; } } break; } case 'aggregate': { const fieldsToAggregate = config.fieldsToAggregate; if (this.isNodeConfig(fieldsToAggregate)) { const fieldToAggregate = fieldsToAggregate.fieldToAggregate; if (this.isNodeConfig(fieldToAggregate) && 'values' in fieldToAggregate) { fixedConfig.fieldsToAggregate = { fieldToAggregate: fieldToAggregate.values }; } } break; } case 'set': { const fields = config.fields; if (this.isNodeConfig(fields)) { const values = fields.values; if (this.isNodeConfig(values) && 'values' in values) { fixedConfig.fields = { values: values.values }; } } break; } case 'html': { const extractionValues = config.extractionValues; if (this.isNodeConfig(extractionValues)) { const values = extractionValues.values; if (this.isNodeConfig(values) && 'values' in values) { fixedConfig.extractionValues = { values: values.values }; } } break; } case 'httprequest': { const body = config.body; if (this.isNodeConfig(body)) { const parameters = body.parameters; if (this.isNodeConfig(parameters) && 'values' in parameters) { fixedConfig.body = { ...body, parameters: parameters.values }; } } break; } case 'airtable': { const sort = config.sort; if (this.isNodeConfig(sort)) { const sortField = sort.sortField; if (this.isNodeConfig(sortField) && 'values' in sortField) { fixedConfig.sort = { sortField: sortField.values }; } } break; } } return fixedConfig; } /** * Get all known patterns (for testing and documentation) * Returns a deep copy to prevent external modifications */ static getAllPatterns(): FixedCollectionPattern[] { return this.KNOWN_PATTERNS.map(pattern => ({ ...pattern, invalidPatterns: [...pattern.invalidPatterns] })); } /** * Check if a node type is susceptible to fixedCollection issues */ static isNodeSusceptible(nodeType: string): boolean { const normalizedType = this.normalizeNodeType(nodeType); return this.KNOWN_PATTERNS.some(p => p.nodeType === normalizedType); } }

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/88-888/n8n-mcp'

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