fix: correct misleading Code node documentation based on real-world testing
Critical fixes based on Claude Desktop feedback:
1. Fixed crypto documentation: require('crypto') IS available despite editor warnings
- Added clear examples of crypto usage
- Updated validation to guide correct require() usage
2. Clarified $helpers vs standalone functions
- $getWorkflowStaticData() is standalone, NOT $helpers.getWorkflowStaticData()
- Added validation to catch incorrect usage (prevents '$helpers is not defined' errors)
- Enhanced examples showing proper $helpers availability checks
3. Fixed JMESPath numeric literal documentation
- n8n requires backticks around numbers in filters: [?age >= `18`]
- Added multiple examples and validation to detect missing backticks
- Prevents 'JMESPath syntax error' that Claude Desktop encountered
4. Fixed webhook data access gotcha
- Webhook payload is at items[0].json.body, NOT items[0].json
- Added dedicated 'Webhook Data Access' section with clear examples
- Created process_webhook_data task template
- Added validation to detect incorrect webhook data access patterns
All fixes based on production workflows TaNqYoZNNeHC4Hne and JZ9urD7PNClDZ1bm
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
43
README.md
43
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
|
||||
|
||||
|
||||
@@ -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<string, string>` to `Record<string, unknown>` 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
|
||||
|
||||
|
||||
203
scripts/test-code-node-enhancements.ts
Executable file
203
scripts/test-code-node-enhancements.ts
Executable file
@@ -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!');
|
||||
133
scripts/test-code-node-fixes.ts
Executable file
133
scripts/test-code-node-fixes.ts
Executable file
@@ -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);
|
||||
138
scripts/test-expression-code-validation.ts
Executable file
138
scripts/test-expression-code-validation.ts
Executable file
@@ -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!');
|
||||
93
scripts/test-helpers-validation.ts
Normal file
93
scripts/test-helpers-validation.ts
Normal file
@@ -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!');
|
||||
114
scripts/test-jmespath-validation.ts
Normal file
114
scripts/test-jmespath-validation.ts
Normal file
@@ -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');
|
||||
111
scripts/test-webhook-validation.ts
Normal file
111
scripts/test-webhook-validation.ts
Normal file
@@ -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');
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -391,12 +391,18 @@ export class ConfigValidator {
|
||||
* Check for common configuration issues
|
||||
*/
|
||||
private static checkCommonIssues(
|
||||
_nodeType: string,
|
||||
nodeType: string,
|
||||
config: Record<string, any>,
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, string[]> {
|
||||
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']
|
||||
|
||||
Reference in New Issue
Block a user