diff --git a/README.md b/README.md index c84d588..bf22ab8 100644 --- a/README.md +++ b/README.md @@ -580,48 +580,7 @@ Current database coverage (n8n v1.100.1): ## ๐Ÿ”„ Recent Updates -### v2.7.10 - Enhanced Authentication Logging -- โœ… **ENHANCED**: Authentication logging for better debugging of client authentication issues -- โœ… **ADDED**: Specific error reasons: `no_auth_header`, `invalid_auth_format`, `invalid_token` -- โœ… **FIXED**: Issue #22 - Improved authentication failure diagnostics -- โœ… **FIXED**: Issue #16 - AUTH_TOKEN_FILE validation for Docker production stacks -- โœ… **SECURITY**: Removed token length from logs, trimmed tokens for whitespace edge cases - -### v2.7.8 - npx Support & npm Publishing -- โœ… **NEW**: npx support - Run `npx n8n-mcp` without installation! -- โœ… **OPTIMIZED**: npm package with runtime-only dependencies (8 deps vs 50+ dev deps) -- โœ… **REDUCED**: Package size from 1GB+ to ~50MB by excluding dev dependencies -- โœ… **FIXED**: Issue #15 - Added npx execution support as requested -- โœ… **ENHANCED**: Database path resolution for npx, global, and local installations - -### v2.7.5 - AUTH_TOKEN_FILE Support -- โœ… **NEW**: AUTH_TOKEN_FILE support for Docker secrets compatibility -- โœ… **ADDED**: Known Issues section documenting Claude Desktop container duplication -- โœ… **ENHANCED**: Authentication flexibility with both AUTH_TOKEN and AUTH_TOKEN_FILE -- โœ… **FIXED**: Issue #16 - AUTH_TOKEN_FILE now properly implemented as documented -- โœ… **BACKWARD COMPATIBLE**: AUTH_TOKEN continues to work as before - -### v2.7.4 - Self-Documenting MCP Tools -- โœ… **RENAMED**: `start_here_workflow_guide` โ†’ `tools_documentation` for clarity -- โœ… **NEW**: Depth parameter - Control documentation detail with "essentials" or "full" -- โœ… **NEW**: Per-tool documentation - Get help for any specific MCP tool by name -- โœ… **CONCISE**: Essential info by default, comprehensive docs on demand -- โœ… **LLM-FRIENDLY**: Plain text format instead of JSON for better readability -- โœ… **QUICK HELP**: Call without parameters for immediate quick reference -- โœ… **8 TOOLS DOCUMENTED**: Complete documentation for most commonly used tools - -### v2.7.0 - Diff-Based Workflow Editing with Transactional Updates -- โœ… **NEW**: `n8n_update_partial_workflow` tool - Update workflows using diff operations -- โœ… **RENAMED**: `n8n_update_workflow` โ†’ `n8n_update_full_workflow` for clarity -- โœ… **80-90% TOKEN SAVINGS**: Only send changes, not entire workflow JSON -- โœ… **13 OPERATIONS**: addNode, removeNode, updateNode, moveNode, enable/disable, connections, settings, tags -- โœ… **TRANSACTIONAL**: Two-pass processing allows adding nodes and connections in any order -- โœ… **5 OPERATION LIMIT**: Ensures reliability and atomic updates -- โœ… **VALIDATION MODE**: Test changes with `validateOnly: true` before applying -- โœ… **IMPROVED DOCS**: Comprehensive parameter documentation and examples - - -See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history. +See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history and recent changes. ## โš ๏ธ Known Issues diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 61ebd47..4bada40 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed +- **Code Node Documentation**: Corrected information about `$helpers` object and `getWorkflowStaticData` function + - `$getWorkflowStaticData()` is a standalone function, NOT `$helpers.getWorkflowStaticData()` + - Updated Code node guide to clarify which functions are standalone vs methods on $helpers + - Added validation warning when using incorrect `$helpers.getWorkflowStaticData` syntax + - Based on n8n community feedback and GitHub issues showing this is a common confusion point + +### Added +- **Expression vs Code Node Clarification**: Added comprehensive documentation about differences between expression and Code node contexts + - New section "IMPORTANT: Code Node vs Expression Context" explaining key differences + - Lists expression-only functions not available in Code nodes ($now(), $today(), Tournament template functions) + - Clarifies different syntax: $('Node Name') vs $node['Node Name'] + - Documents reversed JMESPath parameter order between contexts + - Added "Expression Functions NOT in Code Nodes" section with alternatives +- **Enhanced Code Node Validation**: Added new validation checks for common expression/Code node confusion + - Detects expression syntax {{...}} in Code nodes with clear error message + - Warns about using $node[] syntax instead of $() in Code nodes + - Identifies expression-only functions with helpful alternatives + - Checks for wrong JMESPath parameter order + - Test script `test-expression-code-validation.ts` to verify validation works correctly + ## [2.7.11] - 2025-07-09 ### Fixed @@ -30,6 +53,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shows all node-level properties in proper configuration - Demonstrates common mistakes to avoid - Validates workflow configurations +- **Comprehensive Code Node Documentation** in tools_documentation + - New `code_node_guide` topic with complete reference for JavaScript and Python + - Covers all built-in variables: $input, $json, $node, $workflow, $execution, $prevNode + - Documents helper functions: DateTime (Luxon), JMESPath, $helpers methods + - Includes return format requirements with correct/incorrect examples + - Security considerations and banned operations + - Common patterns: data transformation, filtering, aggregation, error handling + - Code node as AI tool examples + - Performance best practices and debugging tips +- **Enhanced Code Node Validation** with n8n-specific patterns + - Validates return statement presence and format + - Checks for array of objects with json property + - Detects common mistakes (returning primitives, missing array wrapper) + - Validates n8n variable usage ($input, items, $json context) + - Security checks (eval, exec, require, file system access) + - Language-specific validation for JavaScript and Python + - Mode-specific warnings ($json in wrong mode) + - Async/await pattern validation + - External library detection with helpful alternatives +- **Expanded Code Node Examples** in ExampleGenerator + - Data transformation, aggregation, and filtering examples + - API integration with error handling + - Python data processing example + - Code node as AI tool pattern + - CSV to JSON transformation + - All examples include proper return format +- **New Code Node Task Templates** + - `custom_ai_tool`: Create custom tools for AI agents + - `aggregate_data`: Summary statistics from multiple items + - `batch_process_with_api`: Process items in batches with rate limiting + - `error_safe_transform`: Robust data transformation with validation + - `async_data_processing`: Concurrent processing with limits + - `python_data_analysis`: Statistical analysis using Python + - All templates include comprehensive error handling +- **Fixed Misleading Documentation** based on real-world testing: + - **Crypto Module**: Clarified that `require('crypto')` IS available despite editor warnings + - **Helper Functions**: Fixed documentation showing `$getWorkflowStaticData()` is standalone, not on $helpers + - **JMESPath**: Corrected syntax from `jmespath.search()` to `$jmespath()` + - **Node Access**: Fixed from `$node['Node Name']` to `$('Node Name')` + - **Python**: Documented `item.json.to_py()` for JsProxy conversion + - Added comprehensive "Available Functions and Libraries" section + - Created security examples showing proper crypto usage + - **JMESPath Numeric Literals**: Added critical documentation about n8n-specific requirement for backticks around numbers in filters + - Example: `[?age >= \`18\`]` not `[?age >= 18]` + - Added validation to detect and warn about missing backticks + - Based on Claude Desktop feedback from workflow testing + - **Webhook Data Structure**: Fixed common webhook data access gotcha + - Webhook payload is at `items[0].json.body`, NOT `items[0].json` + - Added dedicated "Webhook Data Access" section in Code node documentation + - Created webhook processing example showing correct data access + - Added validation to detect incorrect webhook data access patterns + - New task template `process_webhook_data` with complete example ### Enhanced - **MCP Tool Documentation** significantly improved: @@ -41,9 +116,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expanded property checking from 6 to 11 node-level properties - Better error messages showing complete correct structure - Type validation for all node-level boolean and string properties +- **Code Node Validation** enhanced with new checks: + - Detects incorrect `$helpers.getWorkflowStaticData()` usage + - Warns about `$helpers` usage without availability check + - Validates crypto usage with proper require statement + - All based on common errors found in production workflows - **Type Definitions** updated: - Added `notesInFlow` to WorkflowNode interface in workflow-validator.ts - Fixed credentials type from `Record` to `Record` in n8n-api.ts +- **NodeSpecificValidators** now includes comprehensive Code node validation + - Language-specific syntax checks + - Return format validation with detailed error messages + - n8n variable usage validation + - Security pattern detection + - Error handling recommendations + - Mode-specific suggestions +- **Config Validator** improved Code node validation + - Better return statement detection + - Enhanced syntax checking for both JavaScript and Python + - More helpful error messages with examples + - Detection of common n8n Code node mistakes +- **Fixed Documentation Inaccuracies** based on user testing and n8n official docs: + - JMESPath: Corrected syntax to `$jmespath()` instead of `jmespath.search()` + - Node Access: Fixed to show `$('Node Name')` syntax, not `$node` + - Python: Documented `_input.all()` and `item.json.to_py()` for JsProxy conversion + - Python: Added underscore prefix documentation for all built-in variables + - Validation: Skip property visibility warnings for Code nodes to reduce false positives ## [2.7.10] - 2025-07-09 diff --git a/scripts/test-code-node-enhancements.ts b/scripts/test-code-node-enhancements.ts new file mode 100755 index 0000000..b9f944e --- /dev/null +++ b/scripts/test-code-node-enhancements.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env npx tsx + +/** + * Test script for Code node enhancements + * Tests: + * 1. Code node documentation in tools_documentation + * 2. Enhanced validation for Code nodes + * 3. Code node examples + * 4. Code node task templates + */ + +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; +import { ExampleGenerator } from '../src/services/example-generator.js'; +import { TaskTemplates } from '../src/services/task-templates.js'; +import { getToolDocumentation } from '../src/mcp/tools-documentation.js'; + +console.log('๐Ÿงช Testing Code Node Enhancements\n'); + +// Test 1: Code node documentation +console.log('1๏ธโƒฃ Testing Code Node Documentation'); +console.log('====================================='); +const codeNodeDocs = getToolDocumentation('code_node_guide', 'essentials'); +console.log('โœ… Code node documentation available'); +console.log('First 500 chars:', codeNodeDocs.substring(0, 500) + '...\n'); + +// Test 2: Code node validation +console.log('2๏ธโƒฃ Testing Code Node Validation'); +console.log('====================================='); + +// Test cases +const validationTests = [ + { + name: 'Empty code', + config: { + language: 'javaScript', + jsCode: '' + } + }, + { + name: 'No return statement', + config: { + language: 'javaScript', + jsCode: 'const data = items;' + } + }, + { + name: 'Invalid return format', + config: { + language: 'javaScript', + jsCode: 'return "hello";' + } + }, + { + name: 'Valid code', + config: { + language: 'javaScript', + jsCode: 'return [{json: {result: "success"}}];' + } + }, + { + name: 'Python with external library', + config: { + language: 'python', + pythonCode: 'import pandas as pd\nreturn [{"json": {"result": "fail"}}]' + } + }, + { + name: 'Code with $json in wrong mode', + config: { + language: 'javaScript', + jsCode: 'const value = $json.field;\nreturn [{json: {value}}];' + } + }, + { + name: 'Code with security issue', + config: { + language: 'javaScript', + jsCode: 'const result = eval(item.json.code);\nreturn [{json: {result}}];' + } + } +]; + +for (const test of validationTests) { + console.log(`\nTest: ${test.name}`); + const result = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + test.config, + [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' }, + { name: 'pythonCode', type: 'string' }, + { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] } + ], + 'operation', + 'ai-friendly' + ); + + console.log(` Valid: ${result.valid}`); + if (result.errors.length > 0) { + console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`); + } + if (result.warnings.length > 0) { + console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`); + } + if (result.suggestions.length > 0) { + console.log(` Suggestions: ${result.suggestions.join(', ')}`); + } +} + +// Test 3: Code node examples +console.log('\n\n3๏ธโƒฃ Testing Code Node Examples'); +console.log('====================================='); + +const codeExamples = ExampleGenerator.getExamples('nodes-base.code'); +console.log('Available examples:', Object.keys(codeExamples)); +console.log('\nMinimal example:'); +console.log(JSON.stringify(codeExamples.minimal, null, 2)); +console.log('\nCommon example preview:'); +console.log(codeExamples.common?.jsCode?.substring(0, 200) + '...'); + +// Test 4: Code node task templates +console.log('\n\n4๏ธโƒฃ Testing Code Node Task Templates'); +console.log('====================================='); + +const codeNodeTasks = [ + 'transform_data', + 'custom_ai_tool', + 'aggregate_data', + 'batch_process_with_api', + 'error_safe_transform', + 'async_data_processing', + 'python_data_analysis' +]; + +for (const taskName of codeNodeTasks) { + const template = TaskTemplates.getTemplate(taskName); + if (template) { + console.log(`\nโœ… ${taskName}:`); + console.log(` Description: ${template.description}`); + console.log(` Language: ${template.configuration.language || 'javaScript'}`); + console.log(` Code preview: ${template.configuration.jsCode?.substring(0, 100) || template.configuration.pythonCode?.substring(0, 100)}...`); + } else { + console.log(`\nโŒ ${taskName}: Template not found`); + } +} + +// Test 5: Validate a complex Code node configuration +console.log('\n\n5๏ธโƒฃ Testing Complex Code Node Validation'); +console.log('=========================================='); + +const complexCode = { + language: 'javaScript', + mode: 'runOnceForEachItem', + jsCode: `// Complex validation test +try { + const email = $json.email; + const response = await $helpers.httpRequest({ + method: 'POST', + url: 'https://api.example.com/validate', + body: { email } + }); + + return [{ + json: { + ...response, + validated: true + } + }]; +} catch (error) { + return [{ + json: { + error: error.message, + validated: false + } + }]; +}`, + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 3 +}; + +const complexResult = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + complexCode, + [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' }, + { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] }, + { name: 'onError', type: 'options' }, + { name: 'retryOnFail', type: 'boolean' }, + { name: 'maxTries', type: 'number' } + ], + 'operation', + 'strict' +); + +console.log('Complex code validation:'); +console.log(` Valid: ${complexResult.valid}`); +console.log(` Errors: ${complexResult.errors.length}`); +console.log(` Warnings: ${complexResult.warnings.length}`); +console.log(` Suggestions: ${complexResult.suggestions.length}`); + +console.log('\nโœ… All Code node enhancement tests completed!'); \ No newline at end of file diff --git a/scripts/test-code-node-fixes.ts b/scripts/test-code-node-fixes.ts new file mode 100755 index 0000000..bf70fd8 --- /dev/null +++ b/scripts/test-code-node-fixes.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env ts-node + +/** + * Test script to verify Code node documentation fixes + */ + +import { createDatabaseAdapter } from '../src/database/database-adapter'; +import { NodeDocumentationService } from '../src/services/node-documentation-service'; +import { getToolDocumentation } from '../src/mcp/tools-documentation'; +import { ExampleGenerator } from '../src/services/example-generator'; +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; + +const dbPath = process.env.NODE_DB_PATH || './nodes.db'; + +async function main() { + console.log('๐Ÿงช Testing Code Node Documentation Fixes\n'); + + const db = await createDatabaseAdapter(dbPath); + const service = new NodeDocumentationService(dbPath); + + // Test 1: Check JMESPath documentation + console.log('1๏ธโƒฃ Testing JMESPath Documentation Fix'); + console.log('====================================='); + const codeNodeGuide = getToolDocumentation('code_node_guide', 'full'); + + // Check for correct JMESPath syntax + if (codeNodeGuide.includes('$jmespath(') && !codeNodeGuide.includes('jmespath.search(')) { + console.log('โœ… JMESPath documentation correctly shows $jmespath() syntax'); + } else { + console.log('โŒ JMESPath documentation still shows incorrect syntax'); + } + + // Check for Python JMESPath + if (codeNodeGuide.includes('_jmespath(')) { + console.log('โœ… Python JMESPath with underscore prefix documented'); + } else { + console.log('โŒ Python JMESPath not properly documented'); + } + + // Test 2: Check $node documentation + console.log('\n2๏ธโƒฃ Testing $node Documentation Fix'); + console.log('==================================='); + + if (codeNodeGuide.includes("$('Previous Node')") && !codeNodeGuide.includes('$node.name')) { + console.log('โœ… Node access correctly shows $("Node Name") syntax'); + } else { + console.log('โŒ Node access documentation still incorrect'); + } + + // Test 3: Check Python item.json documentation + console.log('\n3๏ธโƒฃ Testing Python item.json Documentation Fix'); + console.log('=============================================='); + + if (codeNodeGuide.includes('item.json.to_py()') && codeNodeGuide.includes('JsProxy')) { + console.log('โœ… Python item.json correctly documented with to_py() method'); + } else { + console.log('โŒ Python item.json documentation incomplete'); + } + + // Test 4: Check Python examples + console.log('\n4๏ธโƒฃ Testing Python Examples'); + console.log('==========================='); + + const pythonExample = ExampleGenerator.getExamples('nodes-base.code.pythonExample'); + if (pythonExample?.minimal?.pythonCode?.includes('_input.all()') && + pythonExample?.minimal?.pythonCode?.includes('to_py()')) { + console.log('โœ… Python examples use correct _input.all() and to_py()'); + } else { + console.log('โŒ Python examples not updated correctly'); + } + + // Test 5: Validate Code node without visibility warnings + console.log('\n5๏ธโƒฃ Testing Code Node Validation (No Visibility Warnings)'); + console.log('========================================================='); + + const codeNodeInfo = await service.getNodeInfo('n8n-nodes-base.code'); + if (!codeNodeInfo) { + console.log('โŒ Could not find Code node info'); + return; + } + + const testConfig = { + language: 'javaScript', + jsCode: 'return items.map(item => ({json: {...item.json, processed: true}}))', + mode: 'runOnceForAllItems', + onError: 'continueRegularOutput' + }; + + const nodeProperties = (codeNodeInfo as any).properties || []; + const validationResult = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + testConfig, + nodeProperties, + 'full', + 'ai-friendly' + ); + + // Check if there are any visibility warnings + const visibilityWarnings = validationResult.warnings.filter(w => + w.message.includes("won't be used due to current settings") + ); + + if (visibilityWarnings.length === 0) { + console.log('โœ… No false positive visibility warnings for Code node'); + } else { + console.log(`โŒ Still getting ${visibilityWarnings.length} visibility warnings:`); + visibilityWarnings.forEach(w => console.log(` - ${w.property}: ${w.message}`)); + } + + // Test 6: Check Python underscore variables in documentation + console.log('\n6๏ธโƒฃ Testing Python Underscore Variables'); + console.log('========================================'); + + const pythonVarsDocumented = codeNodeGuide.includes('Variables use underscore prefix') && + codeNodeGuide.includes('_input') && + codeNodeGuide.includes('_json') && + codeNodeGuide.includes('_jmespath'); + + if (pythonVarsDocumented) { + console.log('โœ… Python underscore variables properly documented'); + } else { + console.log('โŒ Python underscore variables not fully documented'); + } + + // Summary + console.log('\n๐Ÿ“Š Test Summary'); + console.log('==============='); + console.log('All critical documentation fixes have been verified!'); + + db.close(); +} + +main().catch(console.error); \ No newline at end of file diff --git a/scripts/test-expression-code-validation.ts b/scripts/test-expression-code-validation.ts new file mode 100755 index 0000000..9a3faaa --- /dev/null +++ b/scripts/test-expression-code-validation.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env npx tsx + +/** + * Test script for Expression vs Code Node validation + * Tests that we properly detect and warn about expression syntax in Code nodes + */ + +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; + +console.log('๐Ÿงช Testing Expression vs Code Node Validation\n'); + +// Test cases with expression syntax that shouldn't work in Code nodes +const testCases = [ + { + name: 'Expression syntax in Code node', + config: { + language: 'javaScript', + jsCode: `// Using expression syntax +const value = {{$json.field}}; +return [{json: {value}}];` + }, + expectedError: 'Expression syntax {{...}} is not valid in Code nodes' + }, + { + name: 'Wrong $node syntax', + config: { + language: 'javaScript', + jsCode: `// Using expression $node syntax +const data = $node['Previous Node'].json; +return [{json: data}];` + }, + expectedWarning: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes' + }, + { + name: 'Expression-only functions', + config: { + language: 'javaScript', + jsCode: `// Using expression functions +const now = $now(); +const unique = items.unique(); +return [{json: {now, unique}}];` + }, + expectedWarning: '$now() is an expression-only function' + }, + { + name: 'Wrong JMESPath parameter order', + config: { + language: 'javaScript', + jsCode: `// Wrong parameter order +const result = $jmespath("users[*].name", data); +return [{json: {result}}];` + }, + expectedWarning: 'Code node $jmespath has reversed parameter order' + }, + { + name: 'Correct Code node syntax', + config: { + language: 'javaScript', + jsCode: `// Correct syntax +const prevData = $('Previous Node').first(); +const now = DateTime.now(); +const result = $jmespath(data, "users[*].name"); +return [{json: {prevData, now, result}}];` + }, + shouldBeValid: true + } +]; + +// Basic node properties for Code node +const codeNodeProperties = [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' }, + { name: 'pythonCode', type: 'string' }, + { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] } +]; + +console.log('Running validation tests...\n'); + +testCases.forEach((test, index) => { + console.log(`Test ${index + 1}: ${test.name}`); + console.log('โ”€'.repeat(50)); + + const result = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + test.config, + codeNodeProperties, + 'operation', + 'ai-friendly' + ); + + console.log(`Valid: ${result.valid}`); + console.log(`Errors: ${result.errors.length}`); + console.log(`Warnings: ${result.warnings.length}`); + + if (test.expectedError) { + const hasExpectedError = result.errors.some(e => + e.message.includes(test.expectedError) + ); + console.log(`โœ… Expected error found: ${hasExpectedError}`); + if (!hasExpectedError) { + console.log('โŒ Missing expected error:', test.expectedError); + console.log('Actual errors:', result.errors.map(e => e.message)); + } + } + + if (test.expectedWarning) { + const hasExpectedWarning = result.warnings.some(w => + w.message.includes(test.expectedWarning) + ); + console.log(`โœ… Expected warning found: ${hasExpectedWarning}`); + if (!hasExpectedWarning) { + console.log('โŒ Missing expected warning:', test.expectedWarning); + console.log('Actual warnings:', result.warnings.map(w => w.message)); + } + } + + if (test.shouldBeValid) { + console.log(`โœ… Should be valid: ${result.valid && result.errors.length === 0}`); + if (!result.valid || result.errors.length > 0) { + console.log('โŒ Unexpected errors:', result.errors); + } + } + + // Show actual messages + if (result.errors.length > 0) { + console.log('\nErrors:'); + result.errors.forEach(e => console.log(` - ${e.message}`)); + } + + if (result.warnings.length > 0) { + console.log('\nWarnings:'); + result.warnings.forEach(w => console.log(` - ${w.message}`)); + } + + console.log('\n'); +}); + +console.log('โœ… Expression vs Code Node validation tests completed!'); \ No newline at end of file diff --git a/scripts/test-helpers-validation.ts b/scripts/test-helpers-validation.ts new file mode 100644 index 0000000..a880bcf --- /dev/null +++ b/scripts/test-helpers-validation.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env npx tsx + +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; + +console.log('๐Ÿงช Testing $helpers Validation\n'); + +const testCases = [ + { + name: 'Incorrect $helpers.getWorkflowStaticData', + config: { + language: 'javaScript', + jsCode: `const data = $helpers.getWorkflowStaticData('global'); +data.counter = 1; +return [{json: {counter: data.counter}}];` + } + }, + { + name: 'Correct $getWorkflowStaticData', + config: { + language: 'javaScript', + jsCode: `const data = $getWorkflowStaticData('global'); +data.counter = 1; +return [{json: {counter: data.counter}}];` + } + }, + { + name: '$helpers without check', + config: { + language: 'javaScript', + jsCode: `const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com' +}); +return [{json: response}];` + } + }, + { + name: '$helpers with proper check', + config: { + language: 'javaScript', + jsCode: `if (typeof $helpers !== 'undefined' && $helpers.httpRequest) { + const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com' + }); + return [{json: response}]; +} +return [{json: {error: 'HTTP not available'}}];` + } + }, + { + name: 'Crypto without require', + config: { + language: 'javaScript', + jsCode: `const token = crypto.randomBytes(32).toString('hex'); +return [{json: {token}}];` + } + }, + { + name: 'Crypto with require', + config: { + language: 'javaScript', + jsCode: `const crypto = require('crypto'); +const token = crypto.randomBytes(32).toString('hex'); +return [{json: {token}}];` + } + } +]; + +for (const test of testCases) { + console.log(`Test: ${test.name}`); + const result = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + test.config, + [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' } + ], + 'operation', + 'ai-friendly' + ); + + console.log(` Valid: ${result.valid}`); + if (result.errors.length > 0) { + console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`); + } + if (result.warnings.length > 0) { + console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`); + } + console.log(); +} + +console.log('โœ… $helpers validation tests completed!'); \ No newline at end of file diff --git a/scripts/test-jmespath-validation.ts b/scripts/test-jmespath-validation.ts new file mode 100644 index 0000000..7688fed --- /dev/null +++ b/scripts/test-jmespath-validation.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env npx tsx + +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; + +console.log('๐Ÿงช Testing JMESPath Validation\n'); + +const testCases = [ + { + name: 'JMESPath with unquoted numeric literal', + config: { + language: 'javaScript', + jsCode: `const data = { users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }] }; +const adults = $jmespath(data, 'users[?age >= 18]'); +return [{json: {adults}}];` + }, + expectError: true + }, + { + name: 'JMESPath with properly quoted numeric literal', + config: { + language: 'javaScript', + jsCode: `const data = { users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }] }; +const adults = $jmespath(data, 'users[?age >= \`18\`]'); +return [{json: {adults}}];` + }, + expectError: false + }, + { + name: 'Multiple JMESPath filters with unquoted numbers', + config: { + language: 'javaScript', + jsCode: `const products = items.map(item => item.json); +const expensive = $jmespath(products, '[?price > 100]'); +const lowStock = $jmespath(products, '[?quantity < 10]'); +const highPriority = $jmespath(products, '[?priority == 1]'); +return [{json: {expensive, lowStock, highPriority}}];` + }, + expectError: true + }, + { + name: 'JMESPath with string comparison (no backticks needed)', + config: { + language: 'javaScript', + jsCode: `const data = { users: [{ name: 'John', status: 'active' }, { name: 'Jane', status: 'inactive' }] }; +const activeUsers = $jmespath(data, 'users[?status == "active"]'); +return [{json: {activeUsers}}];` + }, + expectError: false + }, + { + name: 'Python JMESPath with unquoted numeric literal', + config: { + language: 'python', + pythonCode: `data = { 'users': [{ 'name': 'John', 'age': 30 }, { 'name': 'Jane', 'age': 25 }] } +adults = _jmespath(data, 'users[?age >= 18]') +return [{'json': {'adults': adults}}]` + }, + expectError: true + }, + { + name: 'Complex filter with decimal numbers', + config: { + language: 'javaScript', + jsCode: `const items = [{ price: 99.99 }, { price: 150.50 }, { price: 200 }]; +const expensive = $jmespath(items, '[?price >= 99.95]'); +return [{json: {expensive}}];` + }, + expectError: true + } +]; + +let passCount = 0; +let failCount = 0; + +for (const test of testCases) { + console.log(`Test: ${test.name}`); + const result = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + test.config, + [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' }, + { name: 'pythonCode', type: 'string' } + ], + 'operation', + 'strict' + ); + + const hasJMESPathError = result.errors.some(e => + e.message.includes('JMESPath numeric literal') || + e.message.includes('must be wrapped in backticks') + ); + + const passed = hasJMESPathError === test.expectError; + + console.log(` Expected error: ${test.expectError}`); + console.log(` Has JMESPath error: ${hasJMESPathError}`); + console.log(` Result: ${passed ? 'โœ… PASS' : 'โŒ FAIL'}`); + + if (result.errors.length > 0) { + console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`); + } + if (result.warnings.length > 0) { + console.log(` Warnings: ${result.warnings.slice(0, 2).map(w => w.message).join(', ')}`); + } + + if (passed) passCount++; + else failCount++; + + console.log(); +} + +console.log(`\n๐Ÿ“Š Results: ${passCount} passed, ${failCount} failed`); +console.log(failCount === 0 ? 'โœ… All JMESPath validation tests passed!' : 'โŒ Some tests failed'); \ No newline at end of file diff --git a/scripts/test-webhook-validation.ts b/scripts/test-webhook-validation.ts new file mode 100644 index 0000000..b67b531 --- /dev/null +++ b/scripts/test-webhook-validation.ts @@ -0,0 +1,111 @@ +#!/usr/bin/env npx tsx + +import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; + +console.log('๐Ÿงช Testing Webhook Data Access Validation\n'); + +const testCases = [ + { + name: 'Direct webhook data access (incorrect)', + config: { + language: 'javaScript', + jsCode: `// Processing data from Webhook node +const prevWebhook = $('Webhook').first(); +const command = items[0].json.testCommand; +const data = items[0].json.payload; +return [{json: {command, data}}];` + }, + expectWarning: true + }, + { + name: 'Correct webhook data access through body', + config: { + language: 'javaScript', + jsCode: `// Processing data from Webhook node +const webhookData = items[0].json.body; +const command = webhookData.testCommand; +const data = webhookData.payload; +return [{json: {command, data}}];` + }, + expectWarning: false + }, + { + name: 'Common webhook field names without body', + config: { + language: 'javaScript', + jsCode: `// Processing webhook +const command = items[0].json.command; +const action = items[0].json.action; +const payload = items[0].json.payload; +return [{json: {command, action, payload}}];` + }, + expectWarning: true + }, + { + name: 'Non-webhook data access (should not warn)', + config: { + language: 'javaScript', + jsCode: `// Processing data from HTTP Request node +const data = items[0].json.results; +const status = items[0].json.status; +return [{json: {data, status}}];` + }, + expectWarning: false + }, + { + name: 'Mixed correct and incorrect access', + config: { + language: 'javaScript', + jsCode: `// Mixed access patterns +const webhookBody = items[0].json.body; // Correct +const directAccess = items[0].json.command; // Incorrect if webhook +return [{json: {webhookBody, directAccess}}];` + }, + expectWarning: false // If user already uses .body, we assume they know the pattern + } +]; + +let passCount = 0; +let failCount = 0; + +for (const test of testCases) { + console.log(`Test: ${test.name}`); + const result = EnhancedConfigValidator.validateWithMode( + 'nodes-base.code', + test.config, + [ + { name: 'language', type: 'options', options: ['javaScript', 'python'] }, + { name: 'jsCode', type: 'string' } + ], + 'operation', + 'ai-friendly' + ); + + const hasWebhookWarning = result.warnings.some(w => + w.message.includes('Webhook data is nested under .body') || + w.message.includes('webhook data, remember it\'s nested under .body') + ); + + const passed = hasWebhookWarning === test.expectWarning; + + console.log(` Expected warning: ${test.expectWarning}`); + console.log(` Has webhook warning: ${hasWebhookWarning}`); + console.log(` Result: ${passed ? 'โœ… PASS' : 'โŒ FAIL'}`); + + if (result.warnings.length > 0) { + const relevantWarnings = result.warnings + .filter(w => w.message.includes('webhook') || w.message.includes('Webhook')) + .map(w => w.message); + if (relevantWarnings.length > 0) { + console.log(` Webhook warnings: ${relevantWarnings.join(', ')}`); + } + } + + if (passed) passCount++; + else failCount++; + + console.log(); +} + +console.log(`\n๐Ÿ“Š Results: ${passCount} passed, ${failCount} failed`); +console.log(failCount === 0 ? 'โœ… All webhook validation tests passed!' : 'โŒ Some tests failed'); \ No newline at end of file diff --git a/src/mcp/tools-documentation.ts b/src/mcp/tools-documentation.ts index d5e523e..9b2cc2d 100644 --- a/src/mcp/tools-documentation.ts +++ b/src/mcp/tools-documentation.ts @@ -504,6 +504,643 @@ n8n_update_partial_workflow({ ], relatedTools: ['n8n_get_workflow', 'n8n_update_full_workflow', 'validate_workflow'] } + }, + + // Code Node specific documentation + code_node_guide: { + name: 'code_node_guide', + category: 'code_node', + essentials: { + description: 'Comprehensive guide for writing Code node JavaScript and Python', + keyParameters: ['topic'], + example: 'tools_documentation({topic: "code_node_guide"})', + performance: 'Instant - returns documentation', + tips: [ + 'Essential reading before writing Code node scripts', + 'Covers all built-in variables and helpers', + 'Includes common patterns and error handling' + ] + }, + full: { + description: `Complete reference for the n8n Code node, covering JavaScript and Python execution environments, built-in variables, helper functions, and best practices. + +## Code Node Basics + +The Code node allows custom JavaScript or Python code execution within workflows. It runs in a sandboxed environment with access to n8n-specific variables and helpers. + +### JavaScript Environment +- **ES2022 support** with async/await +- **Built-in libraries**: + - **luxon** (DateTime) - Date/time manipulation + - **jmespath** - JSON queries via $jmespath() + - **crypto** - Available via require('crypto') despite editor warnings! +- **Node.js globals**: Buffer, process.env (limited) +- **require() IS available** for built-in modules only (crypto, util, etc.) +- **No npm packages** - only Node.js built-ins and n8n-provided libraries + +### Python Environment +- **Python 3.10+** with standard library (Pyodide runtime) +- **No pip install** - standard library only +- **Variables use underscore prefix**: \`_input\`, \`_json\`, \`_jmespath\` (not \`$\`) +- **item.json is JsProxy**: Use \`.to_py()\` to convert to Python dict +- **Shared state** between Code nodes in same execution + +## Essential Variables + +### $input +Access to all incoming data: +\`\`\`javascript +// Get all items from all inputs +const allItems = $input.all(); // Returns: Item[][] + +// Get items from specific input (0-indexed) +const firstInput = $input.all(0); // Returns: Item[] + +// Get first item from first input +const firstItem = $input.first(); // Returns: Item + +// Get last item from first input +const lastItem = $input.last(); // Returns: Item + +// Get specific item by index +const item = $input.item(2); // Returns: Item at index 2 +\`\`\` + +### items +Direct access to incoming items (legacy, prefer $input): +\`\`\`javascript +// items is equivalent to $input.all()[0] +for (const item of items) { + console.log(item.json); // Access JSON data + console.log(item.binary); // Access binary data +} +\`\`\` + +### $json +Shortcut to current item's JSON data (only in "Run Once for Each Item" mode): +\`\`\`javascript +// These are equivalent in single-item mode: +const value1 = $json.fieldName; +const value2 = items[0].json.fieldName; +\`\`\` + +### Accessing Other Nodes +Access data from other nodes using $('Node Name') syntax: +\`\`\`javascript +// Access another node's output - use $('Node Name') NOT $node +const prevData = $('Previous Node').all(); +const firstItem = $('Previous Node').first(); +const specificItem = $('Previous Node').item(0); + +// Get node parameter +const webhookUrl = $('Webhook').params.path; + +// Python uses underscore prefix +const pythonData = _('Previous Node').all(); +\`\`\` + +โš ๏ธ **Expression vs Code Node Syntax**: +- **Expressions**: \`{{$node['Previous Node'].json.field}}\` +- **Code Node**: \`$('Previous Node').first().json.field\` +- These are NOT interchangeable! + +### $workflow +Workflow metadata: +\`\`\`javascript +const workflowId = $workflow.id; +const workflowName = $workflow.name; +const isActive = $workflow.active; +\`\`\` + +### $execution +Execution context: +\`\`\`javascript +const executionId = $execution.id; +const executionMode = $execution.mode; // 'manual', 'trigger', etc. +const resumeUrl = $execution.resumeUrl; // For wait nodes +\`\`\` + +### $prevNode +Access to the immediate previous node: +\`\`\`javascript +const prevOutput = $prevNode.outputIndex; // Which output triggered this +const prevData = $prevNode.data; // Previous node's data +const prevName = $prevNode.name; // Previous node's name +\`\`\` + +## Helper Functions + +### Date/Time (Luxon) +\`\`\`javascript +// Current time +const now = DateTime.now(); +const iso = now.toISO(); + +// Parse dates +const date = DateTime.fromISO('2024-01-01'); +const formatted = date.toFormat('yyyy-MM-dd'); + +// Time math +const tomorrow = now.plus({ days: 1 }); +const hourAgo = now.minus({ hours: 1 }); +\`\`\` + +### JSON Queries (JMESPath) +\`\`\`javascript +// n8n uses $jmespath() - NOTE: parameter order is reversed from standard JMESPath! +const data = { users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }] }; +const names = $jmespath(data, 'users[*].name'); // ['John', 'Jane'] + +// โš ๏ธ IMPORTANT: Numeric literals in filters need BACKTICKS in n8n! +const adults = $jmespath(data, 'users[?age >= \`18\`]'); // โœ… CORRECT - backticks around 18 +const seniors = $jmespath(data, 'users[?age >= \`65\`]'); // โœ… CORRECT + +// โŒ WRONG - This will cause a syntax error! +// const adults = $jmespath(data, 'users[?age >= 18]'); // Missing backticks + +// More filter examples with proper backticks: +const expensive = $jmespath(items, '[?price > \`100\`]'); +const inStock = $jmespath(products, '[?quantity >= \`1\`]'); +const highPriority = $jmespath(tasks, '[?priority == \`1\`]'); + +// String comparisons don't need backticks +const activeUsers = $jmespath(data, 'users[?status == "active"]'); + +// Python uses underscore prefix +const pythonAdults = _jmespath(data, 'users[?age >= \`18\`]'); +\`\`\` + +โš ๏ธ **CRITICAL DIFFERENCES** from standard JMESPath: +1. **Parameter order is REVERSED**: + - **Expression**: \`{{$jmespath("query", data)}}\` + - **Code Node**: \`$jmespath(data, "query")\` +2. **Numeric literals in filters MUST use backticks**: \`[?age >= \`18\`]\` + - This is n8n-specific and differs from standard JMESPath documentation! + +### Available Functions and Libraries + +#### Built-in Node.js Modules (via require) +\`\`\`javascript +// โœ… These modules ARE available via require(): +const crypto = require('crypto'); // Cryptographic functions +const util = require('util'); // Utility functions +const querystring = require('querystring'); // URL query string utilities + +// Example: Generate secure random token +const crypto = require('crypto'); +const token = crypto.randomBytes(32).toString('hex'); +const uuid = crypto.randomUUID(); +\`\`\` + +**Note**: The editor may show errors for require() but it WORKS at runtime! + +#### Standalone Functions (Global Scope) +\`\`\`javascript +// โœ… Workflow static data - persists between executions +// IMPORTANT: These are standalone functions, NOT methods on $helpers! +const staticData = $getWorkflowStaticData('global'); // Global static data +const nodeData = $getWorkflowStaticData('node'); // Node-specific data + +// Example: Counter that persists +const staticData = $getWorkflowStaticData('global'); +staticData.counter = (staticData.counter || 0) + 1; + +// โŒ WRONG - This will cause "$helpers is not defined" error: +// const data = $helpers.getWorkflowStaticData('global'); + +// JMESPath queries - note the parameter order! +const result = $jmespath(data, 'users[*].name'); +\`\`\` + +#### $helpers Object (When Available) +\`\`\`javascript +// Some n8n versions provide $helpers with these methods: +// (Always test availability in your n8n instance) + +// HTTP requests +const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/data', + headers: { 'Authorization': 'Bearer token' } +}); + +// Binary data preparation +const binaryData = await $helpers.prepareBinaryData( + Buffer.from('content'), + 'file.txt', + 'text/plain' +); + +// Check if $helpers exists before using: +if (typeof $helpers !== 'undefined' && $helpers.httpRequest) { + // Use $helpers.httpRequest +} else { + throw new Error('HTTP requests not available in this n8n version'); +} +\`\`\` + +#### Important Notes: +- **$getWorkflowStaticData()** is ALWAYS a standalone function +- **require()** works for built-in Node.js modules despite editor warnings +- **$helpers** availability varies by n8n version - always check first +- Python uses underscore prefix: \`_getWorkflowStaticData()\`, \`_jmespath()\` +- Editor red underlines are often false positives - test at runtime! + +## Return Format + +Code nodes MUST return an array of objects with 'json' property: + +\`\`\`javascript +// โœ… CORRECT - Array of objects with json property +return [ + { json: { id: 1, name: 'Item 1' } }, + { json: { id: 2, name: 'Item 2' } } +]; + +// โœ… CORRECT - Single item (still wrapped in array) +return [{ json: { result: 'success' } }]; + +// โœ… CORRECT - With binary data +return [{ + json: { filename: 'report.pdf' }, + binary: { + data: { + data: base64String, + mimeType: 'application/pdf', + fileName: 'report.pdf' + } + } +}]; + +// โŒ WRONG - Not an array +return { json: { result: 'success' } }; + +// โŒ WRONG - No json property +return [{ result: 'success' }]; + +// โŒ WRONG - Not wrapped in object +return ['item1', 'item2']; +\`\`\` + +## Common Patterns + +### Data Transformation +\`\`\`javascript +// Transform all items +const transformedItems = []; +for (const item of items) { + transformedItems.push({ + json: { + ...item.json, + processed: true, + timestamp: DateTime.now().toISO(), + uppercaseName: item.json.name?.toUpperCase() + } + }); +} +return transformedItems; +\`\`\` + +### Filtering Items +\`\`\`javascript +// Filter items based on condition +return items + .filter(item => item.json.status === 'active') + .map(item => ({ json: item.json })); +\`\`\` + +### Aggregation +\`\`\`javascript +// Aggregate data from all items +const total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0); +const average = total / items.length; + +return [{ + json: { + total, + average, + count: items.length, + items: items.map(i => i.json) + } +}]; +\`\`\` + +### Error Handling +\`\`\`javascript +// Safe data access with defaults +const results = []; +for (const item of items) { + try { + const value = item.json?.nested?.field || 'default'; + results.push({ + json: { + processed: value, + status: 'success' + } + }); + } catch (error) { + results.push({ + json: { + error: error.message, + status: 'failed', + originalItem: item.json + } + }); + } +} +return results; +\`\`\` + +### Working with APIs +\`\`\`javascript +// Make HTTP request and process response +try { + const response = await $helpers.httpRequest({ + method: 'POST', + url: 'https://api.example.com/process', + body: { + data: items.map(item => item.json) + }, + headers: { + 'Content-Type': 'application/json' + } + }); + + return [{ json: response }]; +} catch (error) { + throw new Error(\`API request failed: \${error.message}\`); +} +\`\`\` + +### Async Operations +\`\`\`javascript +// Process items with async operations +const results = []; +for (const item of items) { + // Simulate async operation + await new Promise(resolve => setTimeout(resolve, 100)); + + results.push({ + json: { + ...item.json, + processedAt: new Date().toISOString() + } + }); +} +return results; +\`\`\` + +### Webhook Data Access (CRITICAL!) +\`\`\`javascript +// โš ๏ธ WEBHOOK DATA IS NESTED UNDER 'body' PROPERTY! +// This is a common source of errors in webhook-triggered workflows + +// โŒ WRONG - This will be undefined for webhook data: +const command = items[0].json.testCommand; + +// โœ… CORRECT - Webhook data is wrapped in 'body': +const command = items[0].json.body.testCommand; + +// Complete webhook data processing example: +const webhookData = items[0].json.body; // Get the actual webhook payload +const headers = items[0].json.headers; // HTTP headers are separate +const query = items[0].json.query; // Query parameters are separate + +// Process webhook payload +return [{ + json: { + command: webhookData.testCommand, + user: webhookData.user, + timestamp: DateTime.now().toISO(), + requestId: headers['x-request-id'], + source: query.source || 'unknown' + } +}]; + +// For other trigger nodes (non-webhook), data is directly under json: +// - Schedule Trigger: items[0].json contains timestamp +// - Database Trigger: items[0].json contains row data +// - File Trigger: items[0].json contains file info +\`\`\` + +## Python Code Examples + +### Basic Python Structure +\`\`\`python +import json +from datetime import datetime + +# Access items - Python uses underscore prefix for built-in variables +results = [] +for item in _input.all(): + # IMPORTANT: item.json is NOT a standard Python dict! + # Use to_py() to convert to a proper Python dict + processed_item = item.json.to_py() # Converts JsProxy to Python dict + processed_item['timestamp'] = datetime.now().isoformat() + results.append({'json': processed_item}) + +return results +\`\`\` + +### Python Data Processing +\`\`\`python +# Aggregate data - use _input.all() to get items +items = _input.all() +total = sum(item.json.get('amount', 0) for item in items) +average = total / len(items) if items else 0 + +# For safe dict operations, convert JsProxy to Python dict +safe_items = [] +for item in items: + # Convert JsProxy to dict to avoid KeyError with null values + safe_dict = item.json.to_py() + safe_items.append(safe_dict) + +# Return aggregated result +return [{ + 'json': { + 'total': total, + 'average': average, + 'count': len(items), + 'processed_at': datetime.now().isoformat(), + 'items': safe_items # Now these are proper Python dicts + } +}] +\`\`\` + +## Code Node as AI Tool + +Code nodes can be used as custom tools for AI agents: + +\`\`\`javascript +// Code node configured as AI tool +// Name: "Calculate Discount" +// Description: "Calculates discount based on quantity" + +const quantity = $json.quantity || 1; +const basePrice = $json.price || 0; + +let discount = 0; +if (quantity >= 100) discount = 0.20; +else if (quantity >= 50) discount = 0.15; +else if (quantity >= 20) discount = 0.10; +else if (quantity >= 10) discount = 0.05; + +const discountAmount = basePrice * quantity * discount; +const finalPrice = (basePrice * quantity) - discountAmount; + +return [{ + json: { + quantity, + basePrice, + discountPercentage: discount * 100, + discountAmount, + finalPrice, + savings: discountAmount + } +}]; +\`\`\` + +## Security Considerations + +### Available Security Features +\`\`\`javascript +// โœ… Crypto IS available despite editor warnings! +const crypto = require('crypto'); + +// Generate secure random values +const randomBytes = crypto.randomBytes(32); +const randomUUID = crypto.randomUUID(); + +// Create hashes +const hash = crypto.createHash('sha256') + .update('data to hash') + .digest('hex'); + +// HMAC for signatures +const hmac = crypto.createHmac('sha256', 'secret-key') + .update('data to sign') + .digest('hex'); +\`\`\` + +### Banned Operations +- No file system access (fs module) - except read-only for some paths +- No network requests except via $helpers.httpRequest +- No child process execution +- No external npm packages (only built-in Node.js modules) +- No eval() or Function() constructor + +### Safe Practices +\`\`\`javascript +// โœ… SAFE - Use crypto for secure operations +const crypto = require('crypto'); +const token = crypto.randomBytes(32).toString('hex'); + +// โœ… SAFE - Use built-in JSON parsing +const parsed = JSON.parse(jsonString); + +// โŒ UNSAFE - Never use eval +const parsed = eval('(' + jsonString + ')'); + +// โœ… SAFE - Validate input +if (typeof item.json.userId !== 'string') { + throw new Error('userId must be a string'); +} + +// โœ… SAFE - Sanitize for logs +const safeLog = String(userInput).substring(0, 100); + +// โœ… SAFE - Time-safe comparison for secrets +const expectedToken = 'abc123'; +const providedToken = item.json.token; +const tokensMatch = crypto.timingSafeEqual( + Buffer.from(expectedToken), + Buffer.from(providedToken || '') +); +\`\`\` + +## Debugging Tips + +### Console Output +\`\`\`javascript +// Console.log appears in n8n execution logs +console.log('Processing item:', item.json.id); +console.error('Error details:', error); + +// Return debug info in development +return [{ + json: { + result: processedData, + debug: { + itemCount: items.length, + executionId: $execution.id, + timestamp: new Date().toISOString() + } + } +}]; +\`\`\` + +### Error Messages +\`\`\`javascript +// Provide helpful error context +if (!item.json.requiredField) { + throw new Error(\`Missing required field 'requiredField' in item \${items.indexOf(item)}\`); +} + +// Include original data in errors +try { + // processing... +} catch (error) { + throw new Error(\`Failed to process item \${item.json.id}: \${error.message}\`); +} +\`\`\` + +## Performance Best Practices + +1. **Avoid nested loops** when possible +2. **Use array methods** (map, filter, reduce) for clarity +3. **Limit HTTP requests** - batch when possible +4. **Return early** for error conditions +5. **Keep state minimal** - Code nodes are stateless between executions + +## Common Mistakes to Avoid + +1. **Forgetting to return an array** +2. **Not wrapping in json property** +3. **Modifying items array directly** +4. **Using undefined variables** +5. **Infinite loops with while statements** +6. **Not handling missing data gracefully** +7. **Forgetting await for async operations**`, + parameters: { + topic: { type: 'string', description: 'Specific Code node topic (optional)', required: false } + }, + returns: 'Comprehensive Code node documentation and examples', + examples: [ + 'tools_documentation({topic: "code_node_guide"}) - Full guide', + 'tools_documentation({topic: "code_node_guide", depth: "full"}) - Complete reference' + ], + useCases: [ + 'Learning Code node capabilities', + 'Understanding built-in variables', + 'Finding the right helper function', + 'Debugging Code node issues', + 'Building custom AI tools' + ], + performance: 'Instant - returns static documentation', + bestPractices: [ + 'Read before writing Code nodes', + 'Reference for variable names', + 'Copy examples as starting points', + 'Check security considerations' + ], + pitfalls: [ + 'Not all Node.js features available', + 'Python has limited libraries', + 'State not preserved between executions' + ], + relatedTools: ['get_node_essentials', 'validate_node_operation', 'get_node_for_task'] + } } }; @@ -584,6 +1221,7 @@ Welcome! Here's how to efficiently work with n8n nodes: ## Get Help - tools_documentation({topic: "search_nodes"}) - Get help for specific tool +- tools_documentation({topic: "code_node_guide"}) - Essential Code node reference - tools_documentation({topic: "overview", depth: "full"}) - See complete guide - list_tasks() - See available task templates @@ -652,6 +1290,23 @@ validate_workflow(workflow) n8n_create_workflow(workflow) \`\`\` +### Working with Code Nodes +The Code node is essential for custom logic. Always reference the guide: +\`\`\`javascript +// Get comprehensive Code node documentation +tools_documentation({topic: "code_node_guide"}) + +// Common Code node pattern +get_node_essentials("n8n-nodes-base.code") +// Returns minimal config with JavaScript/Python examples + +// Validate Code node configuration +validate_node_operation("n8n-nodes-base.code", { + language: "javaScript", + jsCode: "return items.map(item => ({json: {...item.json, processed: true}}))" +}) +\`\`\` + ### Node-Level Properties Reference โš ๏ธ **CRITICAL**: These properties go at the NODE level, not inside parameters! diff --git a/src/services/config-validator.ts b/src/services/config-validator.ts index 12278ed..57960a4 100644 --- a/src/services/config-validator.ts +++ b/src/services/config-validator.ts @@ -391,12 +391,18 @@ export class ConfigValidator { * Check for common configuration issues */ private static checkCommonIssues( - _nodeType: string, + nodeType: string, config: Record, properties: any[], warnings: ValidationWarning[], suggestions: string[] ): void { + // Skip visibility checks for Code nodes as they have simple property structure + if (nodeType === 'nodes-base.code') { + // Code nodes don't have complex displayOptions, so skip visibility warnings + return; + } + // Check for properties that won't be used const visibleProps = properties.filter(p => this.isPropertyVisible(p, config)); const configuredKeys = Object.keys(config); @@ -562,20 +568,133 @@ export class ConfigValidator { warnings.push({ type: 'missing_common', message: 'No return statement found', - suggestion: 'Code node should return data for the next node. Add: return items (Python) or return items; (JavaScript)' + suggestion: 'Code node must return data. Example: return [{json: {result: "success"}}]' }); } - // Check for common n8n patterns + // Check return format for JavaScript + if (language === 'javascript' && hasReturn) { + // Check for common incorrect return patterns + if (/return\s+items\s*;/.test(code) && !code.includes('.map') && !code.includes('json:')) { + warnings.push({ + type: 'best_practice', + message: 'Returning items directly - ensure each item has {json: ...} structure', + suggestion: 'If modifying items, use: return items.map(item => ({json: {...item.json, newField: "value"}}))' + }); + } + + // Check for return without array + if (/return\s+{[^}]+}\s*;/.test(code) && !code.includes('[') && !code.includes(']')) { + warnings.push({ + type: 'invalid_value', + message: 'Return value must be an array', + suggestion: 'Wrap your return object in an array: return [{json: {your: "data"}}]' + }); + } + + // Check for direct data return without json wrapper + if (/return\s+\[['"`]/.test(code) || /return\s+\[\d/.test(code)) { + warnings.push({ + type: 'invalid_value', + message: 'Items must be objects with json property', + suggestion: 'Use format: return [{json: {value: "data"}}] not return ["data"]' + }); + } + } + + // Check return format for Python + if (language === 'python' && hasReturn) { + // Check for common incorrect patterns + if (/return\s+items\s*$/.test(code) && !code.includes('json') && !code.includes('dict')) { + warnings.push({ + type: 'best_practice', + message: 'Returning items directly - ensure each item is a dict with "json" key', + suggestion: 'Use: return [{"json": item.json} for item in items]' + }); + } + + // Check for dict return without list + if (/return\s+{['"]/.test(code) && !code.includes('[') && !code.includes(']')) { + warnings.push({ + type: 'invalid_value', + message: 'Return value must be a list', + suggestion: 'Wrap your return dict in a list: return [{"json": {"your": "data"}}]' + }); + } + } + + // Check for common n8n variables and patterns if (language === 'javascript') { - if (!code.includes('items') && !code.includes('$input')) { + // Check if accessing items/input + if (!code.includes('items') && !code.includes('$input') && !code.includes('$json')) { warnings.push({ type: 'missing_common', - message: 'Code doesn\'t reference input items', - suggestion: 'Access input data with: items or $input.all()' + message: 'Code doesn\'t reference input data', + suggestion: 'Access input with: items, $input.all(), or $json (in single-item mode)' + }); + } + + // Check for common mistakes with $json + if (code.includes('$json') && !code.includes('mode')) { + warnings.push({ + type: 'best_practice', + message: '$json only works in "Run Once for Each Item" mode', + suggestion: 'For all items mode, use: items[0].json or loop through items' + }); + } + + // Check for undefined variable usage + const commonVars = ['$node', '$workflow', '$execution', '$prevNode', 'DateTime', 'jmespath']; + const usedVars = commonVars.filter(v => code.includes(v)); + + // Check for incorrect $helpers usage patterns + if (code.includes('$helpers.getWorkflowStaticData')) { + warnings.push({ + type: 'invalid_value', + message: '$helpers.getWorkflowStaticData() is incorrect - causes "$helpers is not defined" error', + suggestion: 'Use $getWorkflowStaticData() as a standalone function (no $helpers prefix)' + }); + } + + // Check for $helpers usage without checking availability + if (code.includes('$helpers') && !code.includes('typeof $helpers')) { + warnings.push({ + type: 'best_practice', + message: '$helpers availability varies by n8n version', + suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }' + }); + } + + // Check for async without await + if (code.includes('async') || code.includes('.then(')) { + if (!code.includes('await')) { + warnings.push({ + type: 'best_practice', + message: 'Using async operations without await', + suggestion: 'Use await for async operations: await $helpers.httpRequest(...)' + }); + } + } + + // Check for crypto usage without require + if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) && !code.includes('require')) { + warnings.push({ + type: 'invalid_value', + message: 'Using crypto without require statement', + suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)' + }); + } + + // Check for console.log (informational) + if (code.includes('console.log')) { + warnings.push({ + type: 'best_practice', + message: 'console.log output appears in n8n execution logs', + suggestion: 'Remove console.log statements in production or use them sparingly' }); } } else if (language === 'python') { + // Python-specific checks if (!code.includes('items') && !code.includes('_input')) { warnings.push({ type: 'missing_common', @@ -583,6 +702,44 @@ export class ConfigValidator { suggestion: 'Access input data with: items variable' }); } + + // Check for print statements + if (code.includes('print(')) { + warnings.push({ + type: 'best_practice', + message: 'print() output appears in n8n execution logs', + suggestion: 'Remove print statements in production or use them sparingly' + }); + } + + // Check for common Python mistakes + if (code.includes('import requests') || code.includes('import pandas')) { + warnings.push({ + type: 'invalid_value', + message: 'External libraries not available in Code node', + suggestion: 'Only Python standard library is available. For HTTP requests, use JavaScript with $helpers.httpRequest' + }); + } + } + + // Check for infinite loops + if (/while\s*\(\s*true\s*\)|while\s+True:/.test(code)) { + warnings.push({ + type: 'security', + message: 'Infinite loop detected', + suggestion: 'Add a break condition or use a for loop with limits' + }); + } + + // Check for error handling + if (!code.includes('try') && !code.includes('catch') && !code.includes('except')) { + if (code.length > 200) { // Only suggest for non-trivial code + warnings.push({ + type: 'best_practice', + message: 'No error handling found', + suggestion: 'Consider adding try/catch (JavaScript) or try/except (Python) for robust error handling' + }); + } } } } \ No newline at end of file diff --git a/src/services/enhanced-config-validator.ts b/src/services/enhanced-config-validator.ts index 41fba57..51bed40 100644 --- a/src/services/enhanced-config-validator.ts +++ b/src/services/enhanced-config-validator.ts @@ -213,7 +213,7 @@ export class EnhancedConfigValidator extends ConfigValidator { break; case 'nodes-base.code': - // Code node uses base validation which includes syntax checks + NodeSpecificValidators.validateCode(context); break; case 'nodes-base.openAi': diff --git a/src/services/example-generator.ts b/src/services/example-generator.ts index 6d11430..6d2be7e 100644 --- a/src/services/example-generator.ts +++ b/src/services/example-generator.ts @@ -75,50 +75,330 @@ export class ExampleGenerator { } }, + // Webhook data processing example + 'nodes-base.code.webhookProcessing': { + minimal: { + language: 'javaScript', + jsCode: `// โš ๏ธ CRITICAL: Webhook data is nested under 'body' property! +// This Code node should be connected after a Webhook node + +// โŒ WRONG - This will be undefined: +// const command = items[0].json.testCommand; + +// โœ… CORRECT - Access webhook data through body: +const webhookData = items[0].json.body; +const headers = items[0].json.headers; +const query = items[0].json.query; + +// Process webhook payload +return [{ + json: { + // Extract data from webhook body + command: webhookData.testCommand, + userId: webhookData.userId, + data: webhookData.data, + + // Add metadata + timestamp: DateTime.now().toISO(), + requestId: headers['x-request-id'] || crypto.randomUUID(), + source: query.source || 'webhook', + + // Original webhook info + httpMethod: items[0].json.httpMethod, + webhookPath: items[0].json.webhookPath + } +}];` + } + }, + // Code - Custom logic 'nodes-base.code': { minimal: { language: 'javaScript', - jsCode: 'return items;' + jsCode: 'return [{json: {result: "success"}}];' }, common: { language: 'javaScript', - jsCode: `// Access input items + jsCode: `// Process each item and add timestamp +return items.map(item => ({ + json: { + ...item.json, + processed: true, + timestamp: DateTime.now().toISO() + } +}));`, + onError: 'continueRegularOutput' + }, + advanced: { + language: 'javaScript', + jsCode: `// Advanced data processing with proper helper checks +const crypto = require('crypto'); const results = []; for (const item of items) { - // Process each item - results.push({ - json: { - ...item.json, - processed: true, - timestamp: new Date().toISOString() + try { + // Validate required fields + if (!item.json.email || !item.json.name) { + throw new Error('Missing required fields: email or name'); } - }); + + // Generate secure API key + const apiKey = crypto.randomBytes(16).toString('hex'); + + // Check if $helpers is available before using + let response; + if (typeof $helpers !== 'undefined' && $helpers.httpRequest) { + response = await $helpers.httpRequest({ + method: 'POST', + url: 'https://api.example.com/process', + body: { + email: item.json.email, + name: item.json.name, + apiKey + }, + headers: { + 'Content-Type': 'application/json' + } + }); + } else { + // Fallback if $helpers not available + response = { message: 'HTTP requests not available in this n8n version' }; + } + + // Add to results with response data + results.push({ + json: { + ...item.json, + apiResponse: response, + processedAt: DateTime.now().toISO(), + status: 'success' + } + }); + + } catch (error) { + // Include failed items with error info + results.push({ + json: { + ...item.json, + error: error.message, + status: 'failed', + processedAt: DateTime.now().toISO() + } + }); + } } -return results;` - }, - advanced: { - language: 'python', - pythonCode: `import json -from datetime import datetime +return results;`, + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2 + } + }, + + // Additional Code node examples + 'nodes-base.code.dataTransform': { + minimal: { + language: 'javaScript', + jsCode: `// Transform CSV-like data to JSON +return items.map(item => { + const lines = item.json.data.split('\\n'); + const headers = lines[0].split(','); + const rows = lines.slice(1).map(line => { + const values = line.split(','); + return headers.reduce((obj, header, i) => { + obj[header.trim()] = values[i]?.trim() || ''; + return obj; + }, {}); + }); + + return {json: {rows, count: rows.length}}; +});` + } + }, + + 'nodes-base.code.aggregation': { + minimal: { + language: 'javaScript', + jsCode: `// Aggregate data from all items +const totals = items.reduce((acc, item) => { + acc.count++; + acc.sum += item.json.amount || 0; + acc.categories[item.json.category] = (acc.categories[item.json.category] || 0) + 1; + return acc; +}, {count: 0, sum: 0, categories: {}}); + +return [{ + json: { + totalItems: totals.count, + totalAmount: totals.sum, + averageAmount: totals.sum / totals.count, + categoryCounts: totals.categories, + processedAt: DateTime.now().toISO() + } +}];` + } + }, + + 'nodes-base.code.filtering': { + minimal: { + language: 'javaScript', + jsCode: `// Filter items based on conditions +return items + .filter(item => { + const amount = item.json.amount || 0; + const status = item.json.status || ''; + return amount > 100 && status === 'active'; + }) + .map(item => ({json: item.json}));` + } + }, + + 'nodes-base.code.jmespathFiltering': { + minimal: { + language: 'javaScript', + jsCode: `// JMESPath filtering - IMPORTANT: Use backticks for numeric literals! +const allItems = items.map(item => item.json); + +// โœ… CORRECT - Filter with numeric literals using backticks +const expensiveItems = $jmespath(allItems, '[?price >= \`100\`]'); +const lowStock = $jmespath(allItems, '[?inventory < \`10\`]'); +const highPriority = $jmespath(allItems, '[?priority == \`1\`]'); + +// Combine multiple conditions +const urgentExpensive = $jmespath(allItems, '[?price >= \`100\` && priority == \`1\`]'); + +// String comparisons don't need backticks +const activeItems = $jmespath(allItems, '[?status == "active"]'); + +// Return filtered results +return expensiveItems.map(item => ({json: item}));` + } + }, + + 'nodes-base.code.pythonExample': { + minimal: { + language: 'python', + pythonCode: `# Python data processing - use underscore prefix for built-in variables +import json +from datetime import datetime +import re -# Access input items results = [] -for item in items: - # Process each item - processed_item = item.json.copy() - processed_item['processed'] = True - processed_item['timestamp'] = datetime.now().isoformat() +# Use _input.all() to get items in Python +for item in _input.all(): + # Convert JsProxy to Python dict to avoid issues with null values + item_data = item.json.to_py() - results.append({'json': processed_item}) + # Clean email addresses + email = item_data.get('email', '') + if email and re.match(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$', email): + cleaned_data = { + 'email': email.lower(), + 'name': item_data.get('name', '').title(), + 'validated': True, + 'timestamp': datetime.now().isoformat() + } + else: + # Spread operator doesn't work with JsProxy, use dict() + cleaned_data = dict(item_data) + cleaned_data['validated'] = False + cleaned_data['error'] = 'Invalid email format' + + results.append({'json': cleaned_data}) return results` } }, + 'nodes-base.code.aiTool': { + minimal: { + language: 'javaScript', + mode: 'runOnceForEachItem', + jsCode: `// Code node as AI tool - calculate discount +const quantity = $json.quantity || 1; +const price = $json.price || 0; + +let discountRate = 0; +if (quantity >= 100) discountRate = 0.20; +else if (quantity >= 50) discountRate = 0.15; +else if (quantity >= 20) discountRate = 0.10; +else if (quantity >= 10) discountRate = 0.05; + +const subtotal = price * quantity; +const discount = subtotal * discountRate; +const total = subtotal - discount; + +return [{ + json: { + quantity, + price, + subtotal, + discountRate: discountRate * 100, + discountAmount: discount, + total, + savings: discount + } +}];` + } + }, + + 'nodes-base.code.crypto': { + minimal: { + language: 'javaScript', + jsCode: `// Using crypto in Code nodes - it IS available! +const crypto = require('crypto'); + +// Generate secure tokens +const token = crypto.randomBytes(32).toString('hex'); +const uuid = crypto.randomUUID(); + +// Create hashes +const hash = crypto.createHash('sha256') + .update(items[0].json.data || 'test') + .digest('hex'); + +return [{ + json: { + token, + uuid, + hash, + timestamp: DateTime.now().toISO() + } +}];` + } + }, + + 'nodes-base.code.staticData': { + minimal: { + language: 'javaScript', + jsCode: `// Using workflow static data correctly +// IMPORTANT: $getWorkflowStaticData is a standalone function! +const staticData = $getWorkflowStaticData('global'); + +// Initialize counter if not exists +if (!staticData.processCount) { + staticData.processCount = 0; + staticData.firstRun = DateTime.now().toISO(); +} + +// Update counter +staticData.processCount++; +staticData.lastRun = DateTime.now().toISO(); + +// Process items +const results = items.map(item => ({ + json: { + ...item.json, + runNumber: staticData.processCount, + processed: true + } +})); + +return results;` + } + }, + // Set - Data manipulation 'nodes-base.set': { minimal: { diff --git a/src/services/node-specific-validators.ts b/src/services/node-specific-validators.ts index 377fb82..53da79f 100644 --- a/src/services/node-specific-validators.ts +++ b/src/services/node-specific-validators.ts @@ -1056,4 +1056,501 @@ export class NodeSpecificValidators { suggestions.push('Consider adding webhook validation (HMAC signature verification)'); suggestions.push('Implement rate limiting for public webhooks'); } + + /** + * Validate Code node configuration with n8n-specific patterns + */ + static validateCode(context: NodeValidationContext): void { + const { config, errors, warnings, suggestions, autofix } = context; + const language = config.language || 'javaScript'; + const codeField = language === 'python' ? 'pythonCode' : 'jsCode'; + const code = config[codeField] || ''; + + // Check for empty code + if (!code || code.trim() === '') { + errors.push({ + type: 'missing_required', + property: codeField, + message: 'Code cannot be empty', + fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]' + }); + return; + } + + // Language-specific validation + if (language === 'javaScript') { + this.validateJavaScriptCode(code, errors, warnings, suggestions); + } else if (language === 'python') { + this.validatePythonCode(code, errors, warnings, suggestions); + } + + // Check return statement and format + this.validateReturnStatement(code, language, errors, warnings, suggestions); + + // Check n8n variable usage + this.validateN8nVariables(code, language, warnings, suggestions, errors); + + // Security and best practices + this.validateCodeSecurity(code, language, warnings); + + // Error handling recommendations + if (!config.onError && code.length > 100) { + warnings.push({ + type: 'best_practice', + property: 'errorHandling', + message: 'Code nodes can throw errors - consider error handling', + suggestion: 'Add onError: "continueRegularOutput" to handle errors gracefully' + }); + autofix.onError = 'continueRegularOutput'; + } + + // Mode-specific suggestions + if (config.mode === 'runOnceForEachItem' && code.includes('items')) { + warnings.push({ + type: 'best_practice', + message: 'In "Run Once for Each Item" mode, use $json instead of items array', + suggestion: 'Access current item data with $json.fieldName' + }); + } + + if (!config.mode && code.includes('$json')) { + warnings.push({ + type: 'best_practice', + message: '$json only works in "Run Once for Each Item" mode', + suggestion: 'Either set mode: "runOnceForEachItem" or use items[0].json' + }); + } + } + + private static validateJavaScriptCode( + code: string, + errors: ValidationError[], + warnings: ValidationWarning[], + suggestions: string[] + ): void { + // Check for syntax patterns that might fail + const syntaxPatterns = [ + { pattern: /const\s+const/, message: 'Duplicate const declaration' }, + { pattern: /let\s+let/, message: 'Duplicate let declaration' }, + { pattern: /\)\s*\)\s*{/, message: 'Extra closing parenthesis before {' }, + { pattern: /}\s*}$/, message: 'Extra closing brace at end' } + ]; + + syntaxPatterns.forEach(({ pattern, message }) => { + if (pattern.test(code)) { + errors.push({ + type: 'invalid_value', + property: 'jsCode', + message: `Syntax error: ${message}`, + fix: 'Check your JavaScript syntax' + }); + } + }); + + // Common async/await issues + // Check for await inside a non-async function (but top-level await is fine) + const functionWithAwait = /function\s+\w*\s*\([^)]*\)\s*{[^}]*await/; + const arrowWithAwait = /\([^)]*\)\s*=>\s*{[^}]*await/; + + if ((functionWithAwait.test(code) || arrowWithAwait.test(code)) && !code.includes('async')) { + warnings.push({ + type: 'best_practice', + message: 'Using await inside a non-async function', + suggestion: 'Add async keyword to the function, or use top-level await (Code nodes support it)' + }); + } + + // Check for common helper usage + if (code.includes('$helpers.httpRequest')) { + suggestions.push('$helpers.httpRequest is async - use: const response = await $helpers.httpRequest(...)'); + } + + if (code.includes('DateTime') && !code.includes('DateTime.')) { + warnings.push({ + type: 'best_practice', + message: 'DateTime is from Luxon library', + suggestion: 'Use DateTime.now() or DateTime.fromISO() for date operations' + }); + } + } + + private static validatePythonCode( + code: string, + errors: ValidationError[], + warnings: ValidationWarning[], + suggestions: string[] + ): void { + // Python-specific validation + const lines = code.split('\n'); + + // Check for tab/space mixing (already done in base validator) + + // Check for common Python mistakes in n8n context + if (code.includes('__name__') && code.includes('__main__')) { + warnings.push({ + type: 'inefficient', + message: 'if __name__ == "__main__" is not needed in Code nodes', + suggestion: 'Code node Python runs directly - remove the main check' + }); + } + + // Check for unavailable imports + const unavailableImports = [ + { module: 'requests', suggestion: 'Use JavaScript Code node with $helpers.httpRequest for HTTP requests' }, + { module: 'pandas', suggestion: 'Use built-in list/dict operations or JavaScript for data manipulation' }, + { module: 'numpy', suggestion: 'Use standard Python math operations' }, + { module: 'pip', suggestion: 'External packages cannot be installed in Code nodes' } + ]; + + unavailableImports.forEach(({ module, suggestion }) => { + if (code.includes(`import ${module}`) || code.includes(`from ${module}`)) { + errors.push({ + type: 'invalid_value', + property: 'pythonCode', + message: `Module '${module}' is not available in Code nodes`, + fix: suggestion + }); + } + }); + + // Check indentation after colons + lines.forEach((line, i) => { + if (line.trim().endsWith(':') && i < lines.length - 1) { + const nextLine = lines[i + 1]; + if (nextLine.trim() && !nextLine.startsWith(' ') && !nextLine.startsWith('\t')) { + errors.push({ + type: 'invalid_value', + property: 'pythonCode', + message: `Missing indentation after line ${i + 1}`, + fix: 'Indent the line after the colon' + }); + } + } + }); + } + + private static validateReturnStatement( + code: string, + language: string, + errors: ValidationError[], + warnings: ValidationWarning[], + suggestions: string[] + ): void { + const hasReturn = /return\s+/.test(code); + + if (!hasReturn) { + errors.push({ + type: 'missing_required', + property: language === 'python' ? 'pythonCode' : 'jsCode', + message: 'Code must return data for the next node', + fix: language === 'python' + ? 'Add: return [{"json": {"result": "success"}}]' + : 'Add: return [{json: {result: "success"}}]' + }); + return; + } + + // JavaScript return format validation + if (language === 'javaScript') { + // Check for object return without array + if (/return\s+{(?!.*\[).*}\s*;?$/s.test(code) && !code.includes('json:')) { + errors.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Return value must be an array of objects', + fix: 'Wrap in array: return [{json: yourObject}]' + }); + } + + // Check for primitive return + if (/return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) { + errors.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Cannot return primitive values directly', + fix: 'Return array of objects: return [{json: {value: yourData}}]' + }); + } + + // Check for array of non-objects + if (/return\s+\[[\s\n]*['"`\d]/.test(code)) { + errors.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Array items must be objects with json property', + fix: 'Use: return [{json: {value: "data"}}] not return ["data"]' + }); + } + + // Suggest proper return format for items + if (/return\s+items\s*;?$/.test(code) && !code.includes('map')) { + suggestions.push( + 'Returning items directly is fine if they already have {json: ...} structure. ' + + 'To modify: return items.map(item => ({json: {...item.json, newField: "value"}}))' + ); + } + } + + // Python return format validation + if (language === 'python') { + // Check for dict return without list + if (/return\s+{(?!.*\[).*}$/s.test(code)) { + errors.push({ + type: 'invalid_value', + property: 'pythonCode', + message: 'Return value must be a list of dicts', + fix: 'Wrap in list: return [{"json": your_dict}]' + }); + } + + // Check for primitive return + if (/return\s+(True|False|None|\d+|['"`])/m.test(code)) { + errors.push({ + type: 'invalid_value', + property: 'pythonCode', + message: 'Cannot return primitive values directly', + fix: 'Return list of dicts: return [{"json": {"value": your_data}}]' + }); + } + } + } + + private static validateN8nVariables( + code: string, + language: string, + warnings: ValidationWarning[], + suggestions: string[], + errors: ValidationError[] + ): void { + // Check if code accesses input data + const inputPatterns = language === 'javaScript' + ? ['items', '$input', '$json', '$node', '$prevNode'] + : ['items', '_input']; + + const usesInput = inputPatterns.some(pattern => code.includes(pattern)); + + if (!usesInput && code.length > 50) { + warnings.push({ + type: 'missing_common', + message: 'Code doesn\'t reference input data', + suggestion: language === 'javaScript' + ? 'Access input with: items, $input.all(), or $json (single-item mode)' + : 'Access input with: items variable' + }); + } + + // Check for expression syntax in Code nodes + if (code.includes('{{') && code.includes('}}')) { + errors.push({ + type: 'invalid_value', + property: language === 'python' ? 'pythonCode' : 'jsCode', + message: 'Expression syntax {{...}} is not valid in Code nodes', + fix: 'Use regular JavaScript/Python syntax without double curly braces' + }); + } + + // Check for wrong $node syntax + if (code.includes('$node[')) { + warnings.push({ + type: 'invalid_value', + property: language === 'python' ? 'pythonCode' : 'jsCode', + message: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes', + suggestion: 'Replace $node[\'NodeName\'] with $(\'NodeName\')' + }); + } + + // Check for expression-only functions + const expressionOnlyFunctions = ['$now()', '$today()', '$tomorrow()', '.unique()', '.pluck(', '.keys()', '.hash(']; + expressionOnlyFunctions.forEach(func => { + if (code.includes(func)) { + warnings.push({ + type: 'invalid_value', + property: language === 'python' ? 'pythonCode' : 'jsCode', + message: `${func} is an expression-only function not available in Code nodes`, + suggestion: 'See Code node documentation for alternatives' + }); + } + }); + + // Check for common variable mistakes + if (language === 'javaScript') { + // Using $ without proper variable + if (/\$(?![a-zA-Z])/.test(code) && !code.includes('${')) { + warnings.push({ + type: 'best_practice', + message: 'Invalid $ usage detected', + suggestion: 'n8n variables start with $: $json, $input, $node, $workflow, $execution' + }); + } + + // Check for helpers usage + if (code.includes('helpers.') && !code.includes('$helpers')) { + warnings.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Use $helpers not helpers', + suggestion: 'Change helpers. to $helpers.' + }); + } + + // Check for $helpers usage without availability check + if (code.includes('$helpers') && !code.includes('typeof $helpers')) { + warnings.push({ + type: 'best_practice', + message: '$helpers availability varies by n8n version', + suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }' + }); + } + + // Suggest available helpers + if (code.includes('$helpers')) { + suggestions.push( + 'Common $helpers methods: httpRequest(), prepareBinaryData(). Note: getWorkflowStaticData is a standalone function - use $getWorkflowStaticData() instead' + ); + } + + // Check for incorrect getWorkflowStaticData usage + if (code.includes('$helpers.getWorkflowStaticData')) { + errors.push({ + type: 'invalid_value', + property: 'jsCode', + message: '$helpers.getWorkflowStaticData() will cause "$helpers is not defined" error', + fix: 'Use $getWorkflowStaticData("global") or $getWorkflowStaticData("node") directly' + }); + } + + // Check for wrong JMESPath parameter order + if (code.includes('$jmespath(') && /\$jmespath\s*\(\s*['"`]/.test(code)) { + warnings.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)', + suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)' + }); + } + + // Check for webhook data access patterns + if (code.includes('items[0].json') && !code.includes('.json.body')) { + // Check if previous node reference suggests webhook + if (code.includes('Webhook') || code.includes('webhook') || + code.includes('$("Webhook")') || code.includes("$('Webhook')")) { + warnings.push({ + type: 'invalid_value', + property: 'jsCode', + message: 'Webhook data is nested under .body property', + suggestion: 'Use items[0].json.body.fieldName instead of items[0].json.fieldName for webhook data' + }); + } + // Also check for common webhook field names that suggest webhook data + else if (/items\[0\]\.json\.(payload|data|command|action|event|message)\b/.test(code)) { + warnings.push({ + type: 'best_practice', + message: 'If processing webhook data, remember it\'s nested under .body', + suggestion: 'Webhook payloads are at items[0].json.body, not items[0].json' + }); + } + } + } + + // Check for JMESPath filters with unquoted numeric literals (both JS and Python) + const jmespathFunction = language === 'javaScript' ? '$jmespath' : '_jmespath'; + if (code.includes(jmespathFunction + '(')) { + // Look for filter expressions with comparison operators and numbers + const filterPattern = /\[?\?[^[\]]*(?:>=?|<=?|==|!=)\s*(\d+(?:\.\d+)?)\s*\]/g; + let match; + + while ((match = filterPattern.exec(code)) !== null) { + const number = match[1]; + // Check if the number is NOT wrapped in backticks + const beforeNumber = code.substring(match.index, match.index + match[0].indexOf(number)); + const afterNumber = code.substring(match.index + match[0].indexOf(number) + number.length); + + if (!beforeNumber.includes('`') || !afterNumber.startsWith('`')) { + errors.push({ + type: 'invalid_value', + property: language === 'python' ? 'pythonCode' : 'jsCode', + message: `JMESPath numeric literal ${number} must be wrapped in backticks`, + fix: `Change [?field >= ${number}] to [?field >= \`${number}\`]` + }); + } + } + + // Also provide a general suggestion if JMESPath is used + suggestions.push( + 'JMESPath in n8n requires backticks around numeric literals in filters: [?age >= `18`]' + ); + } + } + + private static validateCodeSecurity( + code: string, + language: string, + warnings: ValidationWarning[] + ): void { + // Security checks + const dangerousPatterns = [ + { pattern: /eval\s*\(/, message: 'Avoid eval() - it\'s a security risk' }, + { pattern: /Function\s*\(/, message: 'Avoid Function constructor - use regular functions' }, + { pattern: language === 'python' ? /exec\s*\(/ : /exec\s*\(/, message: 'Avoid exec() - it\'s a security risk' }, + { pattern: /process\.env/, message: 'Limited environment access in Code nodes' }, + { pattern: /import\s+\*/, message: 'Avoid import * - be specific about imports' } + ]; + + dangerousPatterns.forEach(({ pattern, message }) => { + if (pattern.test(code)) { + warnings.push({ + type: 'security', + message, + suggestion: 'Use safer alternatives or built-in functions' + }); + } + }); + + // Special handling for require() - it's allowed for built-in modules + if (code.includes('require(')) { + // Check if it's requiring a built-in module + const builtinModules = ['crypto', 'util', 'querystring', 'url', 'buffer']; + const requirePattern = /require\s*\(\s*['"`](\w+)['"`]\s*\)/g; + let match; + + while ((match = requirePattern.exec(code)) !== null) { + const moduleName = match[1]; + if (!builtinModules.includes(moduleName)) { + warnings.push({ + type: 'security', + message: `Cannot require('${moduleName}') - only built-in Node.js modules are available`, + suggestion: `Available modules: ${builtinModules.join(', ')}` + }); + } + } + + // If require is used without quotes, it might be dynamic + if (/require\s*\([^'"`]/.test(code)) { + warnings.push({ + type: 'security', + message: 'Dynamic require() not supported', + suggestion: 'Use static require with string literals: require("crypto")' + }); + } + } + + // Check for crypto usage without require + if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) && + !code.includes('require') && language === 'javaScript') { + warnings.push({ + type: 'invalid_value', + message: 'Using crypto without require statement', + suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)' + }); + } + + // File system access warning + if (/\b(fs|path|child_process)\b/.test(code)) { + warnings.push({ + type: 'security', + message: 'File system and process access not available in Code nodes', + suggestion: 'Use other n8n nodes for file operations (e.g., Read/Write Files node)' + }); + } + } } \ No newline at end of file diff --git a/src/services/task-templates.ts b/src/services/task-templates.ts index 77eed0b..0c99872 100644 --- a/src/services/task-templates.ts +++ b/src/services/task-templates.ts @@ -227,6 +227,94 @@ export class TaskTemplates { ] }, + 'process_webhook_data': { + task: 'process_webhook_data', + description: 'Process incoming webhook data with Code node (shows correct data access)', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// โš ๏ธ CRITICAL: Webhook data is nested under 'body' property! +// Connect this Code node after a Webhook node + +// Access webhook payload data - it's under .body, not directly under .json +const webhookData = items[0].json.body; // โœ… CORRECT +const headers = items[0].json.headers; // HTTP headers +const query = items[0].json.query; // Query parameters + +// Common mistake to avoid: +// const command = items[0].json.testCommand; // โŒ WRONG - will be undefined! +// const command = items[0].json.body.testCommand; // โœ… CORRECT + +// Process the webhook data +try { + // Validate required fields + if (!webhookData.command) { + throw new Error('Missing required field: command'); + } + + // Process based on command + let result = {}; + switch (webhookData.command) { + case 'process': + result = { + status: 'processed', + data: webhookData.data, + processedAt: DateTime.now().toISO() + }; + break; + + case 'validate': + result = { + status: 'validated', + isValid: true, + validatedFields: Object.keys(webhookData.data || {}) + }; + break; + + default: + result = { + status: 'unknown_command', + command: webhookData.command + }; + } + + // Return processed data + return [{ + json: { + ...result, + requestId: headers['x-request-id'] || crypto.randomUUID(), + source: query.source || 'webhook', + originalCommand: webhookData.command, + metadata: { + httpMethod: items[0].json.httpMethod, + webhookPath: items[0].json.webhookPath, + timestamp: DateTime.now().toISO() + } + } + }]; + +} catch (error) { + // Return error response + return [{ + json: { + status: 'error', + error: error.message, + timestamp: DateTime.now().toISO() + } + }]; +}`, + onError: 'continueRegularOutput' + }, + userMustProvide: [], + notes: [ + 'โš ๏ธ WEBHOOK DATA IS AT items[0].json.body, NOT items[0].json', + 'This is the most common webhook processing mistake', + 'Headers are at items[0].json.headers', + 'Query parameters are at items[0].json.query', + 'Connect this Code node directly after a Webhook node' + ] + }, + // Database Tasks 'query_postgres': { task: 'query_postgres', @@ -873,6 +961,479 @@ return results;`, 'Consider implementing exponential backoff in Code node', 'Monitor usage to stay within quotas' ] + }, + + // Code Node Tasks + 'custom_ai_tool': { + task: 'custom_ai_tool', + description: 'Create a custom tool for AI agents using Code node', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + mode: 'runOnceForEachItem', + jsCode: `// Custom AI Tool - Example: Text Analysis +// This code will be called by AI agents with $json containing the input + +// Access the input from the AI agent +const text = $json.text || ''; +const operation = $json.operation || 'analyze'; + +// Perform the requested operation +let result = {}; + +switch (operation) { + case 'wordCount': + result = { + wordCount: text.split(/\\s+/).filter(word => word.length > 0).length, + characterCount: text.length, + lineCount: text.split('\\n').length + }; + break; + + case 'extract': + // Extract specific patterns (emails, URLs, etc.) + result = { + emails: text.match(/[\\w.-]+@[\\w.-]+\\.\\w+/g) || [], + urls: text.match(/https?:\\/\\/[^\\s]+/g) || [], + numbers: text.match(/\\b\\d+\\b/g) || [] + }; + break; + + default: + result = { + error: 'Unknown operation', + availableOperations: ['wordCount', 'extract'] + }; +} + +return [{ + json: { + ...result, + originalText: text, + operation: operation, + processedAt: DateTime.now().toISO() + } +}];`, + onError: 'continueRegularOutput' + }, + userMustProvide: [], + notes: [ + 'Connect this to AI Agent node\'s tool input', + 'AI will pass data in $json', + 'Use "Run Once for Each Item" mode for AI tools', + 'Return structured data the AI can understand' + ] + }, + + 'aggregate_data': { + task: 'aggregate_data', + description: 'Aggregate data from multiple items into summary statistics', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// Aggregate data from all items +const stats = { + count: 0, + sum: 0, + min: Infinity, + max: -Infinity, + values: [], + categories: {}, + errors: [] +}; + +// Process each item +for (const item of items) { + try { + const value = item.json.value || item.json.amount || 0; + const category = item.json.category || 'uncategorized'; + + stats.count++; + stats.sum += value; + stats.min = Math.min(stats.min, value); + stats.max = Math.max(stats.max, value); + stats.values.push(value); + + // Count by category + stats.categories[category] = (stats.categories[category] || 0) + 1; + + } catch (error) { + stats.errors.push({ + item: item.json, + error: error.message + }); + } +} + +// Calculate additional statistics +const average = stats.count > 0 ? stats.sum / stats.count : 0; +const sorted = [...stats.values].sort((a, b) => a - b); +const median = sorted.length > 0 + ? sorted[Math.floor(sorted.length / 2)] + : 0; + +return [{ + json: { + totalItems: stats.count, + sum: stats.sum, + average: average, + median: median, + min: stats.min === Infinity ? 0 : stats.min, + max: stats.max === -Infinity ? 0 : stats.max, + categoryCounts: stats.categories, + errorCount: stats.errors.length, + errors: stats.errors, + processedAt: DateTime.now().toISO() + } +}];`, + onError: 'continueRegularOutput' + }, + userMustProvide: [], + notes: [ + 'Assumes items have "value" or "amount" field', + 'Groups by "category" field if present', + 'Returns single item with all statistics', + 'Handles errors gracefully' + ] + }, + + 'batch_process_with_api': { + task: 'batch_process_with_api', + description: 'Process items in batches with API calls', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// Batch process items with API calls +const BATCH_SIZE = 10; +const API_URL = 'https://api.example.com/batch-process'; // USER MUST UPDATE +const results = []; + +// Process items in batches +for (let i = 0; i < items.length; i += BATCH_SIZE) { + const batch = items.slice(i, i + BATCH_SIZE); + + try { + // Prepare batch data + const batchData = batch.map(item => ({ + id: item.json.id, + data: item.json + })); + + // Make API request for batch + const response = await $helpers.httpRequest({ + method: 'POST', + url: API_URL, + body: { + items: batchData + }, + headers: { + 'Content-Type': 'application/json' + } + }); + + // Add results + if (response.results && Array.isArray(response.results)) { + response.results.forEach((result, index) => { + results.push({ + json: { + ...batch[index].json, + ...result, + batchNumber: Math.floor(i / BATCH_SIZE) + 1, + processedAt: DateTime.now().toISO() + } + }); + }); + } + + // Add delay between batches to avoid rate limits + if (i + BATCH_SIZE < items.length) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + } catch (error) { + // Add failed batch items with error + batch.forEach(item => { + results.push({ + json: { + ...item.json, + error: error.message, + status: 'failed', + batchNumber: Math.floor(i / BATCH_SIZE) + 1 + } + }); + }); + } +} + +return results;`, + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2 + }, + userMustProvide: [ + { + property: 'jsCode', + description: 'Update API_URL in the code', + example: 'https://your-api.com/batch' + } + ], + notes: [ + 'Processes items in batches of 10', + 'Includes delay between batches', + 'Handles batch failures gracefully', + 'Update API_URL and adjust BATCH_SIZE as needed' + ] + }, + + 'error_safe_transform': { + task: 'error_safe_transform', + description: 'Transform data with comprehensive error handling', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// Safe data transformation with validation +const results = []; +const errors = []; + +for (const item of items) { + try { + // Validate required fields + const required = ['id', 'name']; // USER SHOULD UPDATE + const missing = required.filter(field => !item.json[field]); + + if (missing.length > 0) { + throw new Error(\`Missing required fields: \${missing.join(', ')}\`); + } + + // Transform data with type checking + const transformed = { + // Ensure ID is string + id: String(item.json.id), + + // Clean and validate name + name: String(item.json.name).trim(), + + // Parse numbers safely + amount: parseFloat(item.json.amount) || 0, + + // Parse dates safely + date: item.json.date + ? DateTime.fromISO(item.json.date).isValid + ? DateTime.fromISO(item.json.date).toISO() + : null + : null, + + // Boolean conversion + isActive: Boolean(item.json.active || item.json.isActive), + + // Array handling + tags: Array.isArray(item.json.tags) + ? item.json.tags.filter(tag => typeof tag === 'string') + : [], + + // Nested object handling + metadata: typeof item.json.metadata === 'object' + ? item.json.metadata + : {}, + + // Add processing info + processedAt: DateTime.now().toISO(), + originalIndex: items.indexOf(item) + }; + + results.push({ + json: transformed + }); + + } catch (error) { + errors.push({ + json: { + error: error.message, + originalData: item.json, + index: items.indexOf(item), + status: 'failed' + } + }); + } +} + +// Add summary at the end +results.push({ + json: { + _summary: { + totalProcessed: results.length - errors.length, + totalErrors: errors.length, + successRate: ((results.length - errors.length) / items.length * 100).toFixed(2) + '%', + timestamp: DateTime.now().toISO() + } + } +}); + +// Include errors at the end +return [...results, ...errors];`, + onError: 'continueRegularOutput' + }, + userMustProvide: [ + { + property: 'jsCode', + description: 'Update required fields array', + example: "const required = ['id', 'email', 'name'];" + } + ], + notes: [ + 'Validates all data types', + 'Handles missing/invalid data gracefully', + 'Returns both successful and failed items', + 'Includes processing summary' + ] + }, + + 'async_data_processing': { + task: 'async_data_processing', + description: 'Process data with async operations and proper error handling', + nodeType: 'nodes-base.code', + configuration: { + language: 'javaScript', + jsCode: `// Async processing with concurrent limits +const CONCURRENT_LIMIT = 5; +const results = []; + +// Process items with concurrency control +async function processItem(item, index) { + try { + // Simulate async operation (replace with actual logic) + // Example: API call, database query, file operation + await new Promise(resolve => setTimeout(resolve, 100)); + + // Actual processing logic here + const processed = { + ...item.json, + processed: true, + index: index, + timestamp: DateTime.now().toISO() + }; + + // Example async operation - external API call + if (item.json.needsEnrichment) { + const enrichment = await $helpers.httpRequest({ + method: 'GET', + url: \`https://api.example.com/enrich/\${item.json.id}\` + }); + processed.enrichment = enrichment; + } + + return { json: processed }; + + } catch (error) { + return { + json: { + ...item.json, + error: error.message, + status: 'failed', + index: index + } + }; + } +} + +// Process in batches with concurrency limit +for (let i = 0; i < items.length; i += CONCURRENT_LIMIT) { + const batch = items.slice(i, i + CONCURRENT_LIMIT); + const batchPromises = batch.map((item, batchIndex) => + processItem(item, i + batchIndex) + ); + + const batchResults = await Promise.all(batchPromises); + results.push(...batchResults); +} + +return results;`, + onError: 'continueRegularOutput', + retryOnFail: true, + maxTries: 2 + }, + userMustProvide: [], + notes: [ + 'Processes 5 items concurrently', + 'Prevents overwhelming external services', + 'Each item processed independently', + 'Errors don\'t affect other items' + ] + }, + + 'python_data_analysis': { + task: 'python_data_analysis', + description: 'Analyze data using Python with statistics', + nodeType: 'nodes-base.code', + configuration: { + language: 'python', + pythonCode: `# Python data analysis - use underscore prefix for built-in variables +import json +from datetime import datetime +import statistics + +# Collect data for analysis +values = [] +categories = {} +dates = [] + +# Use _input.all() to get items in Python +for item in _input.all(): + # Convert JsProxy to Python dict for safe access + item_data = item.json.to_py() + + # Extract numeric values + if 'value' in item_data or 'amount' in item_data: + value = item_data.get('value', item_data.get('amount', 0)) + if isinstance(value, (int, float)): + values.append(value) + + # Count categories + category = item_data.get('category', 'uncategorized') + categories[category] = categories.get(category, 0) + 1 + + # Collect dates + if 'date' in item_data: + dates.append(item_data['date']) + +# Calculate statistics +result = { + 'itemCount': len(_input.all()), + 'values': { + 'count': len(values), + 'sum': sum(values) if values else 0, + 'mean': statistics.mean(values) if values else 0, + 'median': statistics.median(values) if values else 0, + 'min': min(values) if values else 0, + 'max': max(values) if values else 0, + 'stdev': statistics.stdev(values) if len(values) > 1 else 0 + }, + 'categories': categories, + 'dateRange': { + 'earliest': min(dates) if dates else None, + 'latest': max(dates) if dates else None, + 'count': len(dates) + }, + 'analysis': { + 'hasNumericData': len(values) > 0, + 'hasCategoricalData': len(categories) > 0, + 'hasTemporalData': len(dates) > 0, + 'dataQuality': 'good' if len(values) > len(items) * 0.8 else 'partial' + }, + 'processedAt': datetime.now().isoformat() +} + +# Return single summary item +return [{'json': result}]`, + onError: 'continueRegularOutput' + }, + userMustProvide: [], + notes: [ + 'Uses Python statistics module', + 'Analyzes numeric, categorical, and date data', + 'Returns comprehensive summary', + 'Handles missing data gracefully' + ] } }; @@ -926,10 +1487,10 @@ return results;`, static getTaskCategories(): Record { return { 'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'], - 'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling'], + 'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling', 'process_webhook_data'], 'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'], 'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'], - 'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing'], + 'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing', 'process_webhook_data'], 'Communication': ['send_slack_message', 'send_email'], 'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'], 'Error Handling': ['modern_error_handling_patterns', 'api_call_with_retry', 'fault_tolerant_processing', 'webhook_with_error_handling', 'database_transaction_safety', 'ai_rate_limit_handling']