Skip to main content
Glama
node-validator.ts8.6 kB
import { N8nNodeType, N8nNodeProperty, ValidationError, ValidationResult } from './types.js'; import { getNodeType } from './node-registry.js'; /** * Validates a node configuration against its type definition */ export function validateNodeConfig(nodeType: string, parameters: Record<string, any>): ValidationResult { const errors: ValidationError[] = []; // Get the node type definition const nodeTypeDef = getNodeType(nodeType); if (!nodeTypeDef) { errors.push({ property: 'type', message: `Unknown node type: ${nodeType}`, code: 'INVALID_TYPE', expected: 'Known node type', actual: nodeType, }); return { valid: false, errors }; } // Validate each property in the node type definition for (const property of nodeTypeDef.properties) { const value = parameters[property.name]; const propertyErrors = validateProperty(property, value, parameters); errors.push(...propertyErrors); } return { valid: errors.length === 0, errors, }; } /** * Validates a single property against its definition */ function validateProperty( property: N8nNodeProperty, value: any, allParameters: Record<string, any> ): ValidationError[] { const errors: ValidationError[] = []; // Check if property should be shown based on displayOptions if (!shouldShowProperty(property, allParameters)) { return errors; } // Check required properties if (property.required && (value === undefined || value === null || value === '')) { errors.push({ property: property.name, message: `Required property '${property.displayName}' is missing`, code: 'MISSING_REQUIRED', expected: `Non-empty value of type ${property.type}`, actual: value, }); return errors; // Don't validate further if required value is missing } // Skip validation if value is not provided and not required if (value === undefined || value === null) { return errors; } // Type-specific validation switch (property.type) { case 'string': if (typeof value !== 'string') { errors.push({ property: property.name, message: `Property '${property.displayName}' must be a string`, code: 'INVALID_TYPE', expected: 'string', actual: typeof value, }); } break; case 'number': if (typeof value !== 'number' || isNaN(value)) { errors.push({ property: property.name, message: `Property '${property.displayName}' must be a number`, code: 'INVALID_TYPE', expected: 'number', actual: typeof value, }); } else { // Check number ranges if (property.typeOptions?.minValue !== undefined && value < property.typeOptions.minValue) { errors.push({ property: property.name, message: `Property '${property.displayName}' must be at least ${property.typeOptions.minValue}`, code: 'INVALID_RANGE', expected: `>= ${property.typeOptions.minValue}`, actual: value, }); } if (property.typeOptions?.maxValue !== undefined && value > property.typeOptions.maxValue) { errors.push({ property: property.name, message: `Property '${property.displayName}' must be at most ${property.typeOptions.maxValue}`, code: 'INVALID_RANGE', expected: `<= ${property.typeOptions.maxValue}`, actual: value, }); } } break; case 'boolean': if (typeof value !== 'boolean') { errors.push({ property: property.name, message: `Property '${property.displayName}' must be a boolean`, code: 'INVALID_TYPE', expected: 'boolean', actual: typeof value, }); } break; case 'options': if (property.options) { const validValues = property.options.map(opt => opt.value); if (!validValues.includes(value)) { errors.push({ property: property.name, message: `Property '${property.displayName}' must be one of: ${validValues.join(', ')}`, code: 'INVALID_ENUM', expected: validValues, actual: value, }); } } break; case 'multiOptions': if (property.options) { const validValues = property.options.map(opt => opt.value); if (!Array.isArray(value)) { errors.push({ property: property.name, message: `Property '${property.displayName}' must be an array`, code: 'INVALID_TYPE', expected: 'array', actual: typeof value, }); } else { const invalidValues = value.filter(v => !validValues.includes(v)); if (invalidValues.length > 0) { errors.push({ property: property.name, message: `Property '${property.displayName}' contains invalid values: ${invalidValues.join(', ')}`, code: 'INVALID_ENUM', expected: validValues, actual: invalidValues, }); } } } break; case 'credentials': // Basic credential validation - check if it's a non-empty string if (typeof value !== 'string' || value.trim() === '') { errors.push({ property: property.name, message: `Property '${property.displayName}' requires a valid credential`, code: 'MISSING_CREDENTIAL', expected: 'non-empty credential string', actual: value, }); } break; case 'collection': case 'fixedCollection': if (value !== null && typeof value !== 'object') { errors.push({ property: property.name, message: `Property '${property.displayName}' must be an object`, code: 'INVALID_TYPE', expected: 'object', actual: typeof value, }); } break; case 'hidden': case 'notice': // These types don't require validation break; default: // Unknown type - log but don't fail validation console.warn(`Unknown property type: ${property.type} for property ${property.name}`); break; } return errors; } /** * Determines if a property should be shown based on display options */ function shouldShowProperty(property: N8nNodeProperty, allParameters: Record<string, any>): boolean { if (!property.displayOptions) { return true; // Show by default if no display options } // Check show conditions if (property.displayOptions.show) { for (const [paramName, allowedValues] of Object.entries(property.displayOptions.show)) { const paramValue = allParameters[paramName]; if (!allowedValues.includes(paramValue)) { return false; // Hide if show condition not met } } } // Check hide conditions if (property.displayOptions.hide) { for (const [paramName, hiddenValues] of Object.entries(property.displayOptions.hide)) { const paramValue = allParameters[paramName]; if (hiddenValues.includes(paramValue)) { return false; // Hide if hide condition met } } } return true; } /** * Validates that required credentials are present */ export function validateCredentials(nodeType: string, credentials: Record<string, string> = {}): ValidationError[] { const errors: ValidationError[] = []; const nodeTypeDef = getNodeType(nodeType); if (!nodeTypeDef || !nodeTypeDef.credentials) { return errors; } for (const credentialType of nodeTypeDef.credentials) { if (credentialType.required && !credentials[credentialType.name]) { errors.push({ property: 'credentials', message: `Required credential '${credentialType.name}' is missing`, code: 'MISSING_CREDENTIAL', expected: credentialType.name, actual: 'undefined', }); } } return errors; } /** * Performs comprehensive validation of a node configuration including credentials */ export function validateFullNodeConfig( nodeType: string, parameters: Record<string, any>, credentials: Record<string, string> = {} ): ValidationResult { const parameterValidation = validateNodeConfig(nodeType, parameters); const credentialErrors = validateCredentials(nodeType, credentials); return { valid: parameterValidation.valid && credentialErrors.length === 0, errors: [...parameterValidation.errors, ...credentialErrors], }; }

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

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