This is page 49 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── _config.yml ├── .claude │ └── agents │ ├── code-reviewer.md │ ├── context-manager.md │ ├── debugger.md │ ├── deployment-engineer.md │ ├── mcp-backend-engineer.md │ ├── n8n-mcp-tester.md │ ├── technical-researcher.md │ └── test-automator.md ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.n8n.example ├── .env.test ├── .env.test.example ├── .github │ ├── ABOUT.md │ ├── BENCHMARK_THRESHOLDS.md │ ├── FUNDING.yml │ ├── gh-pages.yml │ ├── secret_scanning.yml │ └── workflows │ ├── benchmark-pr.yml │ ├── benchmark.yml │ ├── docker-build-fast.yml │ ├── docker-build-n8n.yml │ ├── docker-build.yml │ ├── release.yml │ ├── test.yml │ └── update-n8n-deps.yml ├── .gitignore ├── .npmignore ├── ATTRIBUTION.md ├── CHANGELOG.md ├── CLAUDE.md ├── codecov.yml ├── coverage.json ├── data │ ├── .gitkeep │ ├── nodes.db │ ├── nodes.db-shm │ ├── nodes.db-wal │ └── templates.db ├── deploy │ └── quick-deploy-n8n.sh ├── docker │ ├── docker-entrypoint.sh │ ├── n8n-mcp │ ├── parse-config.js │ └── README.md ├── docker-compose.buildkit.yml ├── docker-compose.extract.yml ├── docker-compose.n8n.yml ├── docker-compose.override.yml.example ├── docker-compose.test-n8n.yml ├── docker-compose.yml ├── Dockerfile ├── Dockerfile.railway ├── Dockerfile.test ├── docs │ ├── AUTOMATED_RELEASES.md │ ├── BENCHMARKS.md │ ├── CHANGELOG.md │ ├── CLAUDE_CODE_SETUP.md │ ├── CLAUDE_INTERVIEW.md │ ├── CODECOV_SETUP.md │ ├── CODEX_SETUP.md │ ├── CURSOR_SETUP.md │ ├── DEPENDENCY_UPDATES.md │ ├── DOCKER_README.md │ ├── DOCKER_TROUBLESHOOTING.md │ ├── FINAL_AI_VALIDATION_SPEC.md │ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md │ ├── HTTP_DEPLOYMENT.md │ ├── img │ │ ├── cc_command.png │ │ ├── cc_connected.png │ │ ├── codex_connected.png │ │ ├── cursor_tut.png │ │ ├── Railway_api.png │ │ ├── Railway_server_address.png │ │ ├── vsc_ghcp_chat_agent_mode.png │ │ ├── vsc_ghcp_chat_instruction_files.png │ │ ├── vsc_ghcp_chat_thinking_tool.png │ │ └── windsurf_tut.png │ ├── INSTALLATION.md │ ├── LIBRARY_USAGE.md │ ├── local │ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md │ │ ├── DEEP_DIVE_ANALYSIS_README.md │ │ ├── Deep_dive_p1_p2.md │ │ ├── integration-testing-plan.md │ │ ├── integration-tests-phase1-summary.md │ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md │ │ ├── P0_IMPLEMENTATION_PLAN.md │ │ └── TEMPLATE_MINING_ANALYSIS.md │ ├── MCP_ESSENTIALS_README.md │ ├── MCP_QUICK_START_GUIDE.md │ ├── N8N_DEPLOYMENT.md │ ├── RAILWAY_DEPLOYMENT.md │ ├── README_CLAUDE_SETUP.md │ ├── README.md │ ├── tools-documentation-usage.md │ ├── VS_CODE_PROJECT_SETUP.md │ ├── WINDSURF_SETUP.md │ └── workflow-diff-examples.md ├── examples │ └── enhanced-documentation-demo.js ├── fetch_log.txt ├── LICENSE ├── MEMORY_N8N_UPDATE.md ├── MEMORY_TEMPLATE_UPDATE.md ├── monitor_fetch.sh ├── N8N_HTTP_STREAMABLE_SETUP.md ├── n8n-nodes.db ├── P0-R3-TEST-PLAN.md ├── package-lock.json ├── package.json ├── package.runtime.json ├── PRIVACY.md ├── railway.json ├── README.md ├── renovate.json ├── scripts │ ├── analyze-optimization.sh │ ├── audit-schema-coverage.ts │ ├── build-optimized.sh │ ├── compare-benchmarks.js │ ├── demo-optimization.sh │ ├── deploy-http.sh │ ├── deploy-to-vm.sh │ ├── export-webhook-workflows.ts │ ├── extract-changelog.js │ ├── extract-from-docker.js │ ├── extract-nodes-docker.sh │ ├── extract-nodes-simple.sh │ ├── format-benchmark-results.js │ ├── generate-benchmark-stub.js │ ├── generate-detailed-reports.js │ ├── generate-test-summary.js │ ├── http-bridge.js │ ├── mcp-http-client.js │ ├── migrate-nodes-fts.ts │ ├── migrate-tool-docs.ts │ ├── n8n-docs-mcp.service │ ├── nginx-n8n-mcp.conf │ ├── prebuild-fts5.ts │ ├── prepare-release.js │ ├── publish-npm-quick.sh │ ├── publish-npm.sh │ ├── quick-test.ts │ ├── run-benchmarks-ci.js │ ├── sync-runtime-version.js │ ├── test-ai-validation-debug.ts │ ├── test-code-node-enhancements.ts │ ├── test-code-node-fixes.ts │ ├── test-docker-config.sh │ ├── test-docker-fingerprint.ts │ ├── test-docker-optimization.sh │ ├── test-docker.sh │ ├── test-empty-connection-validation.ts │ ├── test-error-message-tracking.ts │ ├── test-error-output-validation.ts │ ├── test-error-validation.js │ ├── test-essentials.ts │ ├── test-expression-code-validation.ts │ ├── test-expression-format-validation.js │ ├── test-fts5-search.ts │ ├── test-fuzzy-fix.ts │ ├── test-fuzzy-simple.ts │ ├── test-helpers-validation.ts │ ├── test-http-search.ts │ ├── test-http.sh │ ├── test-jmespath-validation.ts │ ├── test-multi-tenant-simple.ts │ ├── test-multi-tenant.ts │ ├── test-n8n-integration.sh │ ├── test-node-info.js │ ├── test-node-type-validation.ts │ ├── test-nodes-base-prefix.ts │ ├── test-operation-validation.ts │ ├── test-optimized-docker.sh │ ├── test-release-automation.js │ ├── test-search-improvements.ts │ ├── test-security.ts │ ├── test-single-session.sh │ ├── test-sqljs-triggers.ts │ ├── test-telemetry-debug.ts │ ├── test-telemetry-direct.ts │ ├── test-telemetry-env.ts │ ├── test-telemetry-integration.ts │ ├── test-telemetry-no-select.ts │ ├── test-telemetry-security.ts │ ├── test-telemetry-simple.ts │ ├── test-typeversion-validation.ts │ ├── test-url-configuration.ts │ ├── test-user-id-persistence.ts │ ├── test-webhook-validation.ts │ ├── test-workflow-insert.ts │ ├── test-workflow-sanitizer.ts │ ├── test-workflow-tracking-debug.ts │ ├── update-and-publish-prep.sh │ ├── update-n8n-deps.js │ ├── update-readme-version.js │ ├── vitest-benchmark-json-reporter.js │ └── vitest-benchmark-reporter.ts ├── SECURITY.md ├── src │ ├── config │ │ └── n8n-api.ts │ ├── data │ │ └── canonical-ai-tool-examples.json │ ├── database │ │ ├── database-adapter.ts │ │ ├── migrations │ │ │ └── add-template-node-configs.sql │ │ ├── node-repository.ts │ │ ├── nodes.db │ │ ├── schema-optimized.sql │ │ └── schema.sql │ ├── errors │ │ └── validation-service-error.ts │ ├── http-server-single-session.ts │ ├── http-server.ts │ ├── index.ts │ ├── loaders │ │ └── node-loader.ts │ ├── mappers │ │ └── docs-mapper.ts │ ├── mcp │ │ ├── handlers-n8n-manager.ts │ │ ├── handlers-workflow-diff.ts │ │ ├── index.ts │ │ ├── server.ts │ │ ├── stdio-wrapper.ts │ │ ├── tool-docs │ │ │ ├── configuration │ │ │ │ ├── get-node-as-tool-info.ts │ │ │ │ ├── get-node-documentation.ts │ │ │ │ ├── get-node-essentials.ts │ │ │ │ ├── get-node-info.ts │ │ │ │ ├── get-property-dependencies.ts │ │ │ │ ├── index.ts │ │ │ │ └── search-node-properties.ts │ │ │ ├── discovery │ │ │ │ ├── get-database-statistics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-ai-tools.ts │ │ │ │ ├── list-nodes.ts │ │ │ │ └── search-nodes.ts │ │ │ ├── guides │ │ │ │ ├── ai-agents-guide.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── system │ │ │ │ ├── index.ts │ │ │ │ ├── n8n-diagnostic.ts │ │ │ │ ├── n8n-health-check.ts │ │ │ │ ├── n8n-list-available-tools.ts │ │ │ │ └── tools-documentation.ts │ │ │ ├── templates │ │ │ │ ├── get-template.ts │ │ │ │ ├── get-templates-for-task.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-node-templates.ts │ │ │ │ ├── list-tasks.ts │ │ │ │ ├── search-templates-by-metadata.ts │ │ │ │ └── search-templates.ts │ │ │ ├── types.ts │ │ │ ├── validation │ │ │ │ ├── index.ts │ │ │ │ ├── validate-node-minimal.ts │ │ │ │ ├── validate-node-operation.ts │ │ │ │ ├── validate-workflow-connections.ts │ │ │ │ ├── validate-workflow-expressions.ts │ │ │ │ └── validate-workflow.ts │ │ │ └── workflow_management │ │ │ ├── index.ts │ │ │ ├── n8n-autofix-workflow.ts │ │ │ ├── n8n-create-workflow.ts │ │ │ ├── n8n-delete-execution.ts │ │ │ ├── n8n-delete-workflow.ts │ │ │ ├── n8n-get-execution.ts │ │ │ ├── n8n-get-workflow-details.ts │ │ │ ├── n8n-get-workflow-minimal.ts │ │ │ ├── n8n-get-workflow-structure.ts │ │ │ ├── n8n-get-workflow.ts │ │ │ ├── n8n-list-executions.ts │ │ │ ├── n8n-list-workflows.ts │ │ │ ├── n8n-trigger-webhook-workflow.ts │ │ │ ├── n8n-update-full-workflow.ts │ │ │ ├── n8n-update-partial-workflow.ts │ │ │ └── n8n-validate-workflow.ts │ │ ├── tools-documentation.ts │ │ ├── tools-n8n-friendly.ts │ │ ├── tools-n8n-manager.ts │ │ ├── tools.ts │ │ └── workflow-examples.ts │ ├── mcp-engine.ts │ ├── mcp-tools-engine.ts │ ├── n8n │ │ ├── MCPApi.credentials.ts │ │ └── MCPNode.node.ts │ ├── parsers │ │ ├── node-parser.ts │ │ ├── property-extractor.ts │ │ └── simple-parser.ts │ ├── scripts │ │ ├── debug-http-search.ts │ │ ├── extract-from-docker.ts │ │ ├── fetch-templates-robust.ts │ │ ├── fetch-templates.ts │ │ ├── rebuild-database.ts │ │ ├── rebuild-optimized.ts │ │ ├── rebuild.ts │ │ ├── sanitize-templates.ts │ │ ├── seed-canonical-ai-examples.ts │ │ ├── test-autofix-documentation.ts │ │ ├── test-autofix-workflow.ts │ │ ├── test-execution-filtering.ts │ │ ├── test-node-suggestions.ts │ │ ├── test-protocol-negotiation.ts │ │ ├── test-summary.ts │ │ ├── test-webhook-autofix.ts │ │ ├── validate.ts │ │ └── validation-summary.ts │ ├── services │ │ ├── ai-node-validator.ts │ │ ├── ai-tool-validators.ts │ │ ├── confidence-scorer.ts │ │ ├── config-validator.ts │ │ ├── enhanced-config-validator.ts │ │ ├── example-generator.ts │ │ ├── execution-processor.ts │ │ ├── expression-format-validator.ts │ │ ├── expression-validator.ts │ │ ├── n8n-api-client.ts │ │ ├── n8n-validation.ts │ │ ├── node-documentation-service.ts │ │ ├── node-similarity-service.ts │ │ ├── node-specific-validators.ts │ │ ├── operation-similarity-service.ts │ │ ├── property-dependencies.ts │ │ ├── property-filter.ts │ │ ├── resource-similarity-service.ts │ │ ├── sqlite-storage-service.ts │ │ ├── task-templates.ts │ │ ├── universal-expression-validator.ts │ │ ├── workflow-auto-fixer.ts │ │ ├── workflow-diff-engine.ts │ │ └── workflow-validator.ts │ ├── telemetry │ │ ├── batch-processor.ts │ │ ├── config-manager.ts │ │ ├── early-error-logger.ts │ │ ├── error-sanitization-utils.ts │ │ ├── error-sanitizer.ts │ │ ├── event-tracker.ts │ │ ├── event-validator.ts │ │ ├── index.ts │ │ ├── performance-monitor.ts │ │ ├── rate-limiter.ts │ │ ├── startup-checkpoints.ts │ │ ├── telemetry-error.ts │ │ ├── telemetry-manager.ts │ │ ├── telemetry-types.ts │ │ └── workflow-sanitizer.ts │ ├── templates │ │ ├── batch-processor.ts │ │ ├── metadata-generator.ts │ │ ├── README.md │ │ ├── template-fetcher.ts │ │ ├── template-repository.ts │ │ └── template-service.ts │ ├── types │ │ ├── index.ts │ │ ├── instance-context.ts │ │ ├── n8n-api.ts │ │ ├── node-types.ts │ │ └── workflow-diff.ts │ └── utils │ ├── auth.ts │ ├── bridge.ts │ ├── cache-utils.ts │ ├── console-manager.ts │ ├── documentation-fetcher.ts │ ├── enhanced-documentation-fetcher.ts │ ├── error-handler.ts │ ├── example-generator.ts │ ├── fixed-collection-validator.ts │ ├── logger.ts │ ├── mcp-client.ts │ ├── n8n-errors.ts │ ├── node-source-extractor.ts │ ├── node-type-normalizer.ts │ ├── node-type-utils.ts │ ├── node-utils.ts │ ├── npm-version-checker.ts │ ├── protocol-version.ts │ ├── simple-cache.ts │ ├── ssrf-protection.ts │ ├── template-node-resolver.ts │ ├── template-sanitizer.ts │ ├── url-detector.ts │ ├── validation-schemas.ts │ └── version.ts ├── test-output.txt ├── test-reinit-fix.sh ├── tests │ ├── __snapshots__ │ │ └── .gitkeep │ ├── auth.test.ts │ ├── benchmarks │ │ ├── database-queries.bench.ts │ │ ├── index.ts │ │ ├── mcp-tools.bench.ts │ │ ├── mcp-tools.bench.ts.disabled │ │ ├── mcp-tools.bench.ts.skip │ │ ├── node-loading.bench.ts.disabled │ │ ├── README.md │ │ ├── search-operations.bench.ts.disabled │ │ └── validation-performance.bench.ts.disabled │ ├── bridge.test.ts │ ├── comprehensive-extraction-test.js │ ├── data │ │ └── .gitkeep │ ├── debug-slack-doc.js │ ├── demo-enhanced-documentation.js │ ├── docker-tests-README.md │ ├── error-handler.test.ts │ ├── examples │ │ └── using-database-utils.test.ts │ ├── extracted-nodes-db │ │ ├── database-import.json │ │ ├── extraction-report.json │ │ ├── insert-nodes.sql │ │ ├── n8n-nodes-base__Airtable.json │ │ ├── n8n-nodes-base__Discord.json │ │ ├── n8n-nodes-base__Function.json │ │ ├── n8n-nodes-base__HttpRequest.json │ │ ├── n8n-nodes-base__If.json │ │ ├── n8n-nodes-base__Slack.json │ │ ├── n8n-nodes-base__SplitInBatches.json │ │ └── n8n-nodes-base__Webhook.json │ ├── factories │ │ ├── node-factory.ts │ │ └── property-definition-factory.ts │ ├── fixtures │ │ ├── .gitkeep │ │ ├── database │ │ │ └── test-nodes.json │ │ ├── factories │ │ │ ├── node.factory.ts │ │ │ └── parser-node.factory.ts │ │ └── template-configs.ts │ ├── helpers │ │ └── env-helpers.ts │ ├── http-server-auth.test.ts │ ├── integration │ │ ├── ai-validation │ │ │ ├── ai-agent-validation.test.ts │ │ │ ├── ai-tool-validation.test.ts │ │ │ ├── chat-trigger-validation.test.ts │ │ │ ├── e2e-validation.test.ts │ │ │ ├── helpers.ts │ │ │ ├── llm-chain-validation.test.ts │ │ │ ├── README.md │ │ │ └── TEST_REPORT.md │ │ ├── ci │ │ │ └── database-population.test.ts │ │ ├── database │ │ │ ├── connection-management.test.ts │ │ │ ├── empty-database.test.ts │ │ │ ├── fts5-search.test.ts │ │ │ ├── node-fts5-search.test.ts │ │ │ ├── node-repository.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── template-node-configs.test.ts │ │ │ ├── template-repository.test.ts │ │ │ ├── test-utils.ts │ │ │ └── transactions.test.ts │ │ ├── database-integration.test.ts │ │ ├── docker │ │ │ ├── docker-config.test.ts │ │ │ ├── docker-entrypoint.test.ts │ │ │ └── test-helpers.ts │ │ ├── flexible-instance-config.test.ts │ │ ├── mcp │ │ │ └── template-examples-e2e.test.ts │ │ ├── mcp-protocol │ │ │ ├── basic-connection.test.ts │ │ │ ├── error-handling.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── protocol-compliance.test.ts │ │ │ ├── README.md │ │ │ ├── session-management.test.ts │ │ │ ├── test-helpers.ts │ │ │ ├── tool-invocation.test.ts │ │ │ └── workflow-error-validation.test.ts │ │ ├── msw-setup.test.ts │ │ ├── n8n-api │ │ │ ├── executions │ │ │ │ ├── delete-execution.test.ts │ │ │ │ ├── get-execution.test.ts │ │ │ │ ├── list-executions.test.ts │ │ │ │ └── trigger-webhook.test.ts │ │ │ ├── scripts │ │ │ │ └── cleanup-orphans.ts │ │ │ ├── system │ │ │ │ ├── diagnostic.test.ts │ │ │ │ ├── health-check.test.ts │ │ │ │ └── list-tools.test.ts │ │ │ ├── test-connection.ts │ │ │ ├── types │ │ │ │ └── mcp-responses.ts │ │ │ ├── utils │ │ │ │ ├── cleanup-helpers.ts │ │ │ │ ├── credentials.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── mcp-context.ts │ │ │ │ ├── n8n-client.ts │ │ │ │ ├── node-repository.ts │ │ │ │ ├── response-types.ts │ │ │ │ ├── test-context.ts │ │ │ │ └── webhook-workflows.ts │ │ │ └── workflows │ │ │ ├── autofix-workflow.test.ts │ │ │ ├── create-workflow.test.ts │ │ │ ├── delete-workflow.test.ts │ │ │ ├── get-workflow-details.test.ts │ │ │ ├── get-workflow-minimal.test.ts │ │ │ ├── get-workflow-structure.test.ts │ │ │ ├── get-workflow.test.ts │ │ │ ├── list-workflows.test.ts │ │ │ ├── smart-parameters.test.ts │ │ │ ├── update-partial-workflow.test.ts │ │ │ ├── update-workflow.test.ts │ │ │ └── validate-workflow.test.ts │ │ ├── security │ │ │ ├── command-injection-prevention.test.ts │ │ │ └── rate-limiting.test.ts │ │ ├── setup │ │ │ ├── integration-setup.ts │ │ │ └── msw-test-server.ts │ │ ├── telemetry │ │ │ ├── docker-user-id-stability.test.ts │ │ │ └── mcp-telemetry.test.ts │ │ ├── templates │ │ │ └── metadata-operations.test.ts │ │ └── workflow-creation-node-type-format.test.ts │ ├── logger.test.ts │ ├── MOCKING_STRATEGY.md │ ├── mocks │ │ ├── n8n-api │ │ │ ├── data │ │ │ │ ├── credentials.ts │ │ │ │ ├── executions.ts │ │ │ │ └── workflows.ts │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ └── README.md │ ├── node-storage-export.json │ ├── setup │ │ ├── global-setup.ts │ │ ├── msw-setup.ts │ │ ├── TEST_ENV_DOCUMENTATION.md │ │ └── test-env.ts │ ├── test-database-extraction.js │ ├── test-direct-extraction.js │ ├── test-enhanced-documentation.js │ ├── test-enhanced-integration.js │ ├── test-mcp-extraction.js │ ├── test-mcp-server-extraction.js │ ├── test-mcp-tools-integration.js │ ├── test-node-documentation-service.js │ ├── test-node-list.js │ ├── test-package-info.js │ ├── test-parsing-operations.js │ ├── test-slack-node-complete.js │ ├── test-small-rebuild.js │ ├── test-sqlite-search.js │ ├── test-storage-system.js │ ├── unit │ │ ├── __mocks__ │ │ │ ├── n8n-nodes-base.test.ts │ │ │ ├── n8n-nodes-base.ts │ │ │ └── README.md │ │ ├── database │ │ │ ├── __mocks__ │ │ │ │ └── better-sqlite3.ts │ │ │ ├── database-adapter-unit.test.ts │ │ │ ├── node-repository-core.test.ts │ │ │ ├── node-repository-operations.test.ts │ │ │ ├── node-repository-outputs.test.ts │ │ │ ├── README.md │ │ │ └── template-repository-core.test.ts │ │ ├── docker │ │ │ ├── config-security.test.ts │ │ │ ├── edge-cases.test.ts │ │ │ ├── parse-config.test.ts │ │ │ └── serve-command.test.ts │ │ ├── errors │ │ │ └── validation-service-error.test.ts │ │ ├── examples │ │ │ └── using-n8n-nodes-base-mock.test.ts │ │ ├── flexible-instance-security-advanced.test.ts │ │ ├── flexible-instance-security.test.ts │ │ ├── http-server │ │ │ └── multi-tenant-support.test.ts │ │ ├── http-server-n8n-mode.test.ts │ │ ├── http-server-n8n-reinit.test.ts │ │ ├── http-server-session-management.test.ts │ │ ├── loaders │ │ │ └── node-loader.test.ts │ │ ├── mappers │ │ │ └── docs-mapper.test.ts │ │ ├── mcp │ │ │ ├── get-node-essentials-examples.test.ts │ │ │ ├── handlers-n8n-manager-simple.test.ts │ │ │ ├── handlers-n8n-manager.test.ts │ │ │ ├── handlers-workflow-diff.test.ts │ │ │ ├── lru-cache-behavior.test.ts │ │ │ ├── multi-tenant-tool-listing.test.ts.disabled │ │ │ ├── parameter-validation.test.ts │ │ │ ├── search-nodes-examples.test.ts │ │ │ ├── tools-documentation.test.ts │ │ │ └── tools.test.ts │ │ ├── monitoring │ │ │ └── cache-metrics.test.ts │ │ ├── MULTI_TENANT_TEST_COVERAGE.md │ │ ├── multi-tenant-integration.test.ts │ │ ├── parsers │ │ │ ├── node-parser-outputs.test.ts │ │ │ ├── node-parser.test.ts │ │ │ ├── property-extractor.test.ts │ │ │ └── simple-parser.test.ts │ │ ├── scripts │ │ │ └── fetch-templates-extraction.test.ts │ │ ├── services │ │ │ ├── ai-node-validator.test.ts │ │ │ ├── ai-tool-validators.test.ts │ │ │ ├── confidence-scorer.test.ts │ │ │ ├── config-validator-basic.test.ts │ │ │ ├── config-validator-edge-cases.test.ts │ │ │ ├── config-validator-node-specific.test.ts │ │ │ ├── config-validator-security.test.ts │ │ │ ├── debug-validator.test.ts │ │ │ ├── enhanced-config-validator-integration.test.ts │ │ │ ├── enhanced-config-validator-operations.test.ts │ │ │ ├── enhanced-config-validator.test.ts │ │ │ ├── example-generator.test.ts │ │ │ ├── execution-processor.test.ts │ │ │ ├── expression-format-validator.test.ts │ │ │ ├── expression-validator-edge-cases.test.ts │ │ │ ├── expression-validator.test.ts │ │ │ ├── fixed-collection-validation.test.ts │ │ │ ├── loop-output-edge-cases.test.ts │ │ │ ├── n8n-api-client.test.ts │ │ │ ├── n8n-validation.test.ts │ │ │ ├── node-similarity-service.test.ts │ │ │ ├── node-specific-validators.test.ts │ │ │ ├── operation-similarity-service-comprehensive.test.ts │ │ │ ├── operation-similarity-service.test.ts │ │ │ ├── property-dependencies.test.ts │ │ │ ├── property-filter-edge-cases.test.ts │ │ │ ├── property-filter.test.ts │ │ │ ├── resource-similarity-service-comprehensive.test.ts │ │ │ ├── resource-similarity-service.test.ts │ │ │ ├── task-templates.test.ts │ │ │ ├── template-service.test.ts │ │ │ ├── universal-expression-validator.test.ts │ │ │ ├── validation-fixes.test.ts │ │ │ ├── workflow-auto-fixer.test.ts │ │ │ ├── workflow-diff-engine.test.ts │ │ │ ├── workflow-fixed-collection-validation.test.ts │ │ │ ├── workflow-validator-comprehensive.test.ts │ │ │ ├── workflow-validator-edge-cases.test.ts │ │ │ ├── workflow-validator-error-outputs.test.ts │ │ │ ├── workflow-validator-expression-format.test.ts │ │ │ ├── workflow-validator-loops-simple.test.ts │ │ │ ├── workflow-validator-loops.test.ts │ │ │ ├── workflow-validator-mocks.test.ts │ │ │ ├── workflow-validator-performance.test.ts │ │ │ ├── workflow-validator-with-mocks.test.ts │ │ │ └── workflow-validator.test.ts │ │ ├── telemetry │ │ │ ├── batch-processor.test.ts │ │ │ ├── config-manager.test.ts │ │ │ ├── event-tracker.test.ts │ │ │ ├── event-validator.test.ts │ │ │ ├── rate-limiter.test.ts │ │ │ ├── telemetry-error.test.ts │ │ │ ├── telemetry-manager.test.ts │ │ │ ├── v2.18.3-fixes-verification.test.ts │ │ │ └── workflow-sanitizer.test.ts │ │ ├── templates │ │ │ ├── batch-processor.test.ts │ │ │ ├── metadata-generator.test.ts │ │ │ ├── template-repository-metadata.test.ts │ │ │ └── template-repository-security.test.ts │ │ ├── test-env-example.test.ts │ │ ├── test-infrastructure.test.ts │ │ ├── types │ │ │ ├── instance-context-coverage.test.ts │ │ │ └── instance-context-multi-tenant.test.ts │ │ ├── utils │ │ │ ├── auth-timing-safe.test.ts │ │ │ ├── cache-utils.test.ts │ │ │ ├── console-manager.test.ts │ │ │ ├── database-utils.test.ts │ │ │ ├── fixed-collection-validator.test.ts │ │ │ ├── n8n-errors.test.ts │ │ │ ├── node-type-normalizer.test.ts │ │ │ ├── node-type-utils.test.ts │ │ │ ├── node-utils.test.ts │ │ │ ├── simple-cache-memory-leak-fix.test.ts │ │ │ ├── ssrf-protection.test.ts │ │ │ └── template-node-resolver.test.ts │ │ └── validation-fixes.test.ts │ └── utils │ ├── assertions.ts │ ├── builders │ │ └── workflow.builder.ts │ ├── data-generators.ts │ ├── database-utils.ts │ ├── README.md │ └── test-helpers.ts ├── thumbnail.png ├── tsconfig.build.json ├── tsconfig.json ├── types │ ├── mcp.d.ts │ └── test-env.d.ts ├── verify-telemetry-fix.js ├── versioned-nodes.md ├── vitest.config.benchmark.ts ├── vitest.config.integration.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /src/services/workflow-validator.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Workflow Validator for n8n workflows 3 | * Validates complete workflow structure, connections, and node configurations 4 | */ 5 | 6 | import { NodeRepository } from '../database/node-repository'; 7 | import { EnhancedConfigValidator } from './enhanced-config-validator'; 8 | import { ExpressionValidator } from './expression-validator'; 9 | import { ExpressionFormatValidator } from './expression-format-validator'; 10 | import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service'; 11 | import { NodeTypeNormalizer } from '../utils/node-type-normalizer'; 12 | import { Logger } from '../utils/logger'; 13 | import { validateAISpecificNodes, hasAINodes } from './ai-node-validator'; 14 | const logger = new Logger({ prefix: '[WorkflowValidator]' }); 15 | 16 | interface WorkflowNode { 17 | id: string; 18 | name: string; 19 | type: string; 20 | position: [number, number]; 21 | parameters: any; 22 | credentials?: any; 23 | disabled?: boolean; 24 | notes?: string; 25 | notesInFlow?: boolean; 26 | typeVersion?: number; 27 | continueOnFail?: boolean; 28 | onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow'; 29 | retryOnFail?: boolean; 30 | maxTries?: number; 31 | waitBetweenTries?: number; 32 | alwaysOutputData?: boolean; 33 | executeOnce?: boolean; 34 | } 35 | 36 | interface WorkflowConnection { 37 | [sourceNode: string]: { 38 | main?: Array<Array<{ node: string; type: string; index: number }>>; 39 | error?: Array<Array<{ node: string; type: string; index: number }>>; 40 | ai_tool?: Array<Array<{ node: string; type: string; index: number }>>; 41 | }; 42 | } 43 | 44 | interface WorkflowJson { 45 | name?: string; 46 | nodes: WorkflowNode[]; 47 | connections: WorkflowConnection; 48 | settings?: any; 49 | staticData?: any; 50 | pinData?: any; 51 | meta?: any; 52 | } 53 | 54 | interface ValidationIssue { 55 | type: 'error' | 'warning'; 56 | nodeId?: string; 57 | nodeName?: string; 58 | message: string; 59 | details?: any; 60 | } 61 | 62 | export interface WorkflowValidationResult { 63 | valid: boolean; 64 | errors: ValidationIssue[]; 65 | warnings: ValidationIssue[]; 66 | statistics: { 67 | totalNodes: number; 68 | enabledNodes: number; 69 | triggerNodes: number; 70 | validConnections: number; 71 | invalidConnections: number; 72 | expressionsValidated: number; 73 | }; 74 | suggestions: string[]; 75 | } 76 | 77 | export class WorkflowValidator { 78 | private currentWorkflow: WorkflowJson | null = null; 79 | private similarityService: NodeSimilarityService; 80 | 81 | constructor( 82 | private nodeRepository: NodeRepository, 83 | private nodeValidator: typeof EnhancedConfigValidator 84 | ) { 85 | this.similarityService = new NodeSimilarityService(nodeRepository); 86 | } 87 | 88 | /** 89 | * Check if a node is a Sticky Note or other non-executable node 90 | */ 91 | private isStickyNote(node: WorkflowNode): boolean { 92 | const stickyNoteTypes = [ 93 | 'n8n-nodes-base.stickyNote', 94 | 'nodes-base.stickyNote', 95 | '@n8n/n8n-nodes-base.stickyNote' 96 | ]; 97 | return stickyNoteTypes.includes(node.type); 98 | } 99 | 100 | /** 101 | * Validate a complete workflow 102 | */ 103 | async validateWorkflow( 104 | workflow: WorkflowJson, 105 | options: { 106 | validateNodes?: boolean; 107 | validateConnections?: boolean; 108 | validateExpressions?: boolean; 109 | profile?: 'minimal' | 'runtime' | 'ai-friendly' | 'strict'; 110 | } = {} 111 | ): Promise<WorkflowValidationResult> { 112 | // Store current workflow for access in helper methods 113 | this.currentWorkflow = workflow; 114 | 115 | const { 116 | validateNodes = true, 117 | validateConnections = true, 118 | validateExpressions = true, 119 | profile = 'runtime' 120 | } = options; 121 | 122 | const result: WorkflowValidationResult = { 123 | valid: true, 124 | errors: [], 125 | warnings: [], 126 | statistics: { 127 | totalNodes: 0, 128 | enabledNodes: 0, 129 | triggerNodes: 0, 130 | validConnections: 0, 131 | invalidConnections: 0, 132 | expressionsValidated: 0, 133 | }, 134 | suggestions: [] 135 | }; 136 | 137 | try { 138 | // Handle null/undefined workflow 139 | if (!workflow) { 140 | result.errors.push({ 141 | type: 'error', 142 | message: 'Invalid workflow structure: workflow is null or undefined' 143 | }); 144 | result.valid = false; 145 | return result; 146 | } 147 | 148 | // Update statistics after null check (exclude sticky notes from counts) 149 | const executableNodes = Array.isArray(workflow.nodes) ? workflow.nodes.filter(n => !this.isStickyNote(n)) : []; 150 | result.statistics.totalNodes = executableNodes.length; 151 | result.statistics.enabledNodes = executableNodes.filter(n => !n.disabled).length; 152 | 153 | // Basic workflow structure validation 154 | this.validateWorkflowStructure(workflow, result); 155 | 156 | // Only continue if basic structure is valid 157 | if (workflow.nodes && Array.isArray(workflow.nodes) && workflow.connections && typeof workflow.connections === 'object') { 158 | // Validate each node if requested 159 | if (validateNodes && workflow.nodes.length > 0) { 160 | await this.validateAllNodes(workflow, result, profile); 161 | } 162 | 163 | // Validate connections if requested 164 | if (validateConnections) { 165 | this.validateConnections(workflow, result, profile); 166 | } 167 | 168 | // Validate expressions if requested 169 | if (validateExpressions && workflow.nodes.length > 0) { 170 | this.validateExpressions(workflow, result, profile); 171 | } 172 | 173 | // Check workflow patterns and best practices 174 | if (workflow.nodes.length > 0) { 175 | this.checkWorkflowPatterns(workflow, result, profile); 176 | } 177 | 178 | // Validate AI-specific nodes (AI Agent, Chat Trigger, AI tools) 179 | if (workflow.nodes.length > 0 && hasAINodes(workflow)) { 180 | const aiIssues = validateAISpecificNodes(workflow); 181 | // Convert AI validation issues to workflow validation format 182 | for (const issue of aiIssues) { 183 | const validationIssue: ValidationIssue = { 184 | type: issue.severity === 'error' ? 'error' : 'warning', 185 | nodeId: issue.nodeId, 186 | nodeName: issue.nodeName, 187 | message: issue.message, 188 | details: issue.code ? { code: issue.code } : undefined 189 | }; 190 | 191 | if (issue.severity === 'error') { 192 | result.errors.push(validationIssue); 193 | } else { 194 | result.warnings.push(validationIssue); 195 | } 196 | } 197 | } 198 | 199 | // Add suggestions based on findings 200 | this.generateSuggestions(workflow, result); 201 | 202 | // Add AI-specific recovery suggestions if there are errors 203 | if (result.errors.length > 0) { 204 | this.addErrorRecoverySuggestions(result); 205 | } 206 | } 207 | 208 | } catch (error) { 209 | logger.error('Error validating workflow:', error); 210 | result.errors.push({ 211 | type: 'error', 212 | message: `Workflow validation failed: ${error instanceof Error ? error.message : 'Unknown error'}` 213 | }); 214 | } 215 | 216 | result.valid = result.errors.length === 0; 217 | return result; 218 | } 219 | 220 | /** 221 | * Validate basic workflow structure 222 | */ 223 | private validateWorkflowStructure( 224 | workflow: WorkflowJson, 225 | result: WorkflowValidationResult 226 | ): void { 227 | // Check for required fields 228 | if (!workflow.nodes) { 229 | result.errors.push({ 230 | type: 'error', 231 | message: workflow.nodes === null ? 'nodes must be an array' : 'Workflow must have a nodes array' 232 | }); 233 | return; 234 | } 235 | 236 | if (!Array.isArray(workflow.nodes)) { 237 | result.errors.push({ 238 | type: 'error', 239 | message: 'nodes must be an array' 240 | }); 241 | return; 242 | } 243 | 244 | if (!workflow.connections) { 245 | result.errors.push({ 246 | type: 'error', 247 | message: workflow.connections === null ? 'connections must be an object' : 'Workflow must have a connections object' 248 | }); 249 | return; 250 | } 251 | 252 | if (typeof workflow.connections !== 'object' || Array.isArray(workflow.connections)) { 253 | result.errors.push({ 254 | type: 'error', 255 | message: 'connections must be an object' 256 | }); 257 | return; 258 | } 259 | 260 | // Check for empty workflow - this should be a warning, not an error 261 | if (workflow.nodes.length === 0) { 262 | result.warnings.push({ 263 | type: 'warning', 264 | message: 'Workflow is empty - no nodes defined' 265 | }); 266 | return; 267 | } 268 | 269 | // Check for minimum viable workflow 270 | if (workflow.nodes.length === 1) { 271 | const singleNode = workflow.nodes[0]; 272 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(singleNode.type); 273 | const isWebhook = normalizedType === 'nodes-base.webhook' || 274 | normalizedType === 'nodes-base.webhookTrigger'; 275 | const isLangchainNode = normalizedType.startsWith('nodes-langchain.'); 276 | 277 | // Langchain nodes can be validated standalone for AI tool purposes 278 | if (!isWebhook && !isLangchainNode) { 279 | result.errors.push({ 280 | type: 'error', 281 | message: 'Single-node workflows are only valid for webhook endpoints. Add at least one more connected node to create a functional workflow.' 282 | }); 283 | } else if (isWebhook && Object.keys(workflow.connections).length === 0) { 284 | result.warnings.push({ 285 | type: 'warning', 286 | message: 'Webhook node has no connections. Consider adding nodes to process the webhook data.' 287 | }); 288 | } 289 | } 290 | 291 | // Check for empty connections in multi-node workflows 292 | if (workflow.nodes.length > 1) { 293 | const hasEnabledNodes = workflow.nodes.some(n => !n.disabled); 294 | const hasConnections = Object.keys(workflow.connections).length > 0; 295 | 296 | if (hasEnabledNodes && !hasConnections) { 297 | result.errors.push({ 298 | type: 'error', 299 | message: 'Multi-node workflow has no connections. Nodes must be connected to create a workflow. Use connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }' 300 | }); 301 | } 302 | } 303 | 304 | // Check for duplicate node names 305 | const nodeNames = new Set<string>(); 306 | const nodeIds = new Set<string>(); 307 | 308 | for (const node of workflow.nodes) { 309 | if (nodeNames.has(node.name)) { 310 | result.errors.push({ 311 | type: 'error', 312 | nodeId: node.id, 313 | nodeName: node.name, 314 | message: `Duplicate node name: "${node.name}"` 315 | }); 316 | } 317 | nodeNames.add(node.name); 318 | 319 | if (nodeIds.has(node.id)) { 320 | result.errors.push({ 321 | type: 'error', 322 | nodeId: node.id, 323 | message: `Duplicate node ID: "${node.id}"` 324 | }); 325 | } 326 | nodeIds.add(node.id); 327 | } 328 | 329 | // Count trigger nodes - normalize type names first 330 | const triggerNodes = workflow.nodes.filter(n => { 331 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(n.type); 332 | const lowerType = normalizedType.toLowerCase(); 333 | return lowerType.includes('trigger') || 334 | (lowerType.includes('webhook') && !lowerType.includes('respond')) || 335 | normalizedType === 'nodes-base.start' || 336 | normalizedType === 'nodes-base.manualTrigger' || 337 | normalizedType === 'nodes-base.formTrigger'; 338 | }); 339 | result.statistics.triggerNodes = triggerNodes.length; 340 | 341 | // Check for at least one trigger node 342 | if (triggerNodes.length === 0 && workflow.nodes.filter(n => !n.disabled).length > 0) { 343 | result.warnings.push({ 344 | type: 'warning', 345 | message: 'Workflow has no trigger nodes. It can only be executed manually.' 346 | }); 347 | } 348 | } 349 | 350 | /** 351 | * Validate all nodes in the workflow 352 | */ 353 | private async validateAllNodes( 354 | workflow: WorkflowJson, 355 | result: WorkflowValidationResult, 356 | profile: string 357 | ): Promise<void> { 358 | for (const node of workflow.nodes) { 359 | if (node.disabled || this.isStickyNote(node)) continue; 360 | 361 | try { 362 | // Validate node name length 363 | if (node.name && node.name.length > 255) { 364 | result.warnings.push({ 365 | type: 'warning', 366 | nodeId: node.id, 367 | nodeName: node.name, 368 | message: `Node name is very long (${node.name.length} characters). Consider using a shorter name for better readability.` 369 | }); 370 | } 371 | 372 | // Validate node position 373 | if (!Array.isArray(node.position) || node.position.length !== 2) { 374 | result.errors.push({ 375 | type: 'error', 376 | nodeId: node.id, 377 | nodeName: node.name, 378 | message: 'Node position must be an array with exactly 2 numbers [x, y]' 379 | }); 380 | } else { 381 | const [x, y] = node.position; 382 | if (typeof x !== 'number' || typeof y !== 'number' || 383 | !isFinite(x) || !isFinite(y)) { 384 | result.errors.push({ 385 | type: 'error', 386 | nodeId: node.id, 387 | nodeName: node.name, 388 | message: 'Node position values must be finite numbers' 389 | }); 390 | } 391 | } 392 | // Normalize node type FIRST to ensure consistent lookup 393 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type); 394 | 395 | // Update node type in place if it was normalized 396 | if (normalizedType !== node.type) { 397 | node.type = normalizedType; 398 | } 399 | 400 | // Get node definition using normalized type (needed for typeVersion validation) 401 | const nodeInfo = this.nodeRepository.getNode(normalizedType); 402 | 403 | if (!nodeInfo) { 404 | 405 | // Use NodeSimilarityService to find suggestions 406 | const suggestions = await this.similarityService.findSimilarNodes(node.type, 3); 407 | 408 | let message = `Unknown node type: "${node.type}".`; 409 | 410 | if (suggestions.length > 0) { 411 | message += '\n\nDid you mean one of these?'; 412 | for (const suggestion of suggestions) { 413 | const confidence = Math.round(suggestion.confidence * 100); 414 | message += `\n• ${suggestion.nodeType} (${confidence}% match)`; 415 | if (suggestion.displayName) { 416 | message += ` - ${suggestion.displayName}`; 417 | } 418 | message += `\n → ${suggestion.reason}`; 419 | if (suggestion.confidence >= 0.9) { 420 | message += ' (can be auto-fixed)'; 421 | } 422 | } 423 | } else { 424 | message += ' No similar nodes found. Node types must include the package prefix (e.g., "n8n-nodes-base.webhook").'; 425 | } 426 | 427 | const error: any = { 428 | type: 'error', 429 | nodeId: node.id, 430 | nodeName: node.name, 431 | message 432 | }; 433 | 434 | // Add suggestions as metadata for programmatic access 435 | if (suggestions.length > 0) { 436 | error.suggestions = suggestions.map(s => ({ 437 | nodeType: s.nodeType, 438 | confidence: s.confidence, 439 | reason: s.reason 440 | })); 441 | } 442 | 443 | result.errors.push(error); 444 | continue; 445 | } 446 | 447 | // Validate typeVersion for ALL versioned nodes (including langchain nodes) 448 | // CRITICAL: This MUST run BEFORE the langchain skip below! 449 | // Otherwise, langchain nodes with invalid typeVersion (e.g., 99999) would pass validation 450 | // but fail at runtime in n8n. This was the bug fixed in v2.17.4. 451 | if (nodeInfo.isVersioned) { 452 | // Check if typeVersion is missing 453 | if (!node.typeVersion) { 454 | result.errors.push({ 455 | type: 'error', 456 | nodeId: node.id, 457 | nodeName: node.name, 458 | message: `Missing required property 'typeVersion'. Add typeVersion: ${nodeInfo.version || 1}` 459 | }); 460 | } 461 | // Check if typeVersion is invalid (must be non-negative number, version 0 is valid) 462 | else if (typeof node.typeVersion !== 'number' || node.typeVersion < 0) { 463 | result.errors.push({ 464 | type: 'error', 465 | nodeId: node.id, 466 | nodeName: node.name, 467 | message: `Invalid typeVersion: ${node.typeVersion}. Must be a non-negative number` 468 | }); 469 | } 470 | // Check if typeVersion is outdated (less than latest) 471 | else if (nodeInfo.version && node.typeVersion < nodeInfo.version) { 472 | result.warnings.push({ 473 | type: 'warning', 474 | nodeId: node.id, 475 | nodeName: node.name, 476 | message: `Outdated typeVersion: ${node.typeVersion}. Latest is ${nodeInfo.version}` 477 | }); 478 | } 479 | // Check if typeVersion exceeds maximum supported 480 | else if (nodeInfo.version && node.typeVersion > nodeInfo.version) { 481 | result.errors.push({ 482 | type: 'error', 483 | nodeId: node.id, 484 | nodeName: node.name, 485 | message: `typeVersion ${node.typeVersion} exceeds maximum supported version ${nodeInfo.version}` 486 | }); 487 | } 488 | } 489 | 490 | // Skip PARAMETER validation for langchain nodes (but NOT typeVersion validation above!) 491 | // Langchain nodes have dedicated AI-specific validators in validateAISpecificNodes() 492 | // which handle their unique parameter structures (AI connections, tool ports, etc.) 493 | if (normalizedType.startsWith('nodes-langchain.')) { 494 | continue; 495 | } 496 | 497 | // Validate node configuration 498 | const nodeValidation = this.nodeValidator.validateWithMode( 499 | node.type, 500 | node.parameters, 501 | nodeInfo.properties || [], 502 | 'operation', 503 | profile as any 504 | ); 505 | 506 | // Add node-specific errors and warnings 507 | nodeValidation.errors.forEach((error: any) => { 508 | result.errors.push({ 509 | type: 'error', 510 | nodeId: node.id, 511 | nodeName: node.name, 512 | message: typeof error === 'string' ? error : error.message || String(error) 513 | }); 514 | }); 515 | 516 | nodeValidation.warnings.forEach((warning: any) => { 517 | result.warnings.push({ 518 | type: 'warning', 519 | nodeId: node.id, 520 | nodeName: node.name, 521 | message: typeof warning === 'string' ? warning : warning.message || String(warning) 522 | }); 523 | }); 524 | 525 | } catch (error) { 526 | result.errors.push({ 527 | type: 'error', 528 | nodeId: node.id, 529 | nodeName: node.name, 530 | message: `Failed to validate node: ${error instanceof Error ? error.message : 'Unknown error'}` 531 | }); 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * Validate workflow connections 538 | */ 539 | private validateConnections( 540 | workflow: WorkflowJson, 541 | result: WorkflowValidationResult, 542 | profile: string = 'runtime' 543 | ): void { 544 | const nodeMap = new Map(workflow.nodes.map(n => [n.name, n])); 545 | const nodeIdMap = new Map(workflow.nodes.map(n => [n.id, n])); 546 | 547 | // Check all connections 548 | for (const [sourceName, outputs] of Object.entries(workflow.connections)) { 549 | const sourceNode = nodeMap.get(sourceName); 550 | 551 | if (!sourceNode) { 552 | // Check if this is an ID being used instead of a name 553 | const nodeById = nodeIdMap.get(sourceName); 554 | if (nodeById) { 555 | result.errors.push({ 556 | type: 'error', 557 | nodeId: nodeById.id, 558 | nodeName: nodeById.name, 559 | message: `Connection uses node ID '${sourceName}' instead of node name '${nodeById.name}'. In n8n, connections must use node names, not IDs.` 560 | }); 561 | } else { 562 | result.errors.push({ 563 | type: 'error', 564 | message: `Connection from non-existent node: "${sourceName}"` 565 | }); 566 | } 567 | result.statistics.invalidConnections++; 568 | continue; 569 | } 570 | 571 | // Check main outputs 572 | if (outputs.main) { 573 | this.validateConnectionOutputs( 574 | sourceName, 575 | outputs.main, 576 | nodeMap, 577 | nodeIdMap, 578 | result, 579 | 'main' 580 | ); 581 | } 582 | 583 | // Check error outputs 584 | if (outputs.error) { 585 | this.validateConnectionOutputs( 586 | sourceName, 587 | outputs.error, 588 | nodeMap, 589 | nodeIdMap, 590 | result, 591 | 'error' 592 | ); 593 | } 594 | 595 | // Check AI tool outputs 596 | if (outputs.ai_tool) { 597 | this.validateConnectionOutputs( 598 | sourceName, 599 | outputs.ai_tool, 600 | nodeMap, 601 | nodeIdMap, 602 | result, 603 | 'ai_tool' 604 | ); 605 | } 606 | } 607 | 608 | // Check for orphaned nodes (not connected and not triggers) 609 | const connectedNodes = new Set<string>(); 610 | 611 | // Add all source nodes 612 | Object.keys(workflow.connections).forEach(name => connectedNodes.add(name)); 613 | 614 | // Add all target nodes 615 | Object.values(workflow.connections).forEach(outputs => { 616 | if (outputs.main) { 617 | outputs.main.flat().forEach(conn => { 618 | if (conn) connectedNodes.add(conn.node); 619 | }); 620 | } 621 | if (outputs.error) { 622 | outputs.error.flat().forEach(conn => { 623 | if (conn) connectedNodes.add(conn.node); 624 | }); 625 | } 626 | if (outputs.ai_tool) { 627 | outputs.ai_tool.flat().forEach(conn => { 628 | if (conn) connectedNodes.add(conn.node); 629 | }); 630 | } 631 | }); 632 | 633 | // Check for orphaned nodes (exclude sticky notes) 634 | for (const node of workflow.nodes) { 635 | if (node.disabled || this.isStickyNote(node)) continue; 636 | 637 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type); 638 | const isTrigger = normalizedType.toLowerCase().includes('trigger') || 639 | normalizedType.toLowerCase().includes('webhook') || 640 | normalizedType === 'nodes-base.start' || 641 | normalizedType === 'nodes-base.manualTrigger' || 642 | normalizedType === 'nodes-base.formTrigger'; 643 | 644 | if (!connectedNodes.has(node.name) && !isTrigger) { 645 | result.warnings.push({ 646 | type: 'warning', 647 | nodeId: node.id, 648 | nodeName: node.name, 649 | message: 'Node is not connected to any other nodes' 650 | }); 651 | } 652 | } 653 | 654 | // Check for cycles (skip in minimal profile to reduce false positives) 655 | if (profile !== 'minimal' && this.hasCycle(workflow)) { 656 | result.errors.push({ 657 | type: 'error', 658 | message: 'Workflow contains a cycle (infinite loop)' 659 | }); 660 | } 661 | } 662 | 663 | /** 664 | * Validate connection outputs 665 | */ 666 | private validateConnectionOutputs( 667 | sourceName: string, 668 | outputs: Array<Array<{ node: string; type: string; index: number }>>, 669 | nodeMap: Map<string, WorkflowNode>, 670 | nodeIdMap: Map<string, WorkflowNode>, 671 | result: WorkflowValidationResult, 672 | outputType: 'main' | 'error' | 'ai_tool' 673 | ): void { 674 | // Get source node for special validation 675 | const sourceNode = nodeMap.get(sourceName); 676 | 677 | // Special validation for main outputs with error handling 678 | if (outputType === 'main' && sourceNode) { 679 | this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result); 680 | } 681 | 682 | outputs.forEach((outputConnections, outputIndex) => { 683 | if (!outputConnections) return; 684 | 685 | outputConnections.forEach(connection => { 686 | // Check for negative index 687 | if (connection.index < 0) { 688 | result.errors.push({ 689 | type: 'error', 690 | message: `Invalid connection index ${connection.index} from "${sourceName}". Connection indices must be non-negative.` 691 | }); 692 | result.statistics.invalidConnections++; 693 | return; 694 | } 695 | 696 | // Special validation for SplitInBatches node 697 | if (sourceNode && sourceNode.type === 'nodes-base.splitInBatches') { 698 | this.validateSplitInBatchesConnection( 699 | sourceNode, 700 | outputIndex, 701 | connection, 702 | nodeMap, 703 | result 704 | ); 705 | } 706 | 707 | // Check for self-referencing connections 708 | if (connection.node === sourceName) { 709 | // This is only a warning for non-loop nodes 710 | if (sourceNode && sourceNode.type !== 'nodes-base.splitInBatches') { 711 | result.warnings.push({ 712 | type: 'warning', 713 | message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.` 714 | }); 715 | } 716 | } 717 | 718 | const targetNode = nodeMap.get(connection.node); 719 | 720 | if (!targetNode) { 721 | // Check if this is an ID being used instead of a name 722 | const nodeById = nodeIdMap.get(connection.node); 723 | if (nodeById) { 724 | result.errors.push({ 725 | type: 'error', 726 | nodeId: nodeById.id, 727 | nodeName: nodeById.name, 728 | message: `Connection target uses node ID '${connection.node}' instead of node name '${nodeById.name}' (from ${sourceName}). In n8n, connections must use node names, not IDs.` 729 | }); 730 | } else { 731 | result.errors.push({ 732 | type: 'error', 733 | message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"` 734 | }); 735 | } 736 | result.statistics.invalidConnections++; 737 | } else if (targetNode.disabled) { 738 | result.warnings.push({ 739 | type: 'warning', 740 | message: `Connection to disabled node: "${connection.node}" from "${sourceName}"` 741 | }); 742 | } else { 743 | result.statistics.validConnections++; 744 | 745 | // Additional validation for AI tool connections 746 | if (outputType === 'ai_tool') { 747 | this.validateAIToolConnection(sourceName, targetNode, result); 748 | } 749 | } 750 | }); 751 | }); 752 | } 753 | 754 | /** 755 | * Validate error output configuration 756 | */ 757 | private validateErrorOutputConfiguration( 758 | sourceName: string, 759 | sourceNode: WorkflowNode, 760 | outputs: Array<Array<{ node: string; type: string; index: number }>>, 761 | nodeMap: Map<string, WorkflowNode>, 762 | result: WorkflowValidationResult 763 | ): void { 764 | // Check if node has onError: 'continueErrorOutput' 765 | const hasErrorOutputSetting = sourceNode.onError === 'continueErrorOutput'; 766 | const hasErrorConnections = outputs.length > 1 && outputs[1] && outputs[1].length > 0; 767 | 768 | // Validate mismatch between onError setting and connections 769 | if (hasErrorOutputSetting && !hasErrorConnections) { 770 | result.errors.push({ 771 | type: 'error', 772 | nodeId: sourceNode.id, 773 | nodeName: sourceNode.name, 774 | message: `Node has onError: 'continueErrorOutput' but no error output connections in main[1]. Add error handler connections to main[1] or change onError to 'continueRegularOutput' or 'stopWorkflow'.` 775 | }); 776 | } 777 | 778 | if (!hasErrorOutputSetting && hasErrorConnections) { 779 | result.warnings.push({ 780 | type: 'warning', 781 | nodeId: sourceNode.id, 782 | nodeName: sourceNode.name, 783 | message: `Node has error output connections in main[1] but missing onError: 'continueErrorOutput'. Add this property to properly handle errors.` 784 | }); 785 | } 786 | 787 | // Check for common mistake: multiple nodes in main[0] when error handling is intended 788 | if (outputs.length >= 1 && outputs[0] && outputs[0].length > 1) { 789 | // Check if any of the nodes in main[0] look like error handlers 790 | const potentialErrorHandlers = outputs[0].filter(conn => { 791 | const targetNode = nodeMap.get(conn.node); 792 | if (!targetNode) return false; 793 | 794 | const nodeName = targetNode.name.toLowerCase(); 795 | const nodeType = targetNode.type.toLowerCase(); 796 | 797 | // Common patterns for error handler nodes 798 | return nodeName.includes('error') || 799 | nodeName.includes('fail') || 800 | nodeName.includes('catch') || 801 | nodeName.includes('exception') || 802 | nodeType.includes('respondtowebhook') || 803 | nodeType.includes('emailsend'); 804 | }); 805 | 806 | if (potentialErrorHandlers.length > 0) { 807 | const errorHandlerNames = potentialErrorHandlers.map(conn => `"${conn.node}"`).join(', '); 808 | result.errors.push({ 809 | type: 'error', 810 | nodeId: sourceNode.id, 811 | nodeName: sourceNode.name, 812 | message: `Incorrect error output configuration. Nodes ${errorHandlerNames} appear to be error handlers but are in main[0] (success output) along with other nodes.\n\n` + 813 | `INCORRECT (current):\n` + 814 | `"${sourceName}": {\n` + 815 | ` "main": [\n` + 816 | ` [ // main[0] has multiple nodes mixed together\n` + 817 | outputs[0].map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' + 818 | ` ]\n` + 819 | ` ]\n` + 820 | `}\n\n` + 821 | `CORRECT (should be):\n` + 822 | `"${sourceName}": {\n` + 823 | ` "main": [\n` + 824 | ` [ // main[0] = success output\n` + 825 | outputs[0].filter(conn => !potentialErrorHandlers.includes(conn)).map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' + 826 | ` ],\n` + 827 | ` [ // main[1] = error output\n` + 828 | potentialErrorHandlers.map(conn => ` {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' + 829 | ` ]\n` + 830 | ` ]\n` + 831 | `}\n\n` + 832 | `Also add: "onError": "continueErrorOutput" to the "${sourceName}" node.` 833 | }); 834 | } 835 | } 836 | } 837 | 838 | /** 839 | * Validate AI tool connections 840 | */ 841 | private validateAIToolConnection( 842 | sourceName: string, 843 | targetNode: WorkflowNode, 844 | result: WorkflowValidationResult 845 | ): void { 846 | // For AI tool connections, we just need to check if this is being used as a tool 847 | // The source should be an AI Agent connecting to this target node as a tool 848 | 849 | // Get target node info to check if it can be used as a tool 850 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(targetNode.type); 851 | let targetNodeInfo = this.nodeRepository.getNode(normalizedType); 852 | 853 | // Try original type if normalization didn't help (fallback for edge cases) 854 | if (!targetNodeInfo && normalizedType !== targetNode.type) { 855 | targetNodeInfo = this.nodeRepository.getNode(targetNode.type); 856 | } 857 | 858 | if (targetNodeInfo && !targetNodeInfo.isAITool && targetNodeInfo.package !== 'n8n-nodes-base') { 859 | // It's a community node being used as a tool 860 | result.warnings.push({ 861 | type: 'warning', 862 | nodeId: targetNode.id, 863 | nodeName: targetNode.name, 864 | message: `Community node "${targetNode.name}" is being used as an AI tool. Ensure N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true is set.` 865 | }); 866 | } 867 | } 868 | 869 | /** 870 | * Check if workflow has cycles 871 | * Allow legitimate loops for SplitInBatches and similar loop nodes 872 | */ 873 | private hasCycle(workflow: WorkflowJson): boolean { 874 | const visited = new Set<string>(); 875 | const recursionStack = new Set<string>(); 876 | const nodeTypeMap = new Map<string, string>(); 877 | 878 | // Build node type map (exclude sticky notes) 879 | workflow.nodes.forEach(node => { 880 | if (!this.isStickyNote(node)) { 881 | nodeTypeMap.set(node.name, node.type); 882 | } 883 | }); 884 | 885 | // Known legitimate loop node types 886 | const loopNodeTypes = [ 887 | 'n8n-nodes-base.splitInBatches', 888 | 'nodes-base.splitInBatches', 889 | 'n8n-nodes-base.itemLists', 890 | 'nodes-base.itemLists', 891 | 'n8n-nodes-base.loop', 892 | 'nodes-base.loop' 893 | ]; 894 | 895 | const hasCycleDFS = (nodeName: string, pathFromLoopNode: boolean = false): boolean => { 896 | visited.add(nodeName); 897 | recursionStack.add(nodeName); 898 | 899 | const connections = workflow.connections[nodeName]; 900 | if (connections) { 901 | const allTargets: string[] = []; 902 | 903 | if (connections.main) { 904 | connections.main.flat().forEach(conn => { 905 | if (conn) allTargets.push(conn.node); 906 | }); 907 | } 908 | 909 | if (connections.error) { 910 | connections.error.flat().forEach(conn => { 911 | if (conn) allTargets.push(conn.node); 912 | }); 913 | } 914 | 915 | if (connections.ai_tool) { 916 | connections.ai_tool.flat().forEach(conn => { 917 | if (conn) allTargets.push(conn.node); 918 | }); 919 | } 920 | 921 | const currentNodeType = nodeTypeMap.get(nodeName); 922 | const isLoopNode = loopNodeTypes.includes(currentNodeType || ''); 923 | 924 | for (const target of allTargets) { 925 | if (!visited.has(target)) { 926 | if (hasCycleDFS(target, pathFromLoopNode || isLoopNode)) return true; 927 | } else if (recursionStack.has(target)) { 928 | // Allow cycles that involve legitimate loop nodes 929 | const targetNodeType = nodeTypeMap.get(target); 930 | const isTargetLoopNode = loopNodeTypes.includes(targetNodeType || ''); 931 | 932 | // If this cycle involves a loop node, it's legitimate 933 | if (isTargetLoopNode || pathFromLoopNode || isLoopNode) { 934 | continue; // Allow this cycle 935 | } 936 | 937 | return true; // Reject other cycles 938 | } 939 | } 940 | } 941 | 942 | recursionStack.delete(nodeName); 943 | return false; 944 | }; 945 | 946 | // Check from all executable nodes (exclude sticky notes) 947 | for (const node of workflow.nodes) { 948 | if (!this.isStickyNote(node) && !visited.has(node.name)) { 949 | if (hasCycleDFS(node.name)) return true; 950 | } 951 | } 952 | 953 | return false; 954 | } 955 | 956 | /** 957 | * Validate expressions in the workflow 958 | */ 959 | private validateExpressions( 960 | workflow: WorkflowJson, 961 | result: WorkflowValidationResult, 962 | profile: string = 'runtime' 963 | ): void { 964 | const nodeNames = workflow.nodes.map(n => n.name); 965 | 966 | for (const node of workflow.nodes) { 967 | if (node.disabled || this.isStickyNote(node)) continue; 968 | 969 | // Skip expression validation for langchain nodes 970 | // They have AI-specific validators and different expression rules 971 | const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type); 972 | if (normalizedType.startsWith('nodes-langchain.')) { 973 | continue; 974 | } 975 | 976 | // Create expression context 977 | const context = { 978 | availableNodes: nodeNames.filter(n => n !== node.name), 979 | currentNodeName: node.name, 980 | hasInputData: this.nodeHasInput(node.name, workflow), 981 | isInLoop: false // Could be enhanced to detect loop nodes 982 | }; 983 | 984 | // Validate expressions in parameters 985 | const exprValidation = ExpressionValidator.validateNodeExpressions( 986 | node.parameters, 987 | context 988 | ); 989 | 990 | // Count actual expressions found, not just unique variables 991 | const expressionCount = this.countExpressionsInObject(node.parameters); 992 | result.statistics.expressionsValidated += expressionCount; 993 | 994 | // Add expression errors and warnings 995 | exprValidation.errors.forEach(error => { 996 | result.errors.push({ 997 | type: 'error', 998 | nodeId: node.id, 999 | nodeName: node.name, 1000 | message: `Expression error: ${error}` 1001 | }); 1002 | }); 1003 | 1004 | exprValidation.warnings.forEach(warning => { 1005 | result.warnings.push({ 1006 | type: 'warning', 1007 | nodeId: node.id, 1008 | nodeName: node.name, 1009 | message: `Expression warning: ${warning}` 1010 | }); 1011 | }); 1012 | 1013 | // Validate expression format (check for missing = prefix and resource locator format) 1014 | const formatContext = { 1015 | nodeType: node.type, 1016 | nodeName: node.name, 1017 | nodeId: node.id 1018 | }; 1019 | 1020 | const formatIssues = ExpressionFormatValidator.validateNodeParameters( 1021 | node.parameters, 1022 | formatContext 1023 | ); 1024 | 1025 | // Add format errors and warnings 1026 | formatIssues.forEach(issue => { 1027 | const formattedMessage = ExpressionFormatValidator.formatErrorMessage(issue, formatContext); 1028 | 1029 | if (issue.severity === 'error') { 1030 | result.errors.push({ 1031 | type: 'error', 1032 | nodeId: node.id, 1033 | nodeName: node.name, 1034 | message: formattedMessage 1035 | }); 1036 | } else { 1037 | result.warnings.push({ 1038 | type: 'warning', 1039 | nodeId: node.id, 1040 | nodeName: node.name, 1041 | message: formattedMessage 1042 | }); 1043 | } 1044 | }); 1045 | } 1046 | } 1047 | 1048 | /** 1049 | * Count expressions in an object recursively 1050 | */ 1051 | private countExpressionsInObject(obj: any): number { 1052 | let count = 0; 1053 | 1054 | if (typeof obj === 'string') { 1055 | // Count expressions in string 1056 | const matches = obj.match(/\{\{[\s\S]+?\}\}/g); 1057 | if (matches) { 1058 | count += matches.length; 1059 | } 1060 | } else if (Array.isArray(obj)) { 1061 | // Recursively count in arrays 1062 | for (const item of obj) { 1063 | count += this.countExpressionsInObject(item); 1064 | } 1065 | } else if (obj && typeof obj === 'object') { 1066 | // Recursively count in objects 1067 | for (const value of Object.values(obj)) { 1068 | count += this.countExpressionsInObject(value); 1069 | } 1070 | } 1071 | 1072 | return count; 1073 | } 1074 | 1075 | /** 1076 | * Check if a node has input connections 1077 | */ 1078 | private nodeHasInput(nodeName: string, workflow: WorkflowJson): boolean { 1079 | for (const [sourceName, outputs] of Object.entries(workflow.connections)) { 1080 | if (outputs.main) { 1081 | for (const outputConnections of outputs.main) { 1082 | if (outputConnections?.some(conn => conn.node === nodeName)) { 1083 | return true; 1084 | } 1085 | } 1086 | } 1087 | } 1088 | return false; 1089 | } 1090 | 1091 | /** 1092 | * Check workflow patterns and best practices 1093 | */ 1094 | private checkWorkflowPatterns( 1095 | workflow: WorkflowJson, 1096 | result: WorkflowValidationResult, 1097 | profile: string = 'runtime' 1098 | ): void { 1099 | // Check for error handling (n8n uses main[1] for error outputs, not outputs.error) 1100 | const hasErrorHandling = Object.values(workflow.connections).some( 1101 | outputs => outputs.main && outputs.main.length > 1 && outputs.main[1] && outputs.main[1].length > 0 1102 | ); 1103 | 1104 | // Only suggest error handling in stricter profiles 1105 | if (!hasErrorHandling && workflow.nodes.length > 3 && profile !== 'minimal') { 1106 | result.warnings.push({ 1107 | type: 'warning', 1108 | message: 'Consider adding error handling to your workflow' 1109 | }); 1110 | } 1111 | 1112 | // Check node-level error handling properties for ALL executable nodes 1113 | for (const node of workflow.nodes) { 1114 | if (!this.isStickyNote(node)) { 1115 | this.checkNodeErrorHandling(node, workflow, result); 1116 | } 1117 | } 1118 | 1119 | // Check for very long linear workflows 1120 | const linearChainLength = this.getLongestLinearChain(workflow); 1121 | if (linearChainLength > 10) { 1122 | result.warnings.push({ 1123 | type: 'warning', 1124 | message: `Long linear chain detected (${linearChainLength} nodes). Consider breaking into sub-workflows.` 1125 | }); 1126 | } 1127 | 1128 | // Generate error handling suggestions based on all nodes 1129 | this.generateErrorHandlingSuggestions(workflow, result); 1130 | 1131 | // Check for missing credentials 1132 | for (const node of workflow.nodes) { 1133 | if (node.credentials && Object.keys(node.credentials).length > 0) { 1134 | for (const [credType, credConfig] of Object.entries(node.credentials)) { 1135 | if (!credConfig || (typeof credConfig === 'object' && !('id' in credConfig))) { 1136 | result.warnings.push({ 1137 | type: 'warning', 1138 | nodeId: node.id, 1139 | nodeName: node.name, 1140 | message: `Missing credentials configuration for ${credType}` 1141 | }); 1142 | } 1143 | } 1144 | } 1145 | } 1146 | 1147 | // Check for AI Agent workflows 1148 | const aiAgentNodes = workflow.nodes.filter(n => 1149 | n.type.toLowerCase().includes('agent') || 1150 | n.type.includes('langchain.agent') 1151 | ); 1152 | 1153 | if (aiAgentNodes.length > 0) { 1154 | // Check if AI agents have tools connected 1155 | for (const agentNode of aiAgentNodes) { 1156 | const connections = workflow.connections[agentNode.name]; 1157 | if (!connections?.ai_tool || connections.ai_tool.flat().filter(c => c).length === 0) { 1158 | result.warnings.push({ 1159 | type: 'warning', 1160 | nodeId: agentNode.id, 1161 | nodeName: agentNode.name, 1162 | message: 'AI Agent has no tools connected. Consider adding tools to enhance agent capabilities.' 1163 | }); 1164 | } 1165 | } 1166 | 1167 | // Check for community nodes used as tools 1168 | const hasAIToolConnections = Object.values(workflow.connections).some( 1169 | outputs => outputs.ai_tool && outputs.ai_tool.length > 0 1170 | ); 1171 | 1172 | if (hasAIToolConnections) { 1173 | result.suggestions.push( 1174 | 'For community nodes used as AI tools, ensure N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true is set' 1175 | ); 1176 | } 1177 | } 1178 | } 1179 | 1180 | /** 1181 | * Get the longest linear chain in the workflow 1182 | */ 1183 | private getLongestLinearChain(workflow: WorkflowJson): number { 1184 | const memo = new Map<string, number>(); 1185 | const visiting = new Set<string>(); 1186 | 1187 | const getChainLength = (nodeName: string): number => { 1188 | // If we're already visiting this node, we have a cycle 1189 | if (visiting.has(nodeName)) return 0; 1190 | 1191 | if (memo.has(nodeName)) return memo.get(nodeName)!; 1192 | 1193 | visiting.add(nodeName); 1194 | 1195 | let maxLength = 0; 1196 | const connections = workflow.connections[nodeName]; 1197 | 1198 | if (connections?.main) { 1199 | for (const outputConnections of connections.main) { 1200 | if (outputConnections) { 1201 | for (const conn of outputConnections) { 1202 | const length = getChainLength(conn.node); 1203 | maxLength = Math.max(maxLength, length); 1204 | } 1205 | } 1206 | } 1207 | } 1208 | 1209 | visiting.delete(nodeName); 1210 | const result = maxLength + 1; 1211 | memo.set(nodeName, result); 1212 | return result; 1213 | }; 1214 | 1215 | let maxChain = 0; 1216 | for (const node of workflow.nodes) { 1217 | if (!this.nodeHasInput(node.name, workflow)) { 1218 | maxChain = Math.max(maxChain, getChainLength(node.name)); 1219 | } 1220 | } 1221 | 1222 | return maxChain; 1223 | } 1224 | 1225 | 1226 | /** 1227 | * Generate suggestions based on validation results 1228 | */ 1229 | private generateSuggestions( 1230 | workflow: WorkflowJson, 1231 | result: WorkflowValidationResult 1232 | ): void { 1233 | // Suggest adding trigger if missing 1234 | if (result.statistics.triggerNodes === 0) { 1235 | result.suggestions.push( 1236 | 'Add a trigger node (e.g., Webhook, Schedule Trigger) to automate workflow execution' 1237 | ); 1238 | } 1239 | 1240 | // Suggest proper connection structure for workflows with connection errors 1241 | const hasConnectionErrors = result.errors.some(e => 1242 | typeof e.message === 'string' && ( 1243 | e.message.includes('connection') || 1244 | e.message.includes('Connection') || 1245 | e.message.includes('Multi-node workflow has no connections') 1246 | ) 1247 | ); 1248 | 1249 | if (hasConnectionErrors) { 1250 | result.suggestions.push( 1251 | 'Example connection structure: connections: { "Manual Trigger": { "main": [[{ "node": "Set", "type": "main", "index": 0 }]] } }' 1252 | ); 1253 | result.suggestions.push( 1254 | 'Remember: Use node NAMES (not IDs) in connections. The name is what you see in the UI, not the node type.' 1255 | ); 1256 | } 1257 | 1258 | // Suggest error handling 1259 | if (!Object.values(workflow.connections).some(o => o.error)) { 1260 | result.suggestions.push( 1261 | 'Add error handling using the error output of nodes or an Error Trigger node' 1262 | ); 1263 | } 1264 | 1265 | // Suggest optimization for large workflows 1266 | if (workflow.nodes.length > 20) { 1267 | result.suggestions.push( 1268 | 'Consider breaking this workflow into smaller sub-workflows for better maintainability' 1269 | ); 1270 | } 1271 | 1272 | // Suggest using Code node for complex logic 1273 | const complexExpressionNodes = workflow.nodes.filter(node => { 1274 | const jsonString = JSON.stringify(node.parameters); 1275 | const expressionCount = (jsonString.match(/\{\{/g) || []).length; 1276 | return expressionCount > 5; 1277 | }); 1278 | 1279 | if (complexExpressionNodes.length > 0) { 1280 | result.suggestions.push( 1281 | 'Consider using a Code node for complex data transformations instead of multiple expressions' 1282 | ); 1283 | } 1284 | 1285 | // Suggest minimum workflow structure 1286 | if (workflow.nodes.length === 1 && Object.keys(workflow.connections).length === 0) { 1287 | result.suggestions.push( 1288 | 'A minimal workflow needs: 1) A trigger node (e.g., Manual Trigger), 2) An action node (e.g., Set, HTTP Request), 3) A connection between them' 1289 | ); 1290 | } 1291 | } 1292 | 1293 | /** 1294 | * Check node-level error handling configuration for a single node 1295 | */ 1296 | private checkNodeErrorHandling( 1297 | node: WorkflowNode, 1298 | workflow: WorkflowJson, 1299 | result: WorkflowValidationResult 1300 | ): void { 1301 | // Only skip if disabled is explicitly true (not just truthy) 1302 | if (node.disabled === true) return; 1303 | 1304 | // Define node types that typically interact with external services (lowercase for comparison) 1305 | const errorProneNodeTypes = [ 1306 | 'httprequest', 1307 | 'webhook', 1308 | 'emailsend', 1309 | 'slack', 1310 | 'discord', 1311 | 'telegram', 1312 | 'postgres', 1313 | 'mysql', 1314 | 'mongodb', 1315 | 'redis', 1316 | 'github', 1317 | 'gitlab', 1318 | 'jira', 1319 | 'salesforce', 1320 | 'hubspot', 1321 | 'airtable', 1322 | 'googlesheets', 1323 | 'googledrive', 1324 | 'dropbox', 1325 | 's3', 1326 | 'ftp', 1327 | 'ssh', 1328 | 'mqtt', 1329 | 'kafka', 1330 | 'rabbitmq', 1331 | 'graphql', 1332 | 'openai', 1333 | 'anthropic' 1334 | ]; 1335 | 1336 | const normalizedType = node.type.toLowerCase(); 1337 | const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type)); 1338 | 1339 | // CRITICAL: Check for node-level properties in wrong location (inside parameters) 1340 | const nodeLevelProps = [ 1341 | // Error handling properties 1342 | 'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData', 1343 | // Other node-level properties 1344 | 'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials' 1345 | ]; 1346 | const misplacedProps: string[] = []; 1347 | 1348 | if (node.parameters) { 1349 | for (const prop of nodeLevelProps) { 1350 | if (node.parameters[prop] !== undefined) { 1351 | misplacedProps.push(prop); 1352 | } 1353 | } 1354 | } 1355 | 1356 | if (misplacedProps.length > 0) { 1357 | result.errors.push({ 1358 | type: 'error', 1359 | nodeId: node.id, 1360 | nodeName: node.name, 1361 | message: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`, 1362 | details: { 1363 | fix: `Move these properties from node.parameters to the node level. Example:\n` + 1364 | `{\n` + 1365 | ` "name": "${node.name}",\n` + 1366 | ` "type": "${node.type}",\n` + 1367 | ` "parameters": { /* operation-specific params */ },\n` + 1368 | ` "onError": "continueErrorOutput", // ✅ Correct location\n` + 1369 | ` "retryOnFail": true, // ✅ Correct location\n` + 1370 | ` "executeOnce": true, // ✅ Correct location\n` + 1371 | ` "disabled": false, // ✅ Correct location\n` + 1372 | ` "credentials": { /* ... */ } // ✅ Correct location\n` + 1373 | `}` 1374 | } 1375 | }); 1376 | } 1377 | 1378 | // Validate error handling properties 1379 | 1380 | // Check for onError property (the modern approach) 1381 | if (node.onError !== undefined) { 1382 | const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow']; 1383 | if (!validOnErrorValues.includes(node.onError)) { 1384 | result.errors.push({ 1385 | type: 'error', 1386 | nodeId: node.id, 1387 | nodeName: node.name, 1388 | message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}` 1389 | }); 1390 | } 1391 | } 1392 | 1393 | // Check for deprecated continueOnFail 1394 | if (node.continueOnFail !== undefined) { 1395 | if (typeof node.continueOnFail !== 'boolean') { 1396 | result.errors.push({ 1397 | type: 'error', 1398 | nodeId: node.id, 1399 | nodeName: node.name, 1400 | message: 'continueOnFail must be a boolean value' 1401 | }); 1402 | } else if (node.continueOnFail === true) { 1403 | // Warn about using deprecated property 1404 | result.warnings.push({ 1405 | type: 'warning', 1406 | nodeId: node.id, 1407 | nodeName: node.name, 1408 | message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.' 1409 | }); 1410 | } 1411 | } 1412 | 1413 | // Check for conflicting error handling properties 1414 | if (node.continueOnFail !== undefined && node.onError !== undefined) { 1415 | result.errors.push({ 1416 | type: 'error', 1417 | nodeId: node.id, 1418 | nodeName: node.name, 1419 | message: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.' 1420 | }); 1421 | } 1422 | 1423 | if (node.retryOnFail !== undefined) { 1424 | if (typeof node.retryOnFail !== 'boolean') { 1425 | result.errors.push({ 1426 | type: 'error', 1427 | nodeId: node.id, 1428 | nodeName: node.name, 1429 | message: 'retryOnFail must be a boolean value' 1430 | }); 1431 | } 1432 | 1433 | // If retry is enabled, check retry configuration 1434 | if (node.retryOnFail === true) { 1435 | if (node.maxTries !== undefined) { 1436 | if (typeof node.maxTries !== 'number' || node.maxTries < 1) { 1437 | result.errors.push({ 1438 | type: 'error', 1439 | nodeId: node.id, 1440 | nodeName: node.name, 1441 | message: 'maxTries must be a positive number when retryOnFail is enabled' 1442 | }); 1443 | } else if (node.maxTries > 10) { 1444 | result.warnings.push({ 1445 | type: 'warning', 1446 | nodeId: node.id, 1447 | nodeName: node.name, 1448 | message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.` 1449 | }); 1450 | } 1451 | } else { 1452 | // maxTries defaults to 3 if not specified 1453 | result.warnings.push({ 1454 | type: 'warning', 1455 | nodeId: node.id, 1456 | nodeName: node.name, 1457 | message: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.' 1458 | }); 1459 | } 1460 | 1461 | if (node.waitBetweenTries !== undefined) { 1462 | if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) { 1463 | result.errors.push({ 1464 | type: 'error', 1465 | nodeId: node.id, 1466 | nodeName: node.name, 1467 | message: 'waitBetweenTries must be a non-negative number (milliseconds)' 1468 | }); 1469 | } else if (node.waitBetweenTries > 300000) { // 5 minutes 1470 | result.warnings.push({ 1471 | type: 'warning', 1472 | nodeId: node.id, 1473 | nodeName: node.name, 1474 | message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries/1000).toFixed(1)}s). This seems excessive.` 1475 | }); 1476 | } 1477 | } 1478 | } 1479 | } 1480 | 1481 | if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') { 1482 | result.errors.push({ 1483 | type: 'error', 1484 | nodeId: node.id, 1485 | nodeName: node.name, 1486 | message: 'alwaysOutputData must be a boolean value' 1487 | }); 1488 | } 1489 | 1490 | // Warnings for error-prone nodes without error handling 1491 | const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail; 1492 | 1493 | if (isErrorProne && !hasErrorHandling) { 1494 | const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType; 1495 | 1496 | // Special handling for specific node types 1497 | if (normalizedType.includes('httprequest')) { 1498 | result.warnings.push({ 1499 | type: 'warning', 1500 | nodeId: node.id, 1501 | nodeName: node.name, 1502 | message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.' 1503 | }); 1504 | } else if (normalizedType.includes('webhook')) { 1505 | result.warnings.push({ 1506 | type: 'warning', 1507 | nodeId: node.id, 1508 | nodeName: node.name, 1509 | message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.' 1510 | }); 1511 | } else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) { 1512 | result.warnings.push({ 1513 | type: 'warning', 1514 | nodeId: node.id, 1515 | nodeName: node.name, 1516 | message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.` 1517 | }); 1518 | } else { 1519 | result.warnings.push({ 1520 | type: 'warning', 1521 | nodeId: node.id, 1522 | nodeName: node.name, 1523 | message: `${nodeTypeSimple} node without error handling. Consider using "onError" property for better error management.` 1524 | }); 1525 | } 1526 | } 1527 | 1528 | // Check for problematic combinations 1529 | if (node.continueOnFail && node.retryOnFail) { 1530 | result.warnings.push({ 1531 | type: 'warning', 1532 | nodeId: node.id, 1533 | nodeName: node.name, 1534 | message: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.' 1535 | }); 1536 | } 1537 | 1538 | // Validate additional node-level properties 1539 | 1540 | // Check executeOnce 1541 | if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') { 1542 | result.errors.push({ 1543 | type: 'error', 1544 | nodeId: node.id, 1545 | nodeName: node.name, 1546 | message: 'executeOnce must be a boolean value' 1547 | }); 1548 | } 1549 | 1550 | // Check disabled 1551 | if (node.disabled !== undefined && typeof node.disabled !== 'boolean') { 1552 | result.errors.push({ 1553 | type: 'error', 1554 | nodeId: node.id, 1555 | nodeName: node.name, 1556 | message: 'disabled must be a boolean value' 1557 | }); 1558 | } 1559 | 1560 | // Check notesInFlow 1561 | if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') { 1562 | result.errors.push({ 1563 | type: 'error', 1564 | nodeId: node.id, 1565 | nodeName: node.name, 1566 | message: 'notesInFlow must be a boolean value' 1567 | }); 1568 | } 1569 | 1570 | // Check notes 1571 | if (node.notes !== undefined && typeof node.notes !== 'string') { 1572 | result.errors.push({ 1573 | type: 'error', 1574 | nodeId: node.id, 1575 | nodeName: node.name, 1576 | message: 'notes must be a string value' 1577 | }); 1578 | } 1579 | 1580 | // Provide guidance for executeOnce 1581 | if (node.executeOnce === true) { 1582 | result.warnings.push({ 1583 | type: 'warning', 1584 | nodeId: node.id, 1585 | nodeName: node.name, 1586 | message: 'executeOnce is enabled. This node will execute only once regardless of input items.' 1587 | }); 1588 | } 1589 | 1590 | // Suggest alwaysOutputData for debugging 1591 | if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) { 1592 | if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) { 1593 | result.suggestions.push( 1594 | `Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging` 1595 | ); 1596 | } 1597 | } 1598 | 1599 | } 1600 | 1601 | /** 1602 | * Generate error handling suggestions based on all nodes 1603 | */ 1604 | private generateErrorHandlingSuggestions( 1605 | workflow: WorkflowJson, 1606 | result: WorkflowValidationResult 1607 | ): void { 1608 | // Add general suggestions based on findings 1609 | const nodesWithoutErrorHandling = workflow.nodes.filter(n => 1610 | !n.disabled && !n.onError && !n.continueOnFail && !n.retryOnFail 1611 | ).length; 1612 | 1613 | if (nodesWithoutErrorHandling > 5 && workflow.nodes.length > 5) { 1614 | result.suggestions.push( 1615 | 'Most nodes lack error handling. Use "onError" property for modern error handling: "continueRegularOutput" (continue on error), "continueErrorOutput" (use error output), or "stopWorkflow" (stop execution).' 1616 | ); 1617 | } 1618 | 1619 | // Check for nodes using deprecated continueOnFail 1620 | const nodesWithDeprecatedErrorHandling = workflow.nodes.filter(n => 1621 | !n.disabled && n.continueOnFail === true 1622 | ).length; 1623 | 1624 | if (nodesWithDeprecatedErrorHandling > 0) { 1625 | result.suggestions.push( 1626 | 'Replace "continueOnFail: true" with "onError: \'continueRegularOutput\'" for better UI compatibility and control.' 1627 | ); 1628 | } 1629 | } 1630 | 1631 | /** 1632 | * Validate SplitInBatches node connections for common mistakes 1633 | */ 1634 | private validateSplitInBatchesConnection( 1635 | sourceNode: WorkflowNode, 1636 | outputIndex: number, 1637 | connection: { node: string; type: string; index: number }, 1638 | nodeMap: Map<string, WorkflowNode>, 1639 | result: WorkflowValidationResult 1640 | ): void { 1641 | const targetNode = nodeMap.get(connection.node); 1642 | if (!targetNode) return; 1643 | 1644 | // Check if connections appear to be reversed 1645 | // Output 0 = "done", Output 1 = "loop" 1646 | 1647 | if (outputIndex === 0) { 1648 | // This is the "done" output (index 0) 1649 | // Check if target looks like it should be in the loop 1650 | const targetType = targetNode.type.toLowerCase(); 1651 | const targetName = targetNode.name.toLowerCase(); 1652 | 1653 | // Common patterns that suggest this node should be inside the loop 1654 | if (targetType.includes('function') || 1655 | targetType.includes('code') || 1656 | targetType.includes('item') || 1657 | targetName.includes('process') || 1658 | targetName.includes('transform') || 1659 | targetName.includes('handle')) { 1660 | 1661 | // Check if this node connects back to the SplitInBatches 1662 | const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap); 1663 | 1664 | if (hasLoopBack) { 1665 | result.errors.push({ 1666 | type: 'error', 1667 | nodeId: sourceNode.id, 1668 | nodeName: sourceNode.name, 1669 | message: `SplitInBatches outputs appear reversed! Node "${targetNode.name}" is connected to output 0 ("done") but connects back to the loop. It should be connected to output 1 ("loop") instead. Remember: Output 0 = "done" (post-loop), Output 1 = "loop" (inside loop).` 1670 | }); 1671 | } else { 1672 | result.warnings.push({ 1673 | type: 'warning', 1674 | nodeId: sourceNode.id, 1675 | nodeName: sourceNode.name, 1676 | message: `Node "${targetNode.name}" is connected to the "done" output (index 0) but appears to be a processing node. Consider connecting it to the "loop" output (index 1) if it should process items inside the loop.` 1677 | }); 1678 | } 1679 | } 1680 | } else if (outputIndex === 1) { 1681 | // This is the "loop" output (index 1) 1682 | // Check if target looks like it should be after the loop 1683 | const targetType = targetNode.type.toLowerCase(); 1684 | const targetName = targetNode.name.toLowerCase(); 1685 | 1686 | // Common patterns that suggest this node should be after the loop 1687 | if (targetType.includes('aggregate') || 1688 | targetType.includes('merge') || 1689 | targetType.includes('email') || 1690 | targetType.includes('slack') || 1691 | targetName.includes('final') || 1692 | targetName.includes('complete') || 1693 | targetName.includes('summary') || 1694 | targetName.includes('report')) { 1695 | 1696 | result.warnings.push({ 1697 | type: 'warning', 1698 | nodeId: sourceNode.id, 1699 | nodeName: sourceNode.name, 1700 | message: `Node "${targetNode.name}" is connected to the "loop" output (index 1) but appears to be a post-processing node. Consider connecting it to the "done" output (index 0) if it should run after all iterations complete.` 1701 | }); 1702 | } 1703 | 1704 | // Check if loop output doesn't eventually connect back 1705 | const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap); 1706 | if (!hasLoopBack) { 1707 | result.warnings.push({ 1708 | type: 'warning', 1709 | nodeId: sourceNode.id, 1710 | nodeName: sourceNode.name, 1711 | message: `The "loop" output connects to "${targetNode.name}" but doesn't connect back to the SplitInBatches node. The last node in the loop should connect back to complete the iteration.` 1712 | }); 1713 | } 1714 | } 1715 | } 1716 | 1717 | /** 1718 | * Check if a node eventually connects back to a target node 1719 | */ 1720 | private checkForLoopBack( 1721 | startNode: string, 1722 | targetNode: string, 1723 | nodeMap: Map<string, WorkflowNode>, 1724 | visited: Set<string> = new Set(), 1725 | maxDepth: number = 50 1726 | ): boolean { 1727 | if (maxDepth <= 0) return false; // Prevent stack overflow 1728 | if (visited.has(startNode)) return false; 1729 | visited.add(startNode); 1730 | 1731 | const node = nodeMap.get(startNode); 1732 | if (!node) return false; 1733 | 1734 | // Access connections from the workflow structure, not the node 1735 | // We need to access this.currentWorkflow.connections[startNode] 1736 | const connections = (this as any).currentWorkflow?.connections[startNode]; 1737 | if (!connections) return false; 1738 | 1739 | for (const [outputType, outputs] of Object.entries(connections)) { 1740 | if (!Array.isArray(outputs)) continue; 1741 | 1742 | for (const outputConnections of outputs) { 1743 | if (!Array.isArray(outputConnections)) continue; 1744 | 1745 | for (const conn of outputConnections) { 1746 | if (conn.node === targetNode) { 1747 | return true; 1748 | } 1749 | 1750 | // Recursively check connected nodes 1751 | if (this.checkForLoopBack(conn.node, targetNode, nodeMap, visited, maxDepth - 1)) { 1752 | return true; 1753 | } 1754 | } 1755 | } 1756 | } 1757 | 1758 | return false; 1759 | } 1760 | 1761 | /** 1762 | * Add AI-specific error recovery suggestions 1763 | */ 1764 | private addErrorRecoverySuggestions(result: WorkflowValidationResult): void { 1765 | // Categorize errors and provide specific recovery actions 1766 | const errorTypes = { 1767 | nodeType: result.errors.filter(e => e.message.includes('node type') || e.message.includes('Node type')), 1768 | connection: result.errors.filter(e => e.message.includes('connection') || e.message.includes('Connection')), 1769 | structure: result.errors.filter(e => e.message.includes('structure') || e.message.includes('nodes must be')), 1770 | configuration: result.errors.filter(e => e.message.includes('property') || e.message.includes('field')), 1771 | typeVersion: result.errors.filter(e => e.message.includes('typeVersion')) 1772 | }; 1773 | 1774 | // Add recovery suggestions based on error types 1775 | if (errorTypes.nodeType.length > 0) { 1776 | result.suggestions.unshift( 1777 | '🔧 RECOVERY: Invalid node types detected. Use these patterns:', 1778 | ' • For core nodes: "n8n-nodes-base.nodeName" (e.g., "n8n-nodes-base.webhook")', 1779 | ' • For AI nodes: "@n8n/n8n-nodes-langchain.nodeName"', 1780 | ' • Never use just the node name without package prefix' 1781 | ); 1782 | } 1783 | 1784 | if (errorTypes.connection.length > 0) { 1785 | result.suggestions.unshift( 1786 | '🔧 RECOVERY: Connection errors detected. Fix with:', 1787 | ' • Use node NAMES in connections, not IDs or types', 1788 | ' • Structure: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }', 1789 | ' • Ensure all referenced nodes exist in the workflow' 1790 | ); 1791 | } 1792 | 1793 | if (errorTypes.structure.length > 0) { 1794 | result.suggestions.unshift( 1795 | '🔧 RECOVERY: Workflow structure errors. Fix with:', 1796 | ' • Ensure "nodes" is an array: "nodes": [...]', 1797 | ' • Ensure "connections" is an object: "connections": {...}', 1798 | ' • Add at least one node to create a valid workflow' 1799 | ); 1800 | } 1801 | 1802 | if (errorTypes.configuration.length > 0) { 1803 | result.suggestions.unshift( 1804 | '🔧 RECOVERY: Node configuration errors. Fix with:', 1805 | ' • Check required fields using validate_node_minimal first', 1806 | ' • Use get_node_essentials to see what fields are needed', 1807 | ' • Ensure operation-specific fields match the node\'s requirements' 1808 | ); 1809 | } 1810 | 1811 | if (errorTypes.typeVersion.length > 0) { 1812 | result.suggestions.unshift( 1813 | '🔧 RECOVERY: TypeVersion errors. Fix with:', 1814 | ' • Add "typeVersion": 1 (or latest version) to each node', 1815 | ' • Use get_node_info to check the correct version for each node type' 1816 | ); 1817 | } 1818 | 1819 | // Add general recovery workflow 1820 | if (result.errors.length > 3) { 1821 | result.suggestions.push( 1822 | '📋 SUGGESTED WORKFLOW: Too many errors detected. Try this approach:', 1823 | ' 1. Fix structural issues first (nodes array, connections object)', 1824 | ' 2. Validate node types and fix invalid ones', 1825 | ' 3. Add required typeVersion to all nodes', 1826 | ' 4. Test connections step by step', 1827 | ' 5. Use validate_node_minimal on individual nodes to verify configuration' 1828 | ); 1829 | } 1830 | } 1831 | } ``` -------------------------------------------------------------------------------- /tests/extracted-nodes-db/n8n-nodes-base__If.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "node_type": "n8n-nodes-base.If", 3 | "name": "If", 4 | "package_name": "n8n-nodes-base", 5 | "code_hash": "7910ed9177a946b76f04ca847defb81226c37c698e4cdb63913f038c6c257ee1", 6 | "code_length": 20533, 7 | "source_location": "node_modules/n8n-nodes-base/dist/nodes/If/If.node.js", 8 | "has_credentials": false, 9 | "source_code": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.If = void 0;\nconst moment_1 = __importDefault(require(\"moment\"));\nconst n8n_workflow_1 = require(\"n8n-workflow\");\nclass If {\n constructor() {\n this.description = {\n displayName: 'IF',\n name: 'if',\n icon: 'fa:map-signs',\n group: ['transform'],\n version: 1,\n description: 'Route items to different branches (true/false)',\n defaults: {\n name: 'IF',\n color: '#408000',\n },\n inputs: ['main'],\n outputs: ['main', 'main'],\n outputNames: ['true', 'false'],\n properties: [\n {\n displayName: 'Conditions',\n name: 'conditions',\n placeholder: 'Add Condition',\n type: 'fixedCollection',\n typeOptions: {\n multipleValues: true,\n sortable: true,\n },\n description: 'The type of values to compare',\n default: {},\n options: [\n {\n name: 'boolean',\n displayName: 'Boolean',\n values: [\n {\n displayName: 'Value 1',\n name: 'value1',\n type: 'boolean',\n default: false,\n description: 'The value to compare with the second one',\n },\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n options: [\n {\n name: 'Equal',\n value: 'equal',\n },\n {\n name: 'Not Equal',\n value: 'notEqual',\n },\n ],\n default: 'equal',\n description: 'Operation to decide where the the data should be mapped to',\n },\n {\n displayName: 'Value 2',\n name: 'value2',\n type: 'boolean',\n default: false,\n description: 'The value to compare with the first one',\n },\n ],\n },\n {\n name: 'dateTime',\n displayName: 'Date & Time',\n values: [\n {\n displayName: 'Value 1',\n name: 'value1',\n type: 'dateTime',\n default: '',\n description: 'The value to compare with the second one',\n },\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n options: [\n {\n name: 'Occurred After',\n value: 'after',\n },\n {\n name: 'Occurred Before',\n value: 'before',\n },\n ],\n default: 'after',\n description: 'Operation to decide where the the data should be mapped to',\n },\n {\n displayName: 'Value 2',\n name: 'value2',\n type: 'dateTime',\n default: '',\n description: 'The value to compare with the first one',\n },\n ],\n },\n {\n name: 'number',\n displayName: 'Number',\n values: [\n {\n displayName: 'Value 1',\n name: 'value1',\n type: 'number',\n default: 0,\n description: 'The value to compare with the second one',\n },\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n options: [\n {\n name: 'Smaller',\n value: 'smaller',\n },\n {\n name: 'Smaller or Equal',\n value: 'smallerEqual',\n },\n {\n name: 'Equal',\n value: 'equal',\n },\n {\n name: 'Not Equal',\n value: 'notEqual',\n },\n {\n name: 'Larger',\n value: 'larger',\n },\n {\n name: 'Larger or Equal',\n value: 'largerEqual',\n },\n {\n name: 'Is Empty',\n value: 'isEmpty',\n },\n {\n name: 'Is Not Empty',\n value: 'isNotEmpty',\n },\n ],\n default: 'smaller',\n description: 'Operation to decide where the the data should be mapped to',\n },\n {\n displayName: 'Value 2',\n name: 'value2',\n type: 'number',\n displayOptions: {\n hide: {\n operation: ['isEmpty', 'isNotEmpty'],\n },\n },\n default: 0,\n description: 'The value to compare with the first one',\n },\n ],\n },\n {\n name: 'string',\n displayName: 'String',\n values: [\n {\n displayName: 'Value 1',\n name: 'value1',\n type: 'string',\n default: '',\n description: 'The value to compare with the second one',\n },\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n options: [\n {\n name: 'Contains',\n value: 'contains',\n },\n {\n name: 'Not Contains',\n value: 'notContains',\n },\n {\n name: 'Ends With',\n value: 'endsWith',\n },\n {\n name: 'Not Ends With',\n value: 'notEndsWith',\n },\n {\n name: 'Equal',\n value: 'equal',\n },\n {\n name: 'Not Equal',\n value: 'notEqual',\n },\n {\n name: 'Regex Match',\n value: 'regex',\n },\n {\n name: 'Regex Not Match',\n value: 'notRegex',\n },\n {\n name: 'Starts With',\n value: 'startsWith',\n },\n {\n name: 'Not Starts With',\n value: 'notStartsWith',\n },\n {\n name: 'Is Empty',\n value: 'isEmpty',\n },\n {\n name: 'Is Not Empty',\n value: 'isNotEmpty',\n },\n ],\n default: 'equal',\n description: 'Operation to decide where the the data should be mapped to',\n },\n {\n displayName: 'Value 2',\n name: 'value2',\n type: 'string',\n displayOptions: {\n hide: {\n operation: ['isEmpty', 'isNotEmpty', 'regex', 'notRegex'],\n },\n },\n default: '',\n description: 'The value to compare with the first one',\n },\n {\n displayName: 'Regex',\n name: 'value2',\n type: 'string',\n displayOptions: {\n show: {\n operation: ['regex', 'notRegex'],\n },\n },\n default: '',\n placeholder: '/text/i',\n description: 'The regex which has to match',\n },\n ],\n },\n ],\n },\n {\n displayName: 'Combine',\n name: 'combineOperation',\n type: 'options',\n options: [\n {\n name: 'ALL',\n description: 'Only if all conditions are met it goes into \"true\" branch',\n value: 'all',\n },\n {\n name: 'ANY',\n description: 'If any of the conditions is met it goes into \"true\" branch',\n value: 'any',\n },\n ],\n default: 'all',\n description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet',\n },\n ],\n };\n }\n async execute() {\n const returnDataTrue = [];\n const returnDataFalse = [];\n const items = this.getInputData();\n let item;\n let combineOperation;\n const isDateObject = (value) => Object.prototype.toString.call(value) === '[object Date]';\n const isDateInvalid = (value) => (value === null || value === void 0 ? void 0 : value.toString()) === 'Invalid Date';\n const compareOperationFunctions = {\n after: (value1, value2) => (value1 || 0) > (value2 || 0),\n before: (value1, value2) => (value1 || 0) < (value2 || 0),\n contains: (value1, value2) => (value1 || '').toString().includes((value2 || '').toString()),\n notContains: (value1, value2) => !(value1 || '').toString().includes((value2 || '').toString()),\n endsWith: (value1, value2) => value1.endsWith(value2),\n notEndsWith: (value1, value2) => !value1.endsWith(value2),\n equal: (value1, value2) => value1 === value2,\n notEqual: (value1, value2) => value1 !== value2,\n larger: (value1, value2) => (value1 || 0) > (value2 || 0),\n largerEqual: (value1, value2) => (value1 || 0) >= (value2 || 0),\n smaller: (value1, value2) => (value1 || 0) < (value2 || 0),\n smallerEqual: (value1, value2) => (value1 || 0) <= (value2 || 0),\n startsWith: (value1, value2) => value1.startsWith(value2),\n notStartsWith: (value1, value2) => !value1.startsWith(value2),\n isEmpty: (value1) => [undefined, null, '', NaN].includes(value1) ||\n (typeof value1 === 'object' && value1 !== null && !isDateObject(value1)\n ? Object.entries(value1).length === 0\n : false) ||\n (isDateObject(value1) && isDateInvalid(value1)),\n isNotEmpty: (value1) => !([undefined, null, '', NaN].includes(value1) ||\n (typeof value1 === 'object' && value1 !== null && !isDateObject(value1)\n ? Object.entries(value1).length === 0\n : false) ||\n (isDateObject(value1) && isDateInvalid(value1))),\n regex: (value1, value2) => {\n const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));\n let regex;\n if (!regexMatch) {\n regex = new RegExp((value2 || '').toString());\n }\n else if (regexMatch.length === 1) {\n regex = new RegExp(regexMatch[1]);\n }\n else {\n regex = new RegExp(regexMatch[1], regexMatch[2]);\n }\n return !!(value1 || '').toString().match(regex);\n },\n notRegex: (value1, value2) => {\n const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));\n let regex;\n if (!regexMatch) {\n regex = new RegExp((value2 || '').toString());\n }\n else if (regexMatch.length === 1) {\n regex = new RegExp(regexMatch[1]);\n }\n else {\n regex = new RegExp(regexMatch[1], regexMatch[2]);\n }\n return !(value1 || '').toString().match(regex);\n },\n };\n const convertDateTime = (value) => {\n let returnValue = undefined;\n if (typeof value === 'string') {\n returnValue = new Date(value).getTime();\n }\n else if (typeof value === 'number') {\n returnValue = value;\n }\n if (moment_1.default.isMoment(value)) {\n returnValue = value.unix();\n }\n if (value instanceof Date) {\n returnValue = value.getTime();\n }\n if (returnValue === undefined || isNaN(returnValue)) {\n throw new n8n_workflow_1.NodeOperationError(this.getNode(), `The value \"${value}\" is not a valid DateTime.`);\n }\n return returnValue;\n };\n const dataTypes = ['boolean', 'dateTime', 'number', 'string'];\n let dataType;\n let compareOperationResult;\n let value1, value2;\n itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {\n item = items[itemIndex];\n let compareData;\n combineOperation = this.getNodeParameter('combineOperation', itemIndex);\n for (dataType of dataTypes) {\n for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, [])) {\n value1 = compareData.value1;\n value2 = compareData.value2;\n if (dataType === 'dateTime') {\n value1 = convertDateTime(value1);\n value2 = convertDateTime(value2);\n }\n compareOperationResult = compareOperationFunctions[compareData.operation](value1, value2);\n if (compareOperationResult && combineOperation === 'any') {\n returnDataTrue.push(item);\n continue itemLoop;\n }\n else if (!compareOperationResult && combineOperation === 'all') {\n returnDataFalse.push(item);\n continue itemLoop;\n }\n }\n }\n if (item.pairedItem === undefined) {\n item.pairedItem = [{ item: itemIndex }];\n }\n if (combineOperation === 'all') {\n returnDataTrue.push(item);\n }\n else {\n returnDataFalse.push(item);\n }\n }\n return [returnDataTrue, returnDataFalse];\n }\n}\nexports.If = If;\n//# sourceMappingURL=If.node.js.map", 10 | "package_info": { 11 | "name": "n8n-nodes-base", 12 | "version": "1.14.1", 13 | "description": "Base nodes of n8n", 14 | "license": "SEE LICENSE IN LICENSE.md", 15 | "homepage": "https://n8n.io", 16 | "author": { 17 | "name": "Jan Oberhauser", 18 | "email": "[email protected]" 19 | }, 20 | "main": "index.js", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/n8n-io/n8n.git" 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "n8n": { 29 | "credentials": [ 30 | "dist/credentials/ActionNetworkApi.credentials.js", 31 | "dist/credentials/ActiveCampaignApi.credentials.js", 32 | "dist/credentials/AcuitySchedulingApi.credentials.js", 33 | "dist/credentials/AcuitySchedulingOAuth2Api.credentials.js", 34 | "dist/credentials/AdaloApi.credentials.js", 35 | "dist/credentials/AffinityApi.credentials.js", 36 | "dist/credentials/AgileCrmApi.credentials.js", 37 | "dist/credentials/AirtableApi.credentials.js", 38 | "dist/credentials/AirtableOAuth2Api.credentials.js", 39 | "dist/credentials/AirtableTokenApi.credentials.js", 40 | "dist/credentials/AlienVaultApi.credentials.js", 41 | "dist/credentials/Amqp.credentials.js", 42 | "dist/credentials/ApiTemplateIoApi.credentials.js", 43 | "dist/credentials/AsanaApi.credentials.js", 44 | "dist/credentials/AsanaOAuth2Api.credentials.js", 45 | "dist/credentials/Auth0ManagementApi.credentials.js", 46 | "dist/credentials/AutomizyApi.credentials.js", 47 | "dist/credentials/AutopilotApi.credentials.js", 48 | "dist/credentials/Aws.credentials.js", 49 | "dist/credentials/BambooHrApi.credentials.js", 50 | "dist/credentials/BannerbearApi.credentials.js", 51 | "dist/credentials/BaserowApi.credentials.js", 52 | "dist/credentials/BeeminderApi.credentials.js", 53 | "dist/credentials/BitbucketApi.credentials.js", 54 | "dist/credentials/BitlyApi.credentials.js", 55 | "dist/credentials/BitlyOAuth2Api.credentials.js", 56 | "dist/credentials/BitwardenApi.credentials.js", 57 | "dist/credentials/BoxOAuth2Api.credentials.js", 58 | "dist/credentials/BrandfetchApi.credentials.js", 59 | "dist/credentials/BubbleApi.credentials.js", 60 | "dist/credentials/CalApi.credentials.js", 61 | "dist/credentials/CalendlyApi.credentials.js", 62 | "dist/credentials/CarbonBlackApi.credentials.js", 63 | "dist/credentials/ChargebeeApi.credentials.js", 64 | "dist/credentials/CircleCiApi.credentials.js", 65 | "dist/credentials/CiscoMerakiApi.credentials.js", 66 | "dist/credentials/CiscoSecureEndpointApi.credentials.js", 67 | "dist/credentials/CiscoWebexOAuth2Api.credentials.js", 68 | "dist/credentials/CiscoUmbrellaApi.credentials.js", 69 | "dist/credentials/CitrixAdcApi.credentials.js", 70 | "dist/credentials/CloudflareApi.credentials.js", 71 | "dist/credentials/ClearbitApi.credentials.js", 72 | "dist/credentials/ClickUpApi.credentials.js", 73 | "dist/credentials/ClickUpOAuth2Api.credentials.js", 74 | "dist/credentials/ClockifyApi.credentials.js", 75 | "dist/credentials/CockpitApi.credentials.js", 76 | "dist/credentials/CodaApi.credentials.js", 77 | "dist/credentials/ContentfulApi.credentials.js", 78 | "dist/credentials/ConvertKitApi.credentials.js", 79 | "dist/credentials/CopperApi.credentials.js", 80 | "dist/credentials/CortexApi.credentials.js", 81 | "dist/credentials/CrateDb.credentials.js", 82 | "dist/credentials/CrowdStrikeOAuth2Api.credentials.js", 83 | "dist/credentials/CrowdDevApi.credentials.js", 84 | "dist/credentials/CustomerIoApi.credentials.js", 85 | "dist/credentials/DeepLApi.credentials.js", 86 | "dist/credentials/DemioApi.credentials.js", 87 | "dist/credentials/DhlApi.credentials.js", 88 | "dist/credentials/DiscourseApi.credentials.js", 89 | "dist/credentials/DisqusApi.credentials.js", 90 | "dist/credentials/DriftApi.credentials.js", 91 | "dist/credentials/DriftOAuth2Api.credentials.js", 92 | "dist/credentials/DropboxApi.credentials.js", 93 | "dist/credentials/DropboxOAuth2Api.credentials.js", 94 | "dist/credentials/DropcontactApi.credentials.js", 95 | "dist/credentials/EgoiApi.credentials.js", 96 | "dist/credentials/ElasticsearchApi.credentials.js", 97 | "dist/credentials/ElasticSecurityApi.credentials.js", 98 | "dist/credentials/EmeliaApi.credentials.js", 99 | "dist/credentials/ERPNextApi.credentials.js", 100 | "dist/credentials/EventbriteApi.credentials.js", 101 | "dist/credentials/EventbriteOAuth2Api.credentials.js", 102 | "dist/credentials/F5BigIpApi.credentials.js", 103 | "dist/credentials/FacebookGraphApi.credentials.js", 104 | "dist/credentials/FacebookGraphAppApi.credentials.js", 105 | "dist/credentials/FacebookLeadAdsOAuth2Api.credentials.js", 106 | "dist/credentials/FigmaApi.credentials.js", 107 | "dist/credentials/FileMaker.credentials.js", 108 | "dist/credentials/FlowApi.credentials.js", 109 | "dist/credentials/FormIoApi.credentials.js", 110 | "dist/credentials/FormstackApi.credentials.js", 111 | "dist/credentials/FormstackOAuth2Api.credentials.js", 112 | "dist/credentials/FortiGateApi.credentials.js", 113 | "dist/credentials/FreshdeskApi.credentials.js", 114 | "dist/credentials/FreshserviceApi.credentials.js", 115 | "dist/credentials/FreshworksCrmApi.credentials.js", 116 | "dist/credentials/Ftp.credentials.js", 117 | "dist/credentials/GetResponseApi.credentials.js", 118 | "dist/credentials/GetResponseOAuth2Api.credentials.js", 119 | "dist/credentials/GhostAdminApi.credentials.js", 120 | "dist/credentials/GhostContentApi.credentials.js", 121 | "dist/credentials/GithubApi.credentials.js", 122 | "dist/credentials/GithubOAuth2Api.credentials.js", 123 | "dist/credentials/GitlabApi.credentials.js", 124 | "dist/credentials/GitlabOAuth2Api.credentials.js", 125 | "dist/credentials/GitPassword.credentials.js", 126 | "dist/credentials/GmailOAuth2Api.credentials.js", 127 | "dist/credentials/GoogleAdsOAuth2Api.credentials.js", 128 | "dist/credentials/GoogleAnalyticsOAuth2Api.credentials.js", 129 | "dist/credentials/GoogleApi.credentials.js", 130 | "dist/credentials/GoogleBigQueryOAuth2Api.credentials.js", 131 | "dist/credentials/GoogleBooksOAuth2Api.credentials.js", 132 | "dist/credentials/GoogleCalendarOAuth2Api.credentials.js", 133 | "dist/credentials/GoogleCloudNaturalLanguageOAuth2Api.credentials.js", 134 | "dist/credentials/GoogleCloudStorageOAuth2Api.credentials.js", 135 | "dist/credentials/GoogleContactsOAuth2Api.credentials.js", 136 | "dist/credentials/GoogleDocsOAuth2Api.credentials.js", 137 | "dist/credentials/GoogleDriveOAuth2Api.credentials.js", 138 | "dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js", 139 | "dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js", 140 | "dist/credentials/GoogleOAuth2Api.credentials.js", 141 | "dist/credentials/GooglePerspectiveOAuth2Api.credentials.js", 142 | "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", 143 | "dist/credentials/GoogleSheetsTriggerOAuth2Api.credentials.js", 144 | "dist/credentials/GoogleSlidesOAuth2Api.credentials.js", 145 | "dist/credentials/GoogleTasksOAuth2Api.credentials.js", 146 | "dist/credentials/GoogleTranslateOAuth2Api.credentials.js", 147 | "dist/credentials/GotifyApi.credentials.js", 148 | "dist/credentials/GoToWebinarOAuth2Api.credentials.js", 149 | "dist/credentials/GristApi.credentials.js", 150 | "dist/credentials/GrafanaApi.credentials.js", 151 | "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", 152 | "dist/credentials/GumroadApi.credentials.js", 153 | "dist/credentials/HaloPSAApi.credentials.js", 154 | "dist/credentials/HarvestApi.credentials.js", 155 | "dist/credentials/HarvestOAuth2Api.credentials.js", 156 | "dist/credentials/HelpScoutOAuth2Api.credentials.js", 157 | "dist/credentials/HighLevelApi.credentials.js", 158 | "dist/credentials/HomeAssistantApi.credentials.js", 159 | "dist/credentials/HttpBasicAuth.credentials.js", 160 | "dist/credentials/HttpDigestAuth.credentials.js", 161 | "dist/credentials/HttpHeaderAuth.credentials.js", 162 | "dist/credentials/HttpCustomAuth.credentials.js", 163 | "dist/credentials/HttpQueryAuth.credentials.js", 164 | "dist/credentials/HubspotApi.credentials.js", 165 | "dist/credentials/HubspotAppToken.credentials.js", 166 | "dist/credentials/HubspotDeveloperApi.credentials.js", 167 | "dist/credentials/HubspotOAuth2Api.credentials.js", 168 | "dist/credentials/HumanticAiApi.credentials.js", 169 | "dist/credentials/HunterApi.credentials.js", 170 | "dist/credentials/HybridAnalysisApi.credentials.js", 171 | "dist/credentials/Imap.credentials.js", 172 | "dist/credentials/ImpervaWafApi.credentials.js", 173 | "dist/credentials/IntercomApi.credentials.js", 174 | "dist/credentials/InvoiceNinjaApi.credentials.js", 175 | "dist/credentials/IterableApi.credentials.js", 176 | "dist/credentials/JenkinsApi.credentials.js", 177 | "dist/credentials/JiraSoftwareCloudApi.credentials.js", 178 | "dist/credentials/JiraSoftwareServerApi.credentials.js", 179 | "dist/credentials/JotFormApi.credentials.js", 180 | "dist/credentials/Kafka.credentials.js", 181 | "dist/credentials/KeapOAuth2Api.credentials.js", 182 | "dist/credentials/KibanaApi.credentials.js", 183 | "dist/credentials/KitemakerApi.credentials.js", 184 | "dist/credentials/KoBoToolboxApi.credentials.js", 185 | "dist/credentials/Ldap.credentials.js", 186 | "dist/credentials/LemlistApi.credentials.js", 187 | "dist/credentials/LinearApi.credentials.js", 188 | "dist/credentials/LinearOAuth2Api.credentials.js", 189 | "dist/credentials/LineNotifyOAuth2Api.credentials.js", 190 | "dist/credentials/LingvaNexApi.credentials.js", 191 | "dist/credentials/LinkedInOAuth2Api.credentials.js", 192 | "dist/credentials/LoneScaleApi.credentials.js", 193 | "dist/credentials/Magento2Api.credentials.js", 194 | "dist/credentials/MailcheckApi.credentials.js", 195 | "dist/credentials/MailchimpApi.credentials.js", 196 | "dist/credentials/MailchimpOAuth2Api.credentials.js", 197 | "dist/credentials/MailerLiteApi.credentials.js", 198 | "dist/credentials/MailgunApi.credentials.js", 199 | "dist/credentials/MailjetEmailApi.credentials.js", 200 | "dist/credentials/MailjetSmsApi.credentials.js", 201 | "dist/credentials/MandrillApi.credentials.js", 202 | "dist/credentials/MarketstackApi.credentials.js", 203 | "dist/credentials/MatrixApi.credentials.js", 204 | "dist/credentials/MattermostApi.credentials.js", 205 | "dist/credentials/MauticApi.credentials.js", 206 | "dist/credentials/MauticOAuth2Api.credentials.js", 207 | "dist/credentials/MediumApi.credentials.js", 208 | "dist/credentials/MediumOAuth2Api.credentials.js", 209 | "dist/credentials/MetabaseApi.credentials.js", 210 | "dist/credentials/MessageBirdApi.credentials.js", 211 | "dist/credentials/MetabaseApi.credentials.js", 212 | "dist/credentials/MicrosoftDynamicsOAuth2Api.credentials.js", 213 | "dist/credentials/MicrosoftEntraOAuth2Api.credentials.js", 214 | "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js", 215 | "dist/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.js", 216 | "dist/credentials/MicrosoftOAuth2Api.credentials.js", 217 | "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js", 218 | "dist/credentials/MicrosoftOutlookOAuth2Api.credentials.js", 219 | "dist/credentials/MicrosoftSql.credentials.js", 220 | "dist/credentials/MicrosoftTeamsOAuth2Api.credentials.js", 221 | "dist/credentials/MicrosoftToDoOAuth2Api.credentials.js", 222 | "dist/credentials/MindeeInvoiceApi.credentials.js", 223 | "dist/credentials/MindeeReceiptApi.credentials.js", 224 | "dist/credentials/MispApi.credentials.js", 225 | "dist/credentials/MistApi.credentials.js", 226 | "dist/credentials/MoceanApi.credentials.js", 227 | "dist/credentials/MondayComApi.credentials.js", 228 | "dist/credentials/MondayComOAuth2Api.credentials.js", 229 | "dist/credentials/MongoDb.credentials.js", 230 | "dist/credentials/MonicaCrmApi.credentials.js", 231 | "dist/credentials/Mqtt.credentials.js", 232 | "dist/credentials/Msg91Api.credentials.js", 233 | "dist/credentials/MySql.credentials.js", 234 | "dist/credentials/N8nApi.credentials.js", 235 | "dist/credentials/NasaApi.credentials.js", 236 | "dist/credentials/NetlifyApi.credentials.js", 237 | "dist/credentials/NextCloudApi.credentials.js", 238 | "dist/credentials/NextCloudOAuth2Api.credentials.js", 239 | "dist/credentials/NocoDb.credentials.js", 240 | "dist/credentials/NocoDbApiToken.credentials.js", 241 | "dist/credentials/NotionApi.credentials.js", 242 | "dist/credentials/NotionOAuth2Api.credentials.js", 243 | "dist/credentials/NpmApi.credentials.js", 244 | "dist/credentials/OAuth1Api.credentials.js", 245 | "dist/credentials/OAuth2Api.credentials.js", 246 | "dist/credentials/OdooApi.credentials.js", 247 | "dist/credentials/OktaApi.credentials.js", 248 | "dist/credentials/OneSimpleApi.credentials.js", 249 | "dist/credentials/OnfleetApi.credentials.js", 250 | "dist/credentials/OpenAiApi.credentials.js", 251 | "dist/credentials/OpenCTIApi.credentials.js", 252 | "dist/credentials/OpenWeatherMapApi.credentials.js", 253 | "dist/credentials/OrbitApi.credentials.js", 254 | "dist/credentials/OuraApi.credentials.js", 255 | "dist/credentials/PaddleApi.credentials.js", 256 | "dist/credentials/PagerDutyApi.credentials.js", 257 | "dist/credentials/PagerDutyOAuth2Api.credentials.js", 258 | "dist/credentials/PayPalApi.credentials.js", 259 | "dist/credentials/PeekalinkApi.credentials.js", 260 | "dist/credentials/PhantombusterApi.credentials.js", 261 | "dist/credentials/PhilipsHueOAuth2Api.credentials.js", 262 | "dist/credentials/PipedriveApi.credentials.js", 263 | "dist/credentials/PipedriveOAuth2Api.credentials.js", 264 | "dist/credentials/PlivoApi.credentials.js", 265 | "dist/credentials/Postgres.credentials.js", 266 | "dist/credentials/PostHogApi.credentials.js", 267 | "dist/credentials/PostmarkApi.credentials.js", 268 | "dist/credentials/ProfitWellApi.credentials.js", 269 | "dist/credentials/PushbulletOAuth2Api.credentials.js", 270 | "dist/credentials/PushcutApi.credentials.js", 271 | "dist/credentials/PushoverApi.credentials.js", 272 | "dist/credentials/QRadarApi.credentials.js", 273 | "dist/credentials/QualysApi.credentials.js", 274 | "dist/credentials/QuestDb.credentials.js", 275 | "dist/credentials/QuickBaseApi.credentials.js", 276 | "dist/credentials/QuickBooksOAuth2Api.credentials.js", 277 | "dist/credentials/RabbitMQ.credentials.js", 278 | "dist/credentials/RaindropOAuth2Api.credentials.js", 279 | "dist/credentials/RecordedFutureApi.credentials.js", 280 | "dist/credentials/RedditOAuth2Api.credentials.js", 281 | "dist/credentials/Redis.credentials.js", 282 | "dist/credentials/RocketchatApi.credentials.js", 283 | "dist/credentials/RundeckApi.credentials.js", 284 | "dist/credentials/S3.credentials.js", 285 | "dist/credentials/SalesforceJwtApi.credentials.js", 286 | "dist/credentials/SalesforceOAuth2Api.credentials.js", 287 | "dist/credentials/SalesmateApi.credentials.js", 288 | "dist/credentials/SeaTableApi.credentials.js", 289 | "dist/credentials/SecurityScorecardApi.credentials.js", 290 | "dist/credentials/SegmentApi.credentials.js", 291 | "dist/credentials/SekoiaApi.credentials.js", 292 | "dist/credentials/SendGridApi.credentials.js", 293 | "dist/credentials/BrevoApi.credentials.js", 294 | "dist/credentials/SendyApi.credentials.js", 295 | "dist/credentials/SentryIoApi.credentials.js", 296 | "dist/credentials/SentryIoOAuth2Api.credentials.js", 297 | "dist/credentials/SentryIoServerApi.credentials.js", 298 | "dist/credentials/ServiceNowOAuth2Api.credentials.js", 299 | "dist/credentials/ServiceNowBasicApi.credentials.js", 300 | "dist/credentials/Sftp.credentials.js", 301 | "dist/credentials/ShopifyApi.credentials.js", 302 | "dist/credentials/ShopifyAccessTokenApi.credentials.js", 303 | "dist/credentials/ShopifyOAuth2Api.credentials.js", 304 | "dist/credentials/Signl4Api.credentials.js", 305 | "dist/credentials/SlackApi.credentials.js", 306 | "dist/credentials/SlackOAuth2Api.credentials.js", 307 | "dist/credentials/Sms77Api.credentials.js", 308 | "dist/credentials/Smtp.credentials.js", 309 | "dist/credentials/Snowflake.credentials.js", 310 | "dist/credentials/SplunkApi.credentials.js", 311 | "dist/credentials/SpontitApi.credentials.js", 312 | "dist/credentials/SpotifyOAuth2Api.credentials.js", 313 | "dist/credentials/ShufflerApi.credentials.js", 314 | "dist/credentials/SshPassword.credentials.js", 315 | "dist/credentials/SshPrivateKey.credentials.js", 316 | "dist/credentials/StackbyApi.credentials.js", 317 | "dist/credentials/StoryblokContentApi.credentials.js", 318 | "dist/credentials/StoryblokManagementApi.credentials.js", 319 | "dist/credentials/StrapiApi.credentials.js", 320 | "dist/credentials/StrapiTokenApi.credentials.js", 321 | "dist/credentials/StravaOAuth2Api.credentials.js", 322 | "dist/credentials/StripeApi.credentials.js", 323 | "dist/credentials/SupabaseApi.credentials.js", 324 | "dist/credentials/SurveyMonkeyApi.credentials.js", 325 | "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", 326 | "dist/credentials/SyncroMspApi.credentials.js", 327 | "dist/credentials/TaigaApi.credentials.js", 328 | "dist/credentials/TapfiliateApi.credentials.js", 329 | "dist/credentials/TelegramApi.credentials.js", 330 | "dist/credentials/TheHiveProjectApi.credentials.js", 331 | "dist/credentials/TheHiveApi.credentials.js", 332 | "dist/credentials/TimescaleDb.credentials.js", 333 | "dist/credentials/TodoistApi.credentials.js", 334 | "dist/credentials/TodoistOAuth2Api.credentials.js", 335 | "dist/credentials/TogglApi.credentials.js", 336 | "dist/credentials/TotpApi.credentials.js", 337 | "dist/credentials/TravisCiApi.credentials.js", 338 | "dist/credentials/TrellixEpoApi.credentials.js", 339 | "dist/credentials/TrelloApi.credentials.js", 340 | "dist/credentials/TwakeCloudApi.credentials.js", 341 | "dist/credentials/TwakeServerApi.credentials.js", 342 | "dist/credentials/TwilioApi.credentials.js", 343 | "dist/credentials/TwistOAuth2Api.credentials.js", 344 | "dist/credentials/TwitterOAuth1Api.credentials.js", 345 | "dist/credentials/TwitterOAuth2Api.credentials.js", 346 | "dist/credentials/TypeformApi.credentials.js", 347 | "dist/credentials/TypeformOAuth2Api.credentials.js", 348 | "dist/credentials/UnleashedSoftwareApi.credentials.js", 349 | "dist/credentials/UpleadApi.credentials.js", 350 | "dist/credentials/UProcApi.credentials.js", 351 | "dist/credentials/UptimeRobotApi.credentials.js", 352 | "dist/credentials/UrlScanIoApi.credentials.js", 353 | "dist/credentials/VeroApi.credentials.js", 354 | "dist/credentials/VirusTotalApi.credentials.js", 355 | "dist/credentials/VonageApi.credentials.js", 356 | "dist/credentials/VenafiTlsProtectCloudApi.credentials.js", 357 | "dist/credentials/VenafiTlsProtectDatacenterApi.credentials.js", 358 | "dist/credentials/WebflowApi.credentials.js", 359 | "dist/credentials/WebflowOAuth2Api.credentials.js", 360 | "dist/credentials/WekanApi.credentials.js", 361 | "dist/credentials/WhatsAppApi.credentials.js", 362 | "dist/credentials/WiseApi.credentials.js", 363 | "dist/credentials/WooCommerceApi.credentials.js", 364 | "dist/credentials/WordpressApi.credentials.js", 365 | "dist/credentials/WorkableApi.credentials.js", 366 | "dist/credentials/WufooApi.credentials.js", 367 | "dist/credentials/XeroOAuth2Api.credentials.js", 368 | "dist/credentials/YourlsApi.credentials.js", 369 | "dist/credentials/YouTubeOAuth2Api.credentials.js", 370 | "dist/credentials/ZammadBasicAuthApi.credentials.js", 371 | "dist/credentials/ZammadTokenAuthApi.credentials.js", 372 | "dist/credentials/ZendeskApi.credentials.js", 373 | "dist/credentials/ZendeskOAuth2Api.credentials.js", 374 | "dist/credentials/ZohoOAuth2Api.credentials.js", 375 | "dist/credentials/ZoomApi.credentials.js", 376 | "dist/credentials/ZoomOAuth2Api.credentials.js", 377 | "dist/credentials/ZscalerZiaApi.credentials.js", 378 | "dist/credentials/ZulipApi.credentials.js" 379 | ], 380 | "nodes": [ 381 | "dist/nodes/ActionNetwork/ActionNetwork.node.js", 382 | "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", 383 | "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", 384 | "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", 385 | "dist/nodes/Adalo/Adalo.node.js", 386 | "dist/nodes/Affinity/Affinity.node.js", 387 | "dist/nodes/Affinity/AffinityTrigger.node.js", 388 | "dist/nodes/AgileCrm/AgileCrm.node.js", 389 | "dist/nodes/Airtable/Airtable.node.js", 390 | "dist/nodes/Airtable/AirtableTrigger.node.js", 391 | "dist/nodes/Amqp/Amqp.node.js", 392 | "dist/nodes/Amqp/AmqpTrigger.node.js", 393 | "dist/nodes/ApiTemplateIo/ApiTemplateIo.node.js", 394 | "dist/nodes/Asana/Asana.node.js", 395 | "dist/nodes/Asana/AsanaTrigger.node.js", 396 | "dist/nodes/Automizy/Automizy.node.js", 397 | "dist/nodes/Autopilot/Autopilot.node.js", 398 | "dist/nodes/Autopilot/AutopilotTrigger.node.js", 399 | "dist/nodes/Aws/AwsLambda.node.js", 400 | "dist/nodes/Aws/AwsSns.node.js", 401 | "dist/nodes/Aws/AwsSnsTrigger.node.js", 402 | "dist/nodes/Aws/CertificateManager/AwsCertificateManager.node.js", 403 | "dist/nodes/Aws/Comprehend/AwsComprehend.node.js", 404 | "dist/nodes/Aws/DynamoDB/AwsDynamoDB.node.js", 405 | "dist/nodes/Aws/ELB/AwsElb.node.js", 406 | "dist/nodes/Aws/Rekognition/AwsRekognition.node.js", 407 | "dist/nodes/Aws/S3/AwsS3.node.js", 408 | "dist/nodes/Aws/SES/AwsSes.node.js", 409 | "dist/nodes/Aws/SQS/AwsSqs.node.js", 410 | "dist/nodes/Aws/Textract/AwsTextract.node.js", 411 | "dist/nodes/Aws/Transcribe/AwsTranscribe.node.js", 412 | "dist/nodes/BambooHr/BambooHr.node.js", 413 | "dist/nodes/Bannerbear/Bannerbear.node.js", 414 | "dist/nodes/Baserow/Baserow.node.js", 415 | "dist/nodes/Beeminder/Beeminder.node.js", 416 | "dist/nodes/Bitbucket/BitbucketTrigger.node.js", 417 | "dist/nodes/Bitly/Bitly.node.js", 418 | "dist/nodes/Bitwarden/Bitwarden.node.js", 419 | "dist/nodes/Box/Box.node.js", 420 | "dist/nodes/Box/BoxTrigger.node.js", 421 | "dist/nodes/Brandfetch/Brandfetch.node.js", 422 | "dist/nodes/Bubble/Bubble.node.js", 423 | "dist/nodes/Cal/CalTrigger.node.js", 424 | "dist/nodes/Calendly/CalendlyTrigger.node.js", 425 | "dist/nodes/Chargebee/Chargebee.node.js", 426 | "dist/nodes/Chargebee/ChargebeeTrigger.node.js", 427 | "dist/nodes/CircleCi/CircleCi.node.js", 428 | "dist/nodes/Cisco/Webex/CiscoWebex.node.js", 429 | "dist/nodes/Citrix/ADC/CitrixAdc.node.js", 430 | "dist/nodes/Cisco/Webex/CiscoWebexTrigger.node.js", 431 | "dist/nodes/Cloudflare/Cloudflare.node.js", 432 | "dist/nodes/Clearbit/Clearbit.node.js", 433 | "dist/nodes/ClickUp/ClickUp.node.js", 434 | "dist/nodes/ClickUp/ClickUpTrigger.node.js", 435 | "dist/nodes/Clockify/Clockify.node.js", 436 | "dist/nodes/Clockify/ClockifyTrigger.node.js", 437 | "dist/nodes/Cockpit/Cockpit.node.js", 438 | "dist/nodes/Coda/Coda.node.js", 439 | "dist/nodes/Code/Code.node.js", 440 | "dist/nodes/CoinGecko/CoinGecko.node.js", 441 | "dist/nodes/CompareDatasets/CompareDatasets.node.js", 442 | "dist/nodes/Compression/Compression.node.js", 443 | "dist/nodes/Contentful/Contentful.node.js", 444 | "dist/nodes/ConvertKit/ConvertKit.node.js", 445 | "dist/nodes/ConvertKit/ConvertKitTrigger.node.js", 446 | "dist/nodes/Copper/Copper.node.js", 447 | "dist/nodes/Copper/CopperTrigger.node.js", 448 | "dist/nodes/Cortex/Cortex.node.js", 449 | "dist/nodes/CrateDb/CrateDb.node.js", 450 | "dist/nodes/Cron/Cron.node.js", 451 | "dist/nodes/CrowdDev/CrowdDev.node.js", 452 | "dist/nodes/CrowdDev/CrowdDevTrigger.node.js", 453 | "dist/nodes/Crypto/Crypto.node.js", 454 | "dist/nodes/CustomerIo/CustomerIo.node.js", 455 | "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", 456 | "dist/nodes/DateTime/DateTime.node.js", 457 | "dist/nodes/DebugHelper/DebugHelper.node.js", 458 | "dist/nodes/DeepL/DeepL.node.js", 459 | "dist/nodes/Demio/Demio.node.js", 460 | "dist/nodes/Dhl/Dhl.node.js", 461 | "dist/nodes/Discord/Discord.node.js", 462 | "dist/nodes/Discourse/Discourse.node.js", 463 | "dist/nodes/Disqus/Disqus.node.js", 464 | "dist/nodes/Drift/Drift.node.js", 465 | "dist/nodes/Dropbox/Dropbox.node.js", 466 | "dist/nodes/Dropcontact/Dropcontact.node.js", 467 | "dist/nodes/EditImage/EditImage.node.js", 468 | "dist/nodes/E2eTest/E2eTest.node.js", 469 | "dist/nodes/Egoi/Egoi.node.js", 470 | "dist/nodes/Elastic/Elasticsearch/Elasticsearch.node.js", 471 | "dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js", 472 | "dist/nodes/EmailReadImap/EmailReadImap.node.js", 473 | "dist/nodes/EmailSend/EmailSend.node.js", 474 | "dist/nodes/Emelia/Emelia.node.js", 475 | "dist/nodes/Emelia/EmeliaTrigger.node.js", 476 | "dist/nodes/ERPNext/ERPNext.node.js", 477 | "dist/nodes/ErrorTrigger/ErrorTrigger.node.js", 478 | "dist/nodes/Eventbrite/EventbriteTrigger.node.js", 479 | "dist/nodes/ExecuteCommand/ExecuteCommand.node.js", 480 | "dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js", 481 | "dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js", 482 | "dist/nodes/ExecutionData/ExecutionData.node.js", 483 | "dist/nodes/Facebook/FacebookGraphApi.node.js", 484 | "dist/nodes/Facebook/FacebookTrigger.node.js", 485 | "dist/nodes/FacebookLeadAds/FacebookLeadAdsTrigger.node.js", 486 | "dist/nodes/Figma/FigmaTrigger.node.js", 487 | "dist/nodes/FileMaker/FileMaker.node.js", 488 | "dist/nodes/Filter/Filter.node.js", 489 | "dist/nodes/Flow/Flow.node.js", 490 | "dist/nodes/Flow/FlowTrigger.node.js", 491 | "dist/nodes/Form/FormTrigger.node.js", 492 | "dist/nodes/FormIo/FormIoTrigger.node.js", 493 | "dist/nodes/Formstack/FormstackTrigger.node.js", 494 | "dist/nodes/Freshdesk/Freshdesk.node.js", 495 | "dist/nodes/Freshservice/Freshservice.node.js", 496 | "dist/nodes/FreshworksCrm/FreshworksCrm.node.js", 497 | "dist/nodes/Ftp/Ftp.node.js", 498 | "dist/nodes/Function/Function.node.js", 499 | "dist/nodes/FunctionItem/FunctionItem.node.js", 500 | "dist/nodes/GetResponse/GetResponse.node.js", 501 | "dist/nodes/GetResponse/GetResponseTrigger.node.js", 502 | "dist/nodes/Ghost/Ghost.node.js", 503 | "dist/nodes/Git/Git.node.js", 504 | "dist/nodes/Github/Github.node.js", 505 | "dist/nodes/Github/GithubTrigger.node.js", 506 | "dist/nodes/Gitlab/Gitlab.node.js", 507 | "dist/nodes/Gitlab/GitlabTrigger.node.js", 508 | "dist/nodes/Google/Ads/GoogleAds.node.js", 509 | "dist/nodes/Google/Analytics/GoogleAnalytics.node.js", 510 | "dist/nodes/Google/BigQuery/GoogleBigQuery.node.js", 511 | "dist/nodes/Google/Books/GoogleBooks.node.js", 512 | "dist/nodes/Google/Calendar/GoogleCalendar.node.js", 513 | "dist/nodes/Google/Calendar/GoogleCalendarTrigger.node.js", 514 | "dist/nodes/Google/Chat/GoogleChat.node.js", 515 | "dist/nodes/Google/CloudNaturalLanguage/GoogleCloudNaturalLanguage.node.js", 516 | "dist/nodes/Google/CloudStorage/GoogleCloudStorage.node.js", 517 | "dist/nodes/Google/Contacts/GoogleContacts.node.js", 518 | "dist/nodes/Google/Docs/GoogleDocs.node.js", 519 | "dist/nodes/Google/Drive/GoogleDrive.node.js", 520 | "dist/nodes/Google/Drive/GoogleDriveTrigger.node.js", 521 | "dist/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.js", 522 | "dist/nodes/Google/Firebase/RealtimeDatabase/GoogleFirebaseRealtimeDatabase.node.js", 523 | "dist/nodes/Google/Gmail/Gmail.node.js", 524 | "dist/nodes/Google/Gmail/GmailTrigger.node.js", 525 | "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", 526 | "dist/nodes/Google/Perspective/GooglePerspective.node.js", 527 | "dist/nodes/Google/Sheet/GoogleSheets.node.js", 528 | "dist/nodes/Google/Sheet/GoogleSheetsTrigger.node.js", 529 | "dist/nodes/Google/Slides/GoogleSlides.node.js", 530 | "dist/nodes/Google/Task/GoogleTasks.node.js", 531 | "dist/nodes/Google/Translate/GoogleTranslate.node.js", 532 | "dist/nodes/Google/YouTube/YouTube.node.js", 533 | "dist/nodes/Gotify/Gotify.node.js", 534 | "dist/nodes/GoToWebinar/GoToWebinar.node.js", 535 | "dist/nodes/Grafana/Grafana.node.js", 536 | "dist/nodes/GraphQL/GraphQL.node.js", 537 | "dist/nodes/Grist/Grist.node.js", 538 | "dist/nodes/Gumroad/GumroadTrigger.node.js", 539 | "dist/nodes/HackerNews/HackerNews.node.js", 540 | "dist/nodes/HaloPSA/HaloPSA.node.js", 541 | "dist/nodes/Harvest/Harvest.node.js", 542 | "dist/nodes/HelpScout/HelpScout.node.js", 543 | "dist/nodes/HelpScout/HelpScoutTrigger.node.js", 544 | "dist/nodes/HighLevel/HighLevel.node.js", 545 | "dist/nodes/HomeAssistant/HomeAssistant.node.js", 546 | "dist/nodes/HtmlExtract/HtmlExtract.node.js", 547 | "dist/nodes/Html/Html.node.js", 548 | "dist/nodes/HttpRequest/HttpRequest.node.js", 549 | "dist/nodes/Hubspot/Hubspot.node.js", 550 | "dist/nodes/Hubspot/HubspotTrigger.node.js", 551 | "dist/nodes/HumanticAI/HumanticAi.node.js", 552 | "dist/nodes/Hunter/Hunter.node.js", 553 | "dist/nodes/ICalendar/ICalendar.node.js", 554 | "dist/nodes/If/If.node.js", 555 | "dist/nodes/Intercom/Intercom.node.js", 556 | "dist/nodes/Interval/Interval.node.js", 557 | "dist/nodes/InvoiceNinja/InvoiceNinja.node.js", 558 | "dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js", 559 | "dist/nodes/ItemLists/ItemLists.node.js", 560 | "dist/nodes/Iterable/Iterable.node.js", 561 | "dist/nodes/Jenkins/Jenkins.node.js", 562 | "dist/nodes/Jira/Jira.node.js", 563 | "dist/nodes/Jira/JiraTrigger.node.js", 564 | "dist/nodes/JotForm/JotFormTrigger.node.js", 565 | "dist/nodes/Kafka/Kafka.node.js", 566 | "dist/nodes/Kafka/KafkaTrigger.node.js", 567 | "dist/nodes/Keap/Keap.node.js", 568 | "dist/nodes/Keap/KeapTrigger.node.js", 569 | "dist/nodes/Kitemaker/Kitemaker.node.js", 570 | "dist/nodes/KoBoToolbox/KoBoToolbox.node.js", 571 | "dist/nodes/KoBoToolbox/KoBoToolboxTrigger.node.js", 572 | "dist/nodes/Ldap/Ldap.node.js", 573 | "dist/nodes/Lemlist/Lemlist.node.js", 574 | "dist/nodes/Lemlist/LemlistTrigger.node.js", 575 | "dist/nodes/Line/Line.node.js", 576 | "dist/nodes/Linear/Linear.node.js", 577 | "dist/nodes/Linear/LinearTrigger.node.js", 578 | "dist/nodes/LingvaNex/LingvaNex.node.js", 579 | "dist/nodes/LinkedIn/LinkedIn.node.js", 580 | "dist/nodes/LocalFileTrigger/LocalFileTrigger.node.js", 581 | "dist/nodes/LoneScale/LoneScaleTrigger.node.js", 582 | "dist/nodes/LoneScale/LoneScale.node.js", 583 | "dist/nodes/Magento/Magento2.node.js", 584 | "dist/nodes/Mailcheck/Mailcheck.node.js", 585 | "dist/nodes/Mailchimp/Mailchimp.node.js", 586 | "dist/nodes/Mailchimp/MailchimpTrigger.node.js", 587 | "dist/nodes/MailerLite/MailerLite.node.js", 588 | "dist/nodes/MailerLite/MailerLiteTrigger.node.js", 589 | "dist/nodes/Mailgun/Mailgun.node.js", 590 | "dist/nodes/Mailjet/Mailjet.node.js", 591 | "dist/nodes/Mailjet/MailjetTrigger.node.js", 592 | "dist/nodes/Mandrill/Mandrill.node.js", 593 | "dist/nodes/ManualTrigger/ManualTrigger.node.js", 594 | "dist/nodes/Markdown/Markdown.node.js", 595 | "dist/nodes/Marketstack/Marketstack.node.js", 596 | "dist/nodes/Matrix/Matrix.node.js", 597 | "dist/nodes/Mattermost/Mattermost.node.js", 598 | "dist/nodes/Mautic/Mautic.node.js", 599 | "dist/nodes/Mautic/MauticTrigger.node.js", 600 | "dist/nodes/Medium/Medium.node.js", 601 | "dist/nodes/Merge/Merge.node.js", 602 | "dist/nodes/MessageBird/MessageBird.node.js", 603 | "dist/nodes/Metabase/Metabase.node.js", 604 | "dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js", 605 | "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", 606 | "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js", 607 | "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", 608 | "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js", 609 | "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", 610 | "dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js", 611 | "dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js", 612 | "dist/nodes/Mindee/Mindee.node.js", 613 | "dist/nodes/Misp/Misp.node.js", 614 | "dist/nodes/Mocean/Mocean.node.js", 615 | "dist/nodes/MondayCom/MondayCom.node.js", 616 | "dist/nodes/MongoDb/MongoDb.node.js", 617 | "dist/nodes/MonicaCrm/MonicaCrm.node.js", 618 | "dist/nodes/MoveBinaryData/MoveBinaryData.node.js", 619 | "dist/nodes/MQTT/Mqtt.node.js", 620 | "dist/nodes/MQTT/MqttTrigger.node.js", 621 | "dist/nodes/Msg91/Msg91.node.js", 622 | "dist/nodes/MySql/MySql.node.js", 623 | "dist/nodes/N8n/N8n.node.js", 624 | "dist/nodes/N8nTrainingCustomerDatastore/N8nTrainingCustomerDatastore.node.js", 625 | "dist/nodes/N8nTrainingCustomerMessenger/N8nTrainingCustomerMessenger.node.js", 626 | "dist/nodes/N8nTrigger/N8nTrigger.node.js", 627 | "dist/nodes/Nasa/Nasa.node.js", 628 | "dist/nodes/Netlify/Netlify.node.js", 629 | "dist/nodes/Netlify/NetlifyTrigger.node.js", 630 | "dist/nodes/NextCloud/NextCloud.node.js", 631 | "dist/nodes/NocoDB/NocoDB.node.js", 632 | "dist/nodes/Brevo/Brevo.node.js", 633 | "dist/nodes/Brevo/BrevoTrigger.node.js", 634 | "dist/nodes/StickyNote/StickyNote.node.js", 635 | "dist/nodes/NoOp/NoOp.node.js", 636 | "dist/nodes/Onfleet/Onfleet.node.js", 637 | "dist/nodes/Onfleet/OnfleetTrigger.node.js", 638 | "dist/nodes/Notion/Notion.node.js", 639 | "dist/nodes/Notion/NotionTrigger.node.js", 640 | "dist/nodes/Npm/Npm.node.js", 641 | "dist/nodes/Odoo/Odoo.node.js", 642 | "dist/nodes/OneSimpleApi/OneSimpleApi.node.js", 643 | "dist/nodes/OpenAi/OpenAi.node.js", 644 | "dist/nodes/OpenThesaurus/OpenThesaurus.node.js", 645 | "dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js", 646 | "dist/nodes/Orbit/Orbit.node.js", 647 | "dist/nodes/Oura/Oura.node.js", 648 | "dist/nodes/Paddle/Paddle.node.js", 649 | "dist/nodes/PagerDuty/PagerDuty.node.js", 650 | "dist/nodes/PayPal/PayPal.node.js", 651 | "dist/nodes/PayPal/PayPalTrigger.node.js", 652 | "dist/nodes/Peekalink/Peekalink.node.js", 653 | "dist/nodes/Phantombuster/Phantombuster.node.js", 654 | "dist/nodes/PhilipsHue/PhilipsHue.node.js", 655 | "dist/nodes/Pipedrive/Pipedrive.node.js", 656 | "dist/nodes/Pipedrive/PipedriveTrigger.node.js", 657 | "dist/nodes/Plivo/Plivo.node.js", 658 | "dist/nodes/PostBin/PostBin.node.js", 659 | "dist/nodes/Postgres/Postgres.node.js", 660 | "dist/nodes/Postgres/PostgresTrigger.node.js", 661 | "dist/nodes/PostHog/PostHog.node.js", 662 | "dist/nodes/Postmark/PostmarkTrigger.node.js", 663 | "dist/nodes/ProfitWell/ProfitWell.node.js", 664 | "dist/nodes/Pushbullet/Pushbullet.node.js", 665 | "dist/nodes/Pushcut/Pushcut.node.js", 666 | "dist/nodes/Pushcut/PushcutTrigger.node.js", 667 | "dist/nodes/Pushover/Pushover.node.js", 668 | "dist/nodes/QuestDb/QuestDb.node.js", 669 | "dist/nodes/QuickBase/QuickBase.node.js", 670 | "dist/nodes/QuickBooks/QuickBooks.node.js", 671 | "dist/nodes/QuickChart/QuickChart.node.js", 672 | "dist/nodes/RabbitMQ/RabbitMQ.node.js", 673 | "dist/nodes/RabbitMQ/RabbitMQTrigger.node.js", 674 | "dist/nodes/Raindrop/Raindrop.node.js", 675 | "dist/nodes/ReadBinaryFile/ReadBinaryFile.node.js", 676 | "dist/nodes/ReadBinaryFiles/ReadBinaryFiles.node.js", 677 | "dist/nodes/ReadPdf/ReadPDF.node.js", 678 | "dist/nodes/Reddit/Reddit.node.js", 679 | "dist/nodes/Redis/Redis.node.js", 680 | "dist/nodes/Redis/RedisTrigger.node.js", 681 | "dist/nodes/RenameKeys/RenameKeys.node.js", 682 | "dist/nodes/RespondToWebhook/RespondToWebhook.node.js", 683 | "dist/nodes/Rocketchat/Rocketchat.node.js", 684 | "dist/nodes/RssFeedRead/RssFeedRead.node.js", 685 | "dist/nodes/RssFeedRead/RssFeedReadTrigger.node.js", 686 | "dist/nodes/Rundeck/Rundeck.node.js", 687 | "dist/nodes/S3/S3.node.js", 688 | "dist/nodes/Salesforce/Salesforce.node.js", 689 | "dist/nodes/Salesmate/Salesmate.node.js", 690 | "dist/nodes/Schedule/ScheduleTrigger.node.js", 691 | "dist/nodes/SeaTable/SeaTable.node.js", 692 | "dist/nodes/SeaTable/SeaTableTrigger.node.js", 693 | "dist/nodes/SecurityScorecard/SecurityScorecard.node.js", 694 | "dist/nodes/Segment/Segment.node.js", 695 | "dist/nodes/SendGrid/SendGrid.node.js", 696 | "dist/nodes/Sendy/Sendy.node.js", 697 | "dist/nodes/SentryIo/SentryIo.node.js", 698 | "dist/nodes/ServiceNow/ServiceNow.node.js", 699 | "dist/nodes/Set/Set.node.js", 700 | "dist/nodes/Shopify/Shopify.node.js", 701 | "dist/nodes/Shopify/ShopifyTrigger.node.js", 702 | "dist/nodes/Signl4/Signl4.node.js", 703 | "dist/nodes/Slack/Slack.node.js", 704 | "dist/nodes/Sms77/Sms77.node.js", 705 | "dist/nodes/Snowflake/Snowflake.node.js", 706 | "dist/nodes/SplitInBatches/SplitInBatches.node.js", 707 | "dist/nodes/Splunk/Splunk.node.js", 708 | "dist/nodes/Spontit/Spontit.node.js", 709 | "dist/nodes/Spotify/Spotify.node.js", 710 | "dist/nodes/SpreadsheetFile/SpreadsheetFile.node.js", 711 | "dist/nodes/SseTrigger/SseTrigger.node.js", 712 | "dist/nodes/Ssh/Ssh.node.js", 713 | "dist/nodes/Stackby/Stackby.node.js", 714 | "dist/nodes/Start/Start.node.js", 715 | "dist/nodes/StopAndError/StopAndError.node.js", 716 | "dist/nodes/Storyblok/Storyblok.node.js", 717 | "dist/nodes/Strapi/Strapi.node.js", 718 | "dist/nodes/Strava/Strava.node.js", 719 | "dist/nodes/Strava/StravaTrigger.node.js", 720 | "dist/nodes/Stripe/Stripe.node.js", 721 | "dist/nodes/Stripe/StripeTrigger.node.js", 722 | "dist/nodes/Supabase/Supabase.node.js", 723 | "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", 724 | "dist/nodes/Switch/Switch.node.js", 725 | "dist/nodes/SyncroMSP/SyncroMsp.node.js", 726 | "dist/nodes/Taiga/Taiga.node.js", 727 | "dist/nodes/Taiga/TaigaTrigger.node.js", 728 | "dist/nodes/Tapfiliate/Tapfiliate.node.js", 729 | "dist/nodes/Telegram/Telegram.node.js", 730 | "dist/nodes/Telegram/TelegramTrigger.node.js", 731 | "dist/nodes/TheHiveProject/TheHiveProject.node.js", 732 | "dist/nodes/TheHiveProject/TheHiveProjectTrigger.node.js", 733 | "dist/nodes/TheHive/TheHive.node.js", 734 | "dist/nodes/TheHive/TheHiveTrigger.node.js", 735 | "dist/nodes/TimescaleDb/TimescaleDb.node.js", 736 | "dist/nodes/Todoist/Todoist.node.js", 737 | "dist/nodes/Toggl/TogglTrigger.node.js", 738 | "dist/nodes/Totp/Totp.node.js", 739 | "dist/nodes/TravisCi/TravisCi.node.js", 740 | "dist/nodes/Trello/Trello.node.js", 741 | "dist/nodes/Trello/TrelloTrigger.node.js", 742 | "dist/nodes/Twake/Twake.node.js", 743 | "dist/nodes/Twilio/Twilio.node.js", 744 | "dist/nodes/Twist/Twist.node.js", 745 | "dist/nodes/Twitter/Twitter.node.js", 746 | "dist/nodes/Typeform/TypeformTrigger.node.js", 747 | "dist/nodes/UnleashedSoftware/UnleashedSoftware.node.js", 748 | "dist/nodes/Uplead/Uplead.node.js", 749 | "dist/nodes/UProc/UProc.node.js", 750 | "dist/nodes/UptimeRobot/UptimeRobot.node.js", 751 | "dist/nodes/UrlScanIo/UrlScanIo.node.js", 752 | "dist/nodes/Vero/Vero.node.js", 753 | "dist/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.js", 754 | "dist/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloudTrigger.node.js", 755 | "dist/nodes/Venafi/Datacenter/VenafiTlsProtectDatacenter.node.js", 756 | "dist/nodes/Vonage/Vonage.node.js", 757 | "dist/nodes/Wait/Wait.node.js", 758 | "dist/nodes/Webflow/Webflow.node.js", 759 | "dist/nodes/Webflow/WebflowTrigger.node.js", 760 | "dist/nodes/Webhook/Webhook.node.js", 761 | "dist/nodes/Wekan/Wekan.node.js", 762 | "dist/nodes/WhatsApp/WhatsApp.node.js", 763 | "dist/nodes/Wise/Wise.node.js", 764 | "dist/nodes/Wise/WiseTrigger.node.js", 765 | "dist/nodes/WooCommerce/WooCommerce.node.js", 766 | "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", 767 | "dist/nodes/Wordpress/Wordpress.node.js", 768 | "dist/nodes/Workable/WorkableTrigger.node.js", 769 | "dist/nodes/WorkflowTrigger/WorkflowTrigger.node.js", 770 | "dist/nodes/WriteBinaryFile/WriteBinaryFile.node.js", 771 | "dist/nodes/Wufoo/WufooTrigger.node.js", 772 | "dist/nodes/Xero/Xero.node.js", 773 | "dist/nodes/Xml/Xml.node.js", 774 | "dist/nodes/Yourls/Yourls.node.js", 775 | "dist/nodes/Zammad/Zammad.node.js", 776 | "dist/nodes/Zendesk/Zendesk.node.js", 777 | "dist/nodes/Zendesk/ZendeskTrigger.node.js", 778 | "dist/nodes/Zoho/ZohoCrm.node.js", 779 | "dist/nodes/Zoom/Zoom.node.js", 780 | "dist/nodes/Zulip/Zulip.node.js" 781 | ] 782 | }, 783 | "devDependencies": { 784 | "@types/amqplib": "^0.10.1", 785 | "@types/aws4": "^1.5.1", 786 | "@types/basic-auth": "^1.1.3", 787 | "@types/cheerio": "^0.22.15", 788 | "@types/cron": "~1.7.1", 789 | "@types/eventsource": "^1.1.2", 790 | "@types/express": "^4.17.6", 791 | "@types/gm": "^1.25.0", 792 | "@types/imap-simple": "^4.2.0", 793 | "@types/js-nacl": "^1.3.0", 794 | "@types/jsonwebtoken": "^9.0.1", 795 | "@types/lodash": "^4.14.195", 796 | "@types/lossless-json": "^1.0.0", 797 | "@types/mailparser": "^2.7.3", 798 | "@types/mime-types": "^2.1.0", 799 | "@types/mssql": "^6.0.2", 800 | "@types/node-ssh": "^7.0.1", 801 | "@types/nodemailer": "^6.4.0", 802 | "@types/promise-ftp": "^1.3.4", 803 | "@types/redis": "^2.8.11", 804 | "@types/request-promise-native": "~1.0.15", 805 | "@types/rfc2047": "^2.0.1", 806 | "@types/showdown": "^1.9.4", 807 | "@types/snowflake-sdk": "^1.6.12", 808 | "@types/ssh2-sftp-client": "^5.1.0", 809 | "@types/tmp": "^0.2.0", 810 | "@types/uuid": "^8.3.2", 811 | "@types/xml2js": "^0.4.11", 812 | "eslint-plugin-n8n-nodes-base": "^1.16.0", 813 | "gulp": "^4.0.0", 814 | "n8n-core": "1.14.1" 815 | }, 816 | "dependencies": { 817 | "@kafkajs/confluent-schema-registry": "1.0.6", 818 | "@n8n/vm2": "^3.9.20", 819 | "amqplib": "^0.10.3", 820 | "aws4": "^1.8.0", 821 | "basic-auth": "^2.0.1", 822 | "change-case": "^4.1.1", 823 | "cheerio": "1.0.0-rc.6", 824 | "chokidar": "3.5.2", 825 | "cron": "~1.7.2", 826 | "csv-parse": "^5.5.0", 827 | "currency-codes": "^2.1.0", 828 | "eventsource": "^2.0.2", 829 | "fast-glob": "^3.2.5", 830 | "fflate": "^0.7.0", 831 | "get-system-fonts": "^2.0.2", 832 | "gm": "^1.25.0", 833 | "iconv-lite": "^0.6.2", 834 | "ics": "^2.27.0", 835 | "imap-simple": "^4.3.0", 836 | "isbot": "^3.6.13", 837 | "iso-639-1": "^2.1.3", 838 | "js-nacl": "^1.4.0", 839 | "jsonwebtoken": "^9.0.0", 840 | "kafkajs": "^1.14.0", 841 | "ldapts": "^4.2.6", 842 | "lodash": "^4.17.21", 843 | "lossless-json": "^1.0.4", 844 | "luxon": "^3.3.0", 845 | "mailparser": "^3.2.0", 846 | "minifaker": "^1.34.1", 847 | "moment": "~2.29.2", 848 | "moment-timezone": "^0.5.28", 849 | "mongodb": "^4.17.1", 850 | "mqtt": "^5.0.2", 851 | "mssql": "^8.1.2", 852 | "mysql2": "~2.3.0", 853 | "nanoid": "^3.3.6", 854 | "node-html-markdown": "^1.1.3", 855 | "node-ssh": "^12.0.0", 856 | "nodemailer": "^6.7.1", 857 | "otpauth": "^9.1.1", 858 | "pdfjs-dist": "^2.16.105", 859 | "pg": "^8.3.0", 860 | "pg-promise": "^10.5.8", 861 | "pretty-bytes": "^5.6.0", 862 | "promise-ftp": "^1.3.5", 863 | "pyodide": "^0.23.4", 864 | "redis": "^3.1.1", 865 | "rfc2047": "^4.0.1", 866 | "rhea": "^1.0.11", 867 | "rss-parser": "^3.7.0", 868 | "semver": "^7.5.4", 869 | "showdown": "^2.0.3", 870 | "simple-git": "^3.17.0", 871 | "snowflake-sdk": "^1.8.0", 872 | "ssh2-sftp-client": "^7.0.0", 873 | "tmp-promise": "^3.0.2", 874 | "typedi": "^0.10.0", 875 | "uuid": "^8.3.2", 876 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", 877 | "xml2js": "^0.5.0", 878 | "n8n-workflow": "1.14.1" 879 | }, 880 | "scripts": { 881 | "clean": "rimraf dist .turbo", 882 | "dev": "pnpm watch", 883 | "typecheck": "tsc", 884 | "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && gulp build:icons && gulp build:translations && pnpm build:metadata", 885 | "build:translations": "gulp build:translations", 886 | "build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types", 887 | "format": "prettier --write . --ignore-path ../../.prettierignore", 888 | "lint": "eslint . --quiet && node ./scripts/validate-load-options-methods.js", 889 | "lintfix": "eslint . --fix", 890 | "watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\" --onSuccess \"pnpm n8n-generate-ui-types\"", 891 | "test": "jest" 892 | } 893 | }, 894 | "extraction_time_ms": 4, 895 | "extracted_at": "2025-06-07T17:49:22.724Z" 896 | } ```