This is page 14 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 │ │ │ ├── sqljs-memory-leak.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/telemetry/telemetry-manager.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Telemetry Manager 3 | * Main telemetry coordinator using modular components 4 | */ 5 | 6 | import { createClient, SupabaseClient } from '@supabase/supabase-js'; 7 | import { TelemetryConfigManager } from './config-manager'; 8 | import { TelemetryEventTracker } from './event-tracker'; 9 | import { TelemetryBatchProcessor } from './batch-processor'; 10 | import { TelemetryPerformanceMonitor } from './performance-monitor'; 11 | import { TELEMETRY_BACKEND } from './telemetry-types'; 12 | import { TelemetryError, TelemetryErrorType, TelemetryErrorAggregator } from './telemetry-error'; 13 | import { logger } from '../utils/logger'; 14 | 15 | export class TelemetryManager { 16 | private static instance: TelemetryManager; 17 | private supabase: SupabaseClient | null = null; 18 | private configManager: TelemetryConfigManager; 19 | private eventTracker: TelemetryEventTracker; 20 | private batchProcessor: TelemetryBatchProcessor; 21 | private performanceMonitor: TelemetryPerformanceMonitor; 22 | private errorAggregator: TelemetryErrorAggregator; 23 | private isInitialized: boolean = false; 24 | 25 | private constructor() { 26 | // Prevent direct instantiation even when TypeScript is bypassed 27 | if (TelemetryManager.instance) { 28 | throw new Error('Use TelemetryManager.getInstance() instead of new TelemetryManager()'); 29 | } 30 | 31 | this.configManager = TelemetryConfigManager.getInstance(); 32 | this.errorAggregator = new TelemetryErrorAggregator(); 33 | this.performanceMonitor = new TelemetryPerformanceMonitor(); 34 | 35 | // Initialize event tracker with callbacks 36 | this.eventTracker = new TelemetryEventTracker( 37 | () => this.configManager.getUserId(), 38 | () => this.isEnabled() 39 | ); 40 | 41 | // Initialize batch processor (will be configured after Supabase init) 42 | this.batchProcessor = new TelemetryBatchProcessor( 43 | null, 44 | () => this.isEnabled() 45 | ); 46 | 47 | // Delay initialization to first use, not constructor 48 | // this.initialize(); 49 | } 50 | 51 | static getInstance(): TelemetryManager { 52 | if (!TelemetryManager.instance) { 53 | TelemetryManager.instance = new TelemetryManager(); 54 | } 55 | return TelemetryManager.instance; 56 | } 57 | 58 | /** 59 | * Ensure telemetry is initialized before use 60 | */ 61 | private ensureInitialized(): void { 62 | if (!this.isInitialized && this.configManager.isEnabled()) { 63 | this.initialize(); 64 | } 65 | } 66 | 67 | /** 68 | * Initialize telemetry if enabled 69 | */ 70 | private initialize(): void { 71 | if (!this.configManager.isEnabled()) { 72 | logger.debug('Telemetry disabled by user preference'); 73 | return; 74 | } 75 | 76 | // Use hardcoded credentials for zero-configuration telemetry 77 | // Environment variables can override for development/testing 78 | const supabaseUrl = process.env.SUPABASE_URL || TELEMETRY_BACKEND.URL; 79 | const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || TELEMETRY_BACKEND.ANON_KEY; 80 | 81 | try { 82 | this.supabase = createClient(supabaseUrl, supabaseAnonKey, { 83 | auth: { 84 | persistSession: false, 85 | autoRefreshToken: false, 86 | }, 87 | realtime: { 88 | params: { 89 | eventsPerSecond: 1, 90 | }, 91 | }, 92 | }); 93 | 94 | // Update batch processor with Supabase client 95 | this.batchProcessor = new TelemetryBatchProcessor( 96 | this.supabase, 97 | () => this.isEnabled() 98 | ); 99 | 100 | this.batchProcessor.start(); 101 | this.isInitialized = true; 102 | 103 | logger.debug('Telemetry initialized successfully'); 104 | } catch (error) { 105 | const telemetryError = new TelemetryError( 106 | TelemetryErrorType.INITIALIZATION_ERROR, 107 | 'Failed to initialize telemetry', 108 | { error: error instanceof Error ? error.message : String(error) } 109 | ); 110 | this.errorAggregator.record(telemetryError); 111 | telemetryError.log(); 112 | this.isInitialized = false; 113 | } 114 | } 115 | 116 | /** 117 | * Track a tool usage event 118 | */ 119 | trackToolUsage(toolName: string, success: boolean, duration?: number): void { 120 | this.ensureInitialized(); 121 | this.performanceMonitor.startOperation('trackToolUsage'); 122 | this.eventTracker.trackToolUsage(toolName, success, duration); 123 | this.eventTracker.updateToolSequence(toolName); 124 | this.performanceMonitor.endOperation('trackToolUsage'); 125 | } 126 | 127 | /** 128 | * Track workflow creation 129 | */ 130 | async trackWorkflowCreation(workflow: any, validationPassed: boolean): Promise<void> { 131 | this.ensureInitialized(); 132 | this.performanceMonitor.startOperation('trackWorkflowCreation'); 133 | try { 134 | await this.eventTracker.trackWorkflowCreation(workflow, validationPassed); 135 | // Auto-flush workflows to prevent data loss 136 | await this.flush(); 137 | } catch (error) { 138 | const telemetryError = error instanceof TelemetryError 139 | ? error 140 | : new TelemetryError( 141 | TelemetryErrorType.UNKNOWN_ERROR, 142 | 'Failed to track workflow', 143 | { error: String(error) } 144 | ); 145 | this.errorAggregator.record(telemetryError); 146 | } finally { 147 | this.performanceMonitor.endOperation('trackWorkflowCreation'); 148 | } 149 | } 150 | 151 | 152 | /** 153 | * Track an error event 154 | */ 155 | trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void { 156 | this.ensureInitialized(); 157 | this.eventTracker.trackError(errorType, context, toolName, errorMessage); 158 | } 159 | 160 | /** 161 | * Track a generic event 162 | */ 163 | trackEvent(eventName: string, properties: Record<string, any>): void { 164 | this.ensureInitialized(); 165 | this.eventTracker.trackEvent(eventName, properties); 166 | } 167 | 168 | /** 169 | * Track session start 170 | */ 171 | trackSessionStart(): void { 172 | this.ensureInitialized(); 173 | this.eventTracker.trackSessionStart(); 174 | } 175 | 176 | /** 177 | * Track search queries 178 | */ 179 | trackSearchQuery(query: string, resultsFound: number, searchType: string): void { 180 | this.eventTracker.trackSearchQuery(query, resultsFound, searchType); 181 | } 182 | 183 | /** 184 | * Track validation details 185 | */ 186 | trackValidationDetails(nodeType: string, errorType: string, details: Record<string, any>): void { 187 | this.eventTracker.trackValidationDetails(nodeType, errorType, details); 188 | } 189 | 190 | /** 191 | * Track tool sequences 192 | */ 193 | trackToolSequence(previousTool: string, currentTool: string, timeDelta: number): void { 194 | this.eventTracker.trackToolSequence(previousTool, currentTool, timeDelta); 195 | } 196 | 197 | /** 198 | * Track node configuration 199 | */ 200 | trackNodeConfiguration(nodeType: string, propertiesSet: number, usedDefaults: boolean): void { 201 | this.eventTracker.trackNodeConfiguration(nodeType, propertiesSet, usedDefaults); 202 | } 203 | 204 | /** 205 | * Track performance metrics 206 | */ 207 | trackPerformanceMetric(operation: string, duration: number, metadata?: Record<string, any>): void { 208 | this.eventTracker.trackPerformanceMetric(operation, duration, metadata); 209 | } 210 | 211 | 212 | /** 213 | * Flush queued events to Supabase 214 | */ 215 | async flush(): Promise<void> { 216 | this.ensureInitialized(); 217 | if (!this.isEnabled() || !this.supabase) return; 218 | 219 | this.performanceMonitor.startOperation('flush'); 220 | 221 | // Get queued data from event tracker 222 | const events = this.eventTracker.getEventQueue(); 223 | const workflows = this.eventTracker.getWorkflowQueue(); 224 | 225 | // Clear queues immediately to prevent duplicate processing 226 | this.eventTracker.clearEventQueue(); 227 | this.eventTracker.clearWorkflowQueue(); 228 | 229 | try { 230 | // Use batch processor to flush 231 | await this.batchProcessor.flush(events, workflows); 232 | } catch (error) { 233 | const telemetryError = error instanceof TelemetryError 234 | ? error 235 | : new TelemetryError( 236 | TelemetryErrorType.NETWORK_ERROR, 237 | 'Failed to flush telemetry', 238 | { error: String(error) }, 239 | true // Retryable 240 | ); 241 | this.errorAggregator.record(telemetryError); 242 | telemetryError.log(); 243 | } finally { 244 | const duration = this.performanceMonitor.endOperation('flush'); 245 | if (duration > 100) { 246 | logger.debug(`Telemetry flush took ${duration.toFixed(2)}ms`); 247 | } 248 | } 249 | } 250 | 251 | 252 | /** 253 | * Check if telemetry is enabled 254 | */ 255 | private isEnabled(): boolean { 256 | return this.isInitialized && this.configManager.isEnabled(); 257 | } 258 | 259 | /** 260 | * Disable telemetry 261 | */ 262 | disable(): void { 263 | this.configManager.disable(); 264 | this.batchProcessor.stop(); 265 | this.isInitialized = false; 266 | this.supabase = null; 267 | } 268 | 269 | /** 270 | * Enable telemetry 271 | */ 272 | enable(): void { 273 | this.configManager.enable(); 274 | this.initialize(); 275 | } 276 | 277 | /** 278 | * Get telemetry status 279 | */ 280 | getStatus(): string { 281 | return this.configManager.getStatus(); 282 | } 283 | 284 | /** 285 | * Get comprehensive telemetry metrics 286 | */ 287 | getMetrics() { 288 | return { 289 | status: this.isEnabled() ? 'enabled' : 'disabled', 290 | initialized: this.isInitialized, 291 | tracking: this.eventTracker.getStats(), 292 | processing: this.batchProcessor.getMetrics(), 293 | errors: this.errorAggregator.getStats(), 294 | performance: this.performanceMonitor.getDetailedReport(), 295 | overhead: this.performanceMonitor.getTelemetryOverhead() 296 | }; 297 | } 298 | 299 | /** 300 | * Reset singleton instance (for testing purposes) 301 | */ 302 | static resetInstance(): void { 303 | TelemetryManager.instance = undefined as any; 304 | (global as any).__telemetryManager = undefined; 305 | } 306 | } 307 | 308 | // Create a global singleton to ensure only one instance across all imports 309 | const globalAny = global as any; 310 | 311 | if (!globalAny.__telemetryManager) { 312 | globalAny.__telemetryManager = TelemetryManager.getInstance(); 313 | } 314 | 315 | // Export singleton instance 316 | export const telemetry = globalAny.__telemetryManager as TelemetryManager; ``` -------------------------------------------------------------------------------- /tests/utils/data-generators.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { faker } from '@faker-js/faker'; 2 | import { WorkflowNode, Workflow } from '@/types/n8n-api'; 3 | 4 | // Use any type for INodeDefinition since it's from n8n-workflow package 5 | type INodeDefinition = any; 6 | 7 | /** 8 | * Data generators for creating realistic test data 9 | */ 10 | 11 | /** 12 | * Generate a random node type 13 | */ 14 | export function generateNodeType(): string { 15 | const packages = ['n8n-nodes-base', '@n8n/n8n-nodes-langchain']; 16 | const nodeTypes = [ 17 | 'webhook', 'httpRequest', 'slack', 'googleSheets', 'postgres', 18 | 'function', 'code', 'if', 'switch', 'merge', 'splitInBatches', 19 | 'emailSend', 'redis', 'mongodb', 'mysql', 'ftp', 'ssh' 20 | ]; 21 | 22 | const pkg = faker.helpers.arrayElement(packages); 23 | const type = faker.helpers.arrayElement(nodeTypes); 24 | 25 | return `${pkg}.${type}`; 26 | } 27 | 28 | /** 29 | * Generate property definitions for a node 30 | */ 31 | export function generateProperties(count = 5): any[] { 32 | const properties = []; 33 | 34 | for (let i = 0; i < count; i++) { 35 | const type = faker.helpers.arrayElement([ 36 | 'string', 'number', 'boolean', 'options', 'collection' 37 | ]); 38 | 39 | const property: any = { 40 | displayName: faker.helpers.arrayElement([ 41 | 'Resource', 'Operation', 'Field', 'Value', 'Method', 42 | 'URL', 'Headers', 'Body', 'Authentication', 'Options' 43 | ]), 44 | name: faker.helpers.slugify(faker.word.noun()).toLowerCase(), 45 | type, 46 | default: generateDefaultValue(type), 47 | description: faker.lorem.sentence() 48 | }; 49 | 50 | if (type === 'options') { 51 | property.options = generateOptions(); 52 | } 53 | 54 | if (faker.datatype.boolean()) { 55 | property.required = true; 56 | } 57 | 58 | if (faker.datatype.boolean()) { 59 | property.displayOptions = generateDisplayOptions(); 60 | } 61 | 62 | properties.push(property); 63 | } 64 | 65 | return properties; 66 | } 67 | 68 | /** 69 | * Generate default value based on type 70 | */ 71 | function generateDefaultValue(type: string): any { 72 | switch (type) { 73 | case 'string': 74 | return faker.lorem.word(); 75 | case 'number': 76 | return faker.number.int({ min: 0, max: 100 }); 77 | case 'boolean': 78 | return faker.datatype.boolean(); 79 | case 'options': 80 | return 'option1'; 81 | case 'collection': 82 | return {}; 83 | default: 84 | return ''; 85 | } 86 | } 87 | 88 | /** 89 | * Generate options for select fields 90 | */ 91 | function generateOptions(count = 3): any[] { 92 | const options = []; 93 | 94 | for (let i = 0; i < count; i++) { 95 | options.push({ 96 | name: faker.helpers.arrayElement([ 97 | 'Create', 'Read', 'Update', 'Delete', 'List', 98 | 'Get', 'Post', 'Put', 'Patch', 'Send' 99 | ]), 100 | value: `option${i + 1}`, 101 | description: faker.lorem.sentence() 102 | }); 103 | } 104 | 105 | return options; 106 | } 107 | 108 | /** 109 | * Generate display options for conditional fields 110 | */ 111 | function generateDisplayOptions(): any { 112 | return { 113 | show: { 114 | resource: [faker.helpers.arrayElement(['user', 'post', 'message'])], 115 | operation: [faker.helpers.arrayElement(['create', 'update', 'get'])] 116 | } 117 | }; 118 | } 119 | 120 | /** 121 | * Generate a complete node definition 122 | */ 123 | export function generateNodeDefinition(overrides?: Partial<INodeDefinition>): any { 124 | const nodeCategory = faker.helpers.arrayElement([ 125 | 'Core Nodes', 'Communication', 'Data Transformation', 126 | 'Development', 'Files', 'Productivity', 'Analytics' 127 | ]); 128 | 129 | return { 130 | displayName: faker.company.name() + ' Node', 131 | name: faker.helpers.slugify(faker.company.name()).toLowerCase(), 132 | group: [faker.helpers.arrayElement(['trigger', 'transform', 'output'])], 133 | version: faker.number.float({ min: 1, max: 3, fractionDigits: 1 }), 134 | subtitle: `={{$parameter["operation"] + ": " + $parameter["resource"]}}`, 135 | description: faker.lorem.paragraph(), 136 | defaults: { 137 | name: faker.company.name(), 138 | color: faker.color.rgb() 139 | }, 140 | inputs: ['main'], 141 | outputs: ['main'], 142 | credentials: faker.datatype.boolean() ? [{ 143 | name: faker.helpers.slugify(faker.company.name()).toLowerCase() + 'Api', 144 | required: true 145 | }] : undefined, 146 | properties: generateProperties(), 147 | codex: { 148 | categories: [nodeCategory], 149 | subcategories: { 150 | [nodeCategory]: [faker.word.noun()] 151 | }, 152 | alias: [faker.word.noun(), faker.word.verb()] 153 | }, 154 | ...overrides 155 | }; 156 | } 157 | 158 | /** 159 | * Generate workflow nodes 160 | */ 161 | export function generateWorkflowNodes(count = 3): WorkflowNode[] { 162 | const nodes: WorkflowNode[] = []; 163 | 164 | for (let i = 0; i < count; i++) { 165 | nodes.push({ 166 | id: faker.string.uuid(), 167 | name: faker.helpers.arrayElement([ 168 | 'Webhook', 'HTTP Request', 'Set', 'Function', 'IF', 169 | 'Slack', 'Email', 'Database', 'Code' 170 | ]) + (i > 0 ? i : ''), 171 | type: generateNodeType(), 172 | typeVersion: faker.number.float({ min: 1, max: 3, fractionDigits: 1 }), 173 | position: [ 174 | 250 + i * 200, 175 | 300 + (i % 2) * 100 176 | ], 177 | parameters: generateNodeParameters() 178 | }); 179 | } 180 | 181 | return nodes; 182 | } 183 | 184 | /** 185 | * Generate node parameters 186 | */ 187 | function generateNodeParameters(): Record<string, any> { 188 | const params: Record<string, any> = {}; 189 | 190 | // Common parameters 191 | if (faker.datatype.boolean()) { 192 | params.resource = faker.helpers.arrayElement(['user', 'post', 'message']); 193 | params.operation = faker.helpers.arrayElement(['create', 'get', 'update', 'delete']); 194 | } 195 | 196 | // Type-specific parameters 197 | if (faker.datatype.boolean()) { 198 | params.url = faker.internet.url(); 199 | } 200 | 201 | if (faker.datatype.boolean()) { 202 | params.method = faker.helpers.arrayElement(['GET', 'POST', 'PUT', 'DELETE']); 203 | } 204 | 205 | if (faker.datatype.boolean()) { 206 | params.authentication = faker.helpers.arrayElement(['none', 'basicAuth', 'oAuth2']); 207 | } 208 | 209 | // Add some random parameters 210 | const randomParamCount = faker.number.int({ min: 1, max: 5 }); 211 | for (let i = 0; i < randomParamCount; i++) { 212 | const key = faker.word.noun().toLowerCase(); 213 | params[key] = faker.helpers.arrayElement([ 214 | faker.lorem.word(), 215 | faker.number.int(), 216 | faker.datatype.boolean(), 217 | '={{ $json.data }}' 218 | ]); 219 | } 220 | 221 | return params; 222 | } 223 | 224 | /** 225 | * Generate workflow connections 226 | */ 227 | export function generateConnections(nodes: WorkflowNode[]): Record<string, any> { 228 | const connections: Record<string, any> = {}; 229 | 230 | // Connect nodes sequentially 231 | for (let i = 0; i < nodes.length - 1; i++) { 232 | const sourceId = nodes[i].id; 233 | const targetId = nodes[i + 1].id; 234 | 235 | if (!connections[sourceId]) { 236 | connections[sourceId] = { main: [[]] }; 237 | } 238 | 239 | connections[sourceId].main[0].push({ 240 | node: targetId, 241 | type: 'main', 242 | index: 0 243 | }); 244 | } 245 | 246 | // Add some random connections 247 | if (nodes.length > 2 && faker.datatype.boolean()) { 248 | const sourceIdx = faker.number.int({ min: 0, max: nodes.length - 2 }); 249 | const targetIdx = faker.number.int({ min: sourceIdx + 1, max: nodes.length - 1 }); 250 | 251 | const sourceId = nodes[sourceIdx].id; 252 | const targetId = nodes[targetIdx].id; 253 | 254 | if (connections[sourceId]?.main[0]) { 255 | connections[sourceId].main[0].push({ 256 | node: targetId, 257 | type: 'main', 258 | index: 0 259 | }); 260 | } 261 | } 262 | 263 | return connections; 264 | } 265 | 266 | /** 267 | * Generate a complete workflow 268 | */ 269 | export function generateWorkflow(nodeCount = 3): Workflow { 270 | const nodes = generateWorkflowNodes(nodeCount); 271 | 272 | return { 273 | id: faker.string.uuid(), 274 | name: faker.helpers.arrayElement([ 275 | 'Data Processing Workflow', 276 | 'API Integration Flow', 277 | 'Notification Pipeline', 278 | 'ETL Process', 279 | 'Webhook Handler' 280 | ]), 281 | active: faker.datatype.boolean(), 282 | nodes, 283 | connections: generateConnections(nodes), 284 | settings: { 285 | executionOrder: 'v1', 286 | saveManualExecutions: true, 287 | timezone: faker.location.timeZone() 288 | }, 289 | staticData: {}, 290 | tags: generateTags().map(t => t.name), 291 | createdAt: faker.date.past().toISOString(), 292 | updatedAt: faker.date.recent().toISOString() 293 | }; 294 | } 295 | 296 | /** 297 | * Generate workflow tags 298 | */ 299 | function generateTags(): Array<{ id: string; name: string }> { 300 | const tagCount = faker.number.int({ min: 0, max: 3 }); 301 | const tags = []; 302 | 303 | for (let i = 0; i < tagCount; i++) { 304 | tags.push({ 305 | id: faker.string.uuid(), 306 | name: faker.helpers.arrayElement([ 307 | 'production', 'development', 'testing', 308 | 'automation', 'integration', 'notification' 309 | ]) 310 | }); 311 | } 312 | 313 | return tags; 314 | } 315 | 316 | /** 317 | * Generate test templates 318 | */ 319 | export function generateTemplate() { 320 | const workflow = generateWorkflow(); 321 | 322 | return { 323 | id: faker.number.int({ min: 1000, max: 9999 }), 324 | name: workflow.name, 325 | description: faker.lorem.paragraph(), 326 | workflow, 327 | categories: faker.helpers.arrayElements([ 328 | 'Sales', 'Marketing', 'Engineering', 329 | 'HR', 'Finance', 'Operations' 330 | ], { min: 1, max: 3 }), 331 | useCases: faker.helpers.arrayElements([ 332 | 'Lead Generation', 'Data Sync', 'Notifications', 333 | 'Reporting', 'Automation', 'Integration' 334 | ], { min: 1, max: 3 }), 335 | views: faker.number.int({ min: 0, max: 10000 }), 336 | recentViews: faker.number.int({ min: 0, max: 100 }) 337 | }; 338 | } 339 | 340 | /** 341 | * Generate bulk test data 342 | */ 343 | export function generateBulkData(counts: { 344 | nodes?: number; 345 | workflows?: number; 346 | templates?: number; 347 | }) { 348 | const { nodes = 10, workflows = 5, templates = 3 } = counts; 349 | 350 | return { 351 | nodes: Array.from({ length: nodes }, () => generateNodeDefinition()), 352 | workflows: Array.from({ length: workflows }, () => generateWorkflow()), 353 | templates: Array.from({ length: templates }, () => generateTemplate()) 354 | }; 355 | } ``` -------------------------------------------------------------------------------- /MEMORY_TEMPLATE_UPDATE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Template Update Process - Quick Reference 2 | 3 | ## Overview 4 | 5 | The n8n-mcp project maintains a database of workflow templates from n8n.io. This guide explains how to update the template database incrementally without rebuilding from scratch. 6 | 7 | ## Current Database State 8 | 9 | As of the last update: 10 | - **2,598 templates** in database 11 | - Templates from the last 12 months 12 | - Latest template: September 12, 2025 13 | 14 | ## Quick Commands 15 | 16 | ### Incremental Update (Recommended) 17 | ```bash 18 | # Build if needed 19 | npm run build 20 | 21 | # Fetch only NEW templates (5-10 minutes) 22 | npm run fetch:templates:update 23 | ``` 24 | 25 | ### Full Rebuild (Rare) 26 | ```bash 27 | # Rebuild entire database from scratch (30-40 minutes) 28 | npm run fetch:templates 29 | ``` 30 | 31 | ## How It Works 32 | 33 | ### Incremental Update Mode (`--update`) 34 | 35 | The incremental update is **smart and efficient**: 36 | 37 | 1. **Loads existing template IDs** from database (~2,598 templates) 38 | 2. **Fetches template list** from n8n.io API (all templates from last 12 months) 39 | 3. **Filters** to find only NEW templates not in database 40 | 4. **Fetches details** for new templates only (saves time and API calls) 41 | 5. **Saves** new templates to database (existing ones untouched) 42 | 6. **Rebuilds FTS5** search index for new templates 43 | 44 | ### Key Benefits 45 | 46 | ✅ **Non-destructive**: All existing templates preserved 47 | ✅ **Fast**: Only fetches new templates (5-10 min vs 30-40 min) 48 | ✅ **API friendly**: Reduces load on n8n.io API 49 | ✅ **Safe**: Preserves AI-generated metadata 50 | ✅ **Smart**: Automatically skips duplicates 51 | 52 | ## Performance Comparison 53 | 54 | | Mode | Templates Fetched | Time | Use Case | 55 | |------|------------------|------|----------| 56 | | **Update** | Only new (~50-200) | 5-10 min | Regular updates | 57 | | **Rebuild** | All (~8000+) | 30-40 min | Initial setup or corruption | 58 | 59 | ## Command Options 60 | 61 | ### Basic Update 62 | ```bash 63 | npm run fetch:templates:update 64 | ``` 65 | 66 | ### Full Rebuild 67 | ```bash 68 | npm run fetch:templates 69 | ``` 70 | 71 | ### With Metadata Generation 72 | ```bash 73 | # Update templates and generate AI metadata 74 | npm run fetch:templates -- --update --generate-metadata 75 | 76 | # Or just generate metadata for existing templates 77 | npm run fetch:templates -- --metadata-only 78 | ``` 79 | 80 | ### Help 81 | ```bash 82 | npm run fetch:templates -- --help 83 | ``` 84 | 85 | ## Update Frequency 86 | 87 | Recommended update schedule: 88 | - **Weekly**: Run incremental update to get latest templates 89 | - **Monthly**: Review database statistics 90 | - **As needed**: Rebuild only if database corruption suspected 91 | 92 | ## Template Filtering 93 | 94 | The fetcher automatically filters templates: 95 | - ✅ **Includes**: Templates from last 12 months 96 | - ✅ **Includes**: Templates with >10 views 97 | - ❌ **Excludes**: Templates with ≤10 views (too niche) 98 | - ❌ **Excludes**: Templates older than 12 months 99 | 100 | ## Workflow 101 | 102 | ### Regular Update Workflow 103 | 104 | ```bash 105 | # 1. Check current state 106 | sqlite3 data/nodes.db "SELECT COUNT(*) FROM templates" 107 | 108 | # 2. Build project (if code changed) 109 | npm run build 110 | 111 | # 3. Run incremental update 112 | npm run fetch:templates:update 113 | 114 | # 4. Verify new templates added 115 | sqlite3 data/nodes.db "SELECT COUNT(*) FROM templates" 116 | ``` 117 | 118 | ### After n8n Dependency Update 119 | 120 | When you update n8n dependencies, templates remain compatible: 121 | ```bash 122 | # 1. Update n8n (from MEMORY_N8N_UPDATE.md) 123 | npm run update:all 124 | 125 | # 2. Fetch new templates incrementally 126 | npm run fetch:templates:update 127 | 128 | # 3. Check how many templates were added 129 | sqlite3 data/nodes.db "SELECT COUNT(*) FROM templates" 130 | 131 | # 4. Generate AI metadata for new templates (optional, requires OPENAI_API_KEY) 132 | npm run fetch:templates -- --metadata-only 133 | 134 | # 5. IMPORTANT: Sanitize templates before pushing database 135 | npm run build 136 | npm run sanitize:templates 137 | ``` 138 | 139 | Templates are independent of n8n version - they're just workflow JSON data. 140 | 141 | **CRITICAL**: Always run `npm run sanitize:templates` before pushing the database to remove API tokens from template workflows. 142 | 143 | **Note**: New templates fetched via `--update` mode will NOT have AI-generated metadata by default. You need to run `--metadata-only` separately to generate metadata for templates that don't have it yet. 144 | 145 | ## Troubleshooting 146 | 147 | ### No New Templates Found 148 | 149 | This is normal! It means: 150 | - All recent templates are already in your database 151 | - n8n.io hasn't published many new templates recently 152 | - Your database is up to date 153 | 154 | ```bash 155 | 📊 Update mode: 0 new templates to fetch (skipping 2598 existing) 156 | ✅ All templates already have metadata 157 | ``` 158 | 159 | ### API Rate Limiting 160 | 161 | If you hit rate limits: 162 | - The fetcher includes built-in delays (150ms between requests) 163 | - Wait a few minutes and try again 164 | - Use `--update` mode instead of full rebuild 165 | 166 | ### Database Corruption 167 | 168 | If you suspect corruption: 169 | ```bash 170 | # Full rebuild from scratch 171 | npm run fetch:templates 172 | 173 | # This will: 174 | # - Drop and recreate templates table 175 | # - Fetch all templates fresh 176 | # - Rebuild search indexes 177 | ``` 178 | 179 | ## Database Schema 180 | 181 | Templates are stored with: 182 | - Basic info (id, name, description, author, views, created_at) 183 | - Node types used (JSON array) 184 | - Complete workflow (gzip compressed, base64 encoded) 185 | - AI-generated metadata (optional, requires OpenAI API key) 186 | - FTS5 search index for fast text search 187 | 188 | ## Metadata Generation 189 | 190 | Generate AI metadata for templates: 191 | ```bash 192 | # Requires OPENAI_API_KEY in .env 193 | export OPENAI_API_KEY="sk-..." 194 | 195 | # Generate for templates without metadata (recommended after incremental update) 196 | npm run fetch:templates -- --metadata-only 197 | 198 | # Generate during template fetch (slower, but automatic) 199 | npm run fetch:templates:update -- --generate-metadata 200 | ``` 201 | 202 | **Important**: Incremental updates (`--update`) do NOT generate metadata by default. After running `npm run fetch:templates:update`, you'll have new templates without metadata. Run `--metadata-only` separately to generate metadata for them. 203 | 204 | ### Check Metadata Coverage 205 | 206 | ```bash 207 | # See how many templates have metadata 208 | sqlite3 data/nodes.db "SELECT 209 | COUNT(*) as total, 210 | SUM(CASE WHEN metadata_json IS NOT NULL THEN 1 ELSE 0 END) as with_metadata, 211 | SUM(CASE WHEN metadata_json IS NULL THEN 1 ELSE 0 END) as without_metadata 212 | FROM templates" 213 | 214 | # See recent templates without metadata 215 | sqlite3 data/nodes.db "SELECT id, name, created_at 216 | FROM templates 217 | WHERE metadata_json IS NULL 218 | ORDER BY created_at DESC 219 | LIMIT 10" 220 | ``` 221 | 222 | Metadata includes: 223 | - Categories 224 | - Complexity level (simple/medium/complex) 225 | - Use cases 226 | - Estimated setup time 227 | - Required services 228 | - Key features 229 | - Target audience 230 | 231 | ### Metadata Generation Troubleshooting 232 | 233 | If metadata generation fails: 234 | 235 | 1. **Check error file**: Errors are saved to `temp/batch/batch_*_error.jsonl` 236 | 2. **Common issues**: 237 | - `"Unsupported value: 'temperature'"` - Model doesn't support custom temperature 238 | - `"Invalid request"` - Check OPENAI_API_KEY is valid 239 | - Model availability issues 240 | 3. **Model**: Uses `gpt-5-mini-2025-08-07` by default 241 | 4. **Token limit**: 3000 tokens per request for detailed metadata 242 | 243 | The system will automatically: 244 | - Process error files and assign default metadata to failed templates 245 | - Save error details for debugging 246 | - Continue processing even if some templates fail 247 | 248 | **Example error handling**: 249 | ```bash 250 | # If you see: "No output file available for batch job" 251 | # Check: temp/batch/batch_*_error.jsonl for error details 252 | # The system now automatically processes errors and generates default metadata 253 | ``` 254 | 255 | ## Environment Variables 256 | 257 | Optional configuration: 258 | ```bash 259 | # OpenAI for metadata generation 260 | OPENAI_API_KEY=sk-... 261 | OPENAI_MODEL=gpt-4o-mini # Default model 262 | OPENAI_BATCH_SIZE=50 # Batch size for metadata generation 263 | 264 | # Metadata generation limits 265 | METADATA_LIMIT=100 # Max templates to process (0 = all) 266 | ``` 267 | 268 | ## Statistics 269 | 270 | After update, check stats: 271 | ```bash 272 | # Template count 273 | sqlite3 data/nodes.db "SELECT COUNT(*) FROM templates" 274 | 275 | # Most recent template 276 | sqlite3 data/nodes.db "SELECT MAX(created_at) FROM templates" 277 | 278 | # Templates by view count 279 | sqlite3 data/nodes.db "SELECT COUNT(*), 280 | CASE 281 | WHEN views < 50 THEN '<50' 282 | WHEN views < 100 THEN '50-100' 283 | WHEN views < 500 THEN '100-500' 284 | ELSE '500+' 285 | END as view_range 286 | FROM templates GROUP BY view_range" 287 | ``` 288 | 289 | ## Integration with n8n-mcp 290 | 291 | Templates are available through MCP tools: 292 | - `list_templates`: List all templates 293 | - `get_template`: Get specific template with workflow 294 | - `search_templates`: Search by keyword 295 | - `list_node_templates`: Templates using specific nodes 296 | - `get_templates_for_task`: Templates for common tasks 297 | - `search_templates_by_metadata`: Advanced filtering 298 | 299 | See `npm run test:templates` for usage examples. 300 | 301 | ## Time Estimates 302 | 303 | Typical incremental update: 304 | - Loading existing IDs: 1-2 seconds 305 | - Fetching template list: 2-3 minutes 306 | - Filtering new templates: instant 307 | - Fetching details for 100 new templates: ~15 seconds (0.15s each) 308 | - Saving and indexing: 5-10 seconds 309 | - **Total: 3-5 minutes** 310 | 311 | Full rebuild: 312 | - Fetching 8000+ templates: 25-30 minutes 313 | - Saving and indexing: 5-10 minutes 314 | - **Total: 30-40 minutes** 315 | 316 | ## Best Practices 317 | 318 | 1. **Use incremental updates** for regular maintenance 319 | 2. **Rebuild only when necessary** (corruption, major changes) 320 | 3. **Generate metadata incrementally** to avoid OpenAI costs 321 | 4. **Monitor template count** to verify updates working 322 | 5. **Keep database backed up** before major operations 323 | 324 | ## Next Steps 325 | 326 | After updating templates: 327 | 1. Test template search: `npm run test:templates` 328 | 2. Verify MCP tools work: Test in Claude Desktop 329 | 3. Check statistics in database 330 | 4. Commit changes if desired (database changes) 331 | 332 | ## Related Documentation 333 | 334 | - `MEMORY_N8N_UPDATE.md` - Updating n8n dependencies 335 | - `CLAUDE.md` - Project overview and architecture 336 | - `README.md` - User documentation ``` -------------------------------------------------------------------------------- /src/utils/validation-schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Zod validation schemas for MCP tool parameters 3 | * Provides robust input validation with detailed error messages 4 | */ 5 | 6 | // Simple validation without zod for now, since it's not installed 7 | // We can use TypeScript's built-in validation with better error messages 8 | 9 | export class ValidationError extends Error { 10 | constructor(message: string, public field?: string, public value?: any) { 11 | super(message); 12 | this.name = 'ValidationError'; 13 | } 14 | } 15 | 16 | export interface ValidationResult { 17 | valid: boolean; 18 | errors: Array<{ 19 | field: string; 20 | message: string; 21 | value?: any; 22 | }>; 23 | } 24 | 25 | /** 26 | * Basic validation utilities 27 | */ 28 | export class Validator { 29 | /** 30 | * Validate that a value is a non-empty string 31 | */ 32 | static validateString(value: any, fieldName: string, required: boolean = true): ValidationResult { 33 | const errors: Array<{field: string, message: string, value?: any}> = []; 34 | 35 | if (required && (value === undefined || value === null)) { 36 | errors.push({ 37 | field: fieldName, 38 | message: `${fieldName} is required`, 39 | value 40 | }); 41 | } else if (value !== undefined && value !== null && typeof value !== 'string') { 42 | errors.push({ 43 | field: fieldName, 44 | message: `${fieldName} must be a string, got ${typeof value}`, 45 | value 46 | }); 47 | } else if (required && typeof value === 'string' && value.trim().length === 0) { 48 | errors.push({ 49 | field: fieldName, 50 | message: `${fieldName} cannot be empty`, 51 | value 52 | }); 53 | } 54 | 55 | return { 56 | valid: errors.length === 0, 57 | errors 58 | }; 59 | } 60 | 61 | /** 62 | * Validate that a value is a valid object (not null, not array) 63 | */ 64 | static validateObject(value: any, fieldName: string, required: boolean = true): ValidationResult { 65 | const errors: Array<{field: string, message: string, value?: any}> = []; 66 | 67 | if (required && (value === undefined || value === null)) { 68 | errors.push({ 69 | field: fieldName, 70 | message: `${fieldName} is required`, 71 | value 72 | }); 73 | } else if (value !== undefined && value !== null) { 74 | if (typeof value !== 'object') { 75 | errors.push({ 76 | field: fieldName, 77 | message: `${fieldName} must be an object, got ${typeof value}`, 78 | value 79 | }); 80 | } else if (Array.isArray(value)) { 81 | errors.push({ 82 | field: fieldName, 83 | message: `${fieldName} must be an object, not an array`, 84 | value 85 | }); 86 | } 87 | } 88 | 89 | return { 90 | valid: errors.length === 0, 91 | errors 92 | }; 93 | } 94 | 95 | /** 96 | * Validate that a value is an array 97 | */ 98 | static validateArray(value: any, fieldName: string, required: boolean = true): ValidationResult { 99 | const errors: Array<{field: string, message: string, value?: any}> = []; 100 | 101 | if (required && (value === undefined || value === null)) { 102 | errors.push({ 103 | field: fieldName, 104 | message: `${fieldName} is required`, 105 | value 106 | }); 107 | } else if (value !== undefined && value !== null && !Array.isArray(value)) { 108 | errors.push({ 109 | field: fieldName, 110 | message: `${fieldName} must be an array, got ${typeof value}`, 111 | value 112 | }); 113 | } 114 | 115 | return { 116 | valid: errors.length === 0, 117 | errors 118 | }; 119 | } 120 | 121 | /** 122 | * Validate that a value is a number 123 | */ 124 | static validateNumber(value: any, fieldName: string, required: boolean = true, min?: number, max?: number): ValidationResult { 125 | const errors: Array<{field: string, message: string, value?: any}> = []; 126 | 127 | if (required && (value === undefined || value === null)) { 128 | errors.push({ 129 | field: fieldName, 130 | message: `${fieldName} is required`, 131 | value 132 | }); 133 | } else if (value !== undefined && value !== null) { 134 | if (typeof value !== 'number' || isNaN(value)) { 135 | errors.push({ 136 | field: fieldName, 137 | message: `${fieldName} must be a number, got ${typeof value}`, 138 | value 139 | }); 140 | } else { 141 | if (min !== undefined && value < min) { 142 | errors.push({ 143 | field: fieldName, 144 | message: `${fieldName} must be at least ${min}, got ${value}`, 145 | value 146 | }); 147 | } 148 | if (max !== undefined && value > max) { 149 | errors.push({ 150 | field: fieldName, 151 | message: `${fieldName} must be at most ${max}, got ${value}`, 152 | value 153 | }); 154 | } 155 | } 156 | } 157 | 158 | return { 159 | valid: errors.length === 0, 160 | errors 161 | }; 162 | } 163 | 164 | /** 165 | * Validate that a value is one of allowed values 166 | */ 167 | static validateEnum<T>(value: any, fieldName: string, allowedValues: T[], required: boolean = true): ValidationResult { 168 | const errors: Array<{field: string, message: string, value?: any}> = []; 169 | 170 | if (required && (value === undefined || value === null)) { 171 | errors.push({ 172 | field: fieldName, 173 | message: `${fieldName} is required`, 174 | value 175 | }); 176 | } else if (value !== undefined && value !== null && !allowedValues.includes(value)) { 177 | errors.push({ 178 | field: fieldName, 179 | message: `${fieldName} must be one of: ${allowedValues.join(', ')}, got "${value}"`, 180 | value 181 | }); 182 | } 183 | 184 | return { 185 | valid: errors.length === 0, 186 | errors 187 | }; 188 | } 189 | 190 | /** 191 | * Combine multiple validation results 192 | */ 193 | static combineResults(...results: ValidationResult[]): ValidationResult { 194 | const allErrors = results.flatMap(r => r.errors); 195 | return { 196 | valid: allErrors.length === 0, 197 | errors: allErrors 198 | }; 199 | } 200 | 201 | /** 202 | * Create a detailed error message from validation result 203 | */ 204 | static formatErrors(result: ValidationResult, toolName?: string): string { 205 | if (result.valid) return ''; 206 | 207 | const prefix = toolName ? `${toolName}: ` : ''; 208 | const errors = result.errors.map(e => ` • ${e.field}: ${e.message}`).join('\n'); 209 | 210 | return `${prefix}Validation failed:\n${errors}`; 211 | } 212 | } 213 | 214 | /** 215 | * Tool-specific validation schemas 216 | */ 217 | export class ToolValidation { 218 | /** 219 | * Validate parameters for validate_node_operation tool 220 | */ 221 | static validateNodeOperation(args: any): ValidationResult { 222 | const nodeTypeResult = Validator.validateString(args.nodeType, 'nodeType'); 223 | const configResult = Validator.validateObject(args.config, 'config'); 224 | const profileResult = Validator.validateEnum( 225 | args.profile, 226 | 'profile', 227 | ['minimal', 'runtime', 'ai-friendly', 'strict'], 228 | false // optional 229 | ); 230 | 231 | return Validator.combineResults(nodeTypeResult, configResult, profileResult); 232 | } 233 | 234 | /** 235 | * Validate parameters for validate_node_minimal tool 236 | */ 237 | static validateNodeMinimal(args: any): ValidationResult { 238 | const nodeTypeResult = Validator.validateString(args.nodeType, 'nodeType'); 239 | const configResult = Validator.validateObject(args.config, 'config'); 240 | 241 | return Validator.combineResults(nodeTypeResult, configResult); 242 | } 243 | 244 | /** 245 | * Validate parameters for validate_workflow tool 246 | */ 247 | static validateWorkflow(args: any): ValidationResult { 248 | const workflowResult = Validator.validateObject(args.workflow, 'workflow'); 249 | 250 | // Validate workflow structure if it's an object 251 | let nodesResult: ValidationResult = { valid: true, errors: [] }; 252 | let connectionsResult: ValidationResult = { valid: true, errors: [] }; 253 | 254 | if (workflowResult.valid && args.workflow) { 255 | nodesResult = Validator.validateArray(args.workflow.nodes, 'workflow.nodes'); 256 | connectionsResult = Validator.validateObject(args.workflow.connections, 'workflow.connections'); 257 | } 258 | 259 | const optionsResult = args.options ? 260 | Validator.validateObject(args.options, 'options', false) : 261 | { valid: true, errors: [] }; 262 | 263 | return Validator.combineResults(workflowResult, nodesResult, connectionsResult, optionsResult); 264 | } 265 | 266 | /** 267 | * Validate parameters for search_nodes tool 268 | */ 269 | static validateSearchNodes(args: any): ValidationResult { 270 | const queryResult = Validator.validateString(args.query, 'query'); 271 | const limitResult = Validator.validateNumber(args.limit, 'limit', false, 1, 200); 272 | const modeResult = Validator.validateEnum( 273 | args.mode, 274 | 'mode', 275 | ['OR', 'AND', 'FUZZY'], 276 | false 277 | ); 278 | 279 | return Validator.combineResults(queryResult, limitResult, modeResult); 280 | } 281 | 282 | /** 283 | * Validate parameters for list_node_templates tool 284 | */ 285 | static validateListNodeTemplates(args: any): ValidationResult { 286 | const nodeTypesResult = Validator.validateArray(args.nodeTypes, 'nodeTypes'); 287 | const limitResult = Validator.validateNumber(args.limit, 'limit', false, 1, 50); 288 | 289 | return Validator.combineResults(nodeTypesResult, limitResult); 290 | } 291 | 292 | /** 293 | * Validate parameters for n8n workflow operations 294 | */ 295 | static validateWorkflowId(args: any): ValidationResult { 296 | return Validator.validateString(args.id, 'id'); 297 | } 298 | 299 | /** 300 | * Validate parameters for n8n_create_workflow tool 301 | */ 302 | static validateCreateWorkflow(args: any): ValidationResult { 303 | const nameResult = Validator.validateString(args.name, 'name'); 304 | const nodesResult = Validator.validateArray(args.nodes, 'nodes'); 305 | const connectionsResult = Validator.validateObject(args.connections, 'connections'); 306 | const settingsResult = args.settings ? 307 | Validator.validateObject(args.settings, 'settings', false) : 308 | { valid: true, errors: [] }; 309 | 310 | return Validator.combineResults(nameResult, nodesResult, connectionsResult, settingsResult); 311 | } 312 | } ``` -------------------------------------------------------------------------------- /tests/examples/using-database-utils.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { 3 | createTestDatabase, 4 | seedTestNodes, 5 | seedTestTemplates, 6 | createTestNode, 7 | createTestTemplate, 8 | createDatabaseSnapshot, 9 | restoreDatabaseSnapshot, 10 | loadFixtures, 11 | dbHelpers, 12 | TestDatabase 13 | } from '../utils/database-utils'; 14 | import * as path from 'path'; 15 | 16 | /** 17 | * Example test file showing how to use database utilities 18 | * in real test scenarios 19 | */ 20 | 21 | describe('Example: Using Database Utils in Tests', () => { 22 | let testDb: TestDatabase; 23 | 24 | // Always cleanup after each test 25 | afterEach(async () => { 26 | if (testDb) { 27 | await testDb.cleanup(); 28 | } 29 | }); 30 | 31 | describe('Basic Database Setup', () => { 32 | it('should setup a test database for unit testing', async () => { 33 | // Create an in-memory database for fast tests 34 | testDb = await createTestDatabase(); 35 | 36 | // Seed some test data 37 | await seedTestNodes(testDb.nodeRepository, [ 38 | { nodeType: 'nodes-base.myCustomNode', displayName: 'My Custom Node' } 39 | ]); 40 | 41 | // Use the repository to test your logic 42 | const node = testDb.nodeRepository.getNode('nodes-base.myCustomNode'); 43 | expect(node).toBeDefined(); 44 | expect(node.displayName).toBe('My Custom Node'); 45 | }); 46 | 47 | it('should setup a file-based database for integration testing', async () => { 48 | // Create a file-based database when you need persistence 49 | testDb = await createTestDatabase({ 50 | inMemory: false, 51 | dbPath: path.join(__dirname, '../temp/integration-test.db') 52 | }); 53 | 54 | // The database will persist until cleanup() is called 55 | await seedTestNodes(testDb.nodeRepository); 56 | 57 | // You can verify the file exists 58 | expect(testDb.path).toContain('integration-test.db'); 59 | }); 60 | }); 61 | 62 | describe('Testing with Fixtures', () => { 63 | it('should load complex test scenarios from fixtures', async () => { 64 | testDb = await createTestDatabase(); 65 | 66 | // Load fixtures from JSON file 67 | const fixturePath = path.join(__dirname, '../fixtures/database/test-nodes.json'); 68 | await loadFixtures(testDb.adapter, fixturePath); 69 | 70 | // Verify the fixture data was loaded 71 | expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(3); 72 | expect(dbHelpers.countRows(testDb.adapter, 'templates')).toBe(1); 73 | 74 | // Test your business logic with the fixture data 75 | const slackNode = testDb.nodeRepository.getNode('nodes-base.slack'); 76 | expect(slackNode.isAITool).toBe(true); 77 | expect(slackNode.category).toBe('Communication'); 78 | }); 79 | }); 80 | 81 | describe('Testing Repository Methods', () => { 82 | beforeEach(async () => { 83 | testDb = await createTestDatabase(); 84 | }); 85 | 86 | it('should test custom repository queries', async () => { 87 | // Seed nodes with specific properties 88 | await seedTestNodes(testDb.nodeRepository, [ 89 | { nodeType: 'nodes-base.ai1', isAITool: true }, 90 | { nodeType: 'nodes-base.ai2', isAITool: true }, 91 | { nodeType: 'nodes-base.regular', isAITool: false } 92 | ]); 93 | 94 | // Test custom queries 95 | const aiNodes = testDb.nodeRepository.getAITools(); 96 | expect(aiNodes).toHaveLength(4); // 2 custom + 2 default (httpRequest, slack) 97 | 98 | // Use dbHelpers for quick checks 99 | const allNodeTypes = dbHelpers.getAllNodeTypes(testDb.adapter); 100 | expect(allNodeTypes).toContain('nodes-base.ai1'); 101 | expect(allNodeTypes).toContain('nodes-base.ai2'); 102 | }); 103 | }); 104 | 105 | describe('Testing with Snapshots', () => { 106 | it('should test rollback scenarios using snapshots', async () => { 107 | testDb = await createTestDatabase(); 108 | 109 | // Setup initial state 110 | await seedTestNodes(testDb.nodeRepository); 111 | await seedTestTemplates(testDb.templateRepository); 112 | 113 | // Create a snapshot of the good state 114 | const snapshot = await createDatabaseSnapshot(testDb.adapter); 115 | 116 | // Perform operations that might fail 117 | try { 118 | // Simulate a complex operation 119 | await testDb.nodeRepository.saveNode(createTestNode({ 120 | nodeType: 'nodes-base.problematic', 121 | displayName: 'This might cause issues' 122 | })); 123 | 124 | // Simulate an error 125 | throw new Error('Something went wrong!'); 126 | } catch (error) { 127 | // Restore to the known good state 128 | await restoreDatabaseSnapshot(testDb.adapter, snapshot); 129 | } 130 | 131 | // Verify we're back to the original state 132 | expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(snapshot.metadata.nodeCount); 133 | expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.problematic')).toBe(false); 134 | }); 135 | }); 136 | 137 | describe('Testing Database Performance', () => { 138 | it('should measure performance of database operations', async () => { 139 | testDb = await createTestDatabase(); 140 | 141 | // Measure bulk insert performance 142 | const insertDuration = await measureDatabaseOperation('Bulk Insert', async () => { 143 | const nodes = Array.from({ length: 100 }, (_, i) => 144 | createTestNode({ 145 | nodeType: `nodes-base.perf${i}`, 146 | displayName: `Performance Test Node ${i}` 147 | }) 148 | ); 149 | 150 | for (const node of nodes) { 151 | testDb.nodeRepository.saveNode(node); 152 | } 153 | }); 154 | 155 | // Measure query performance 156 | const queryDuration = await measureDatabaseOperation('Query All Nodes', async () => { 157 | const allNodes = testDb.nodeRepository.getAllNodes(); 158 | expect(allNodes.length).toBe(100); // 100 bulk nodes (no defaults as we're not using seedTestNodes) 159 | }); 160 | 161 | // Assert reasonable performance 162 | expect(insertDuration).toBeLessThan(1000); // Should complete in under 1 second 163 | expect(queryDuration).toBeLessThan(100); // Queries should be fast 164 | }); 165 | }); 166 | 167 | describe('Testing with Different Database States', () => { 168 | it('should test behavior with empty database', async () => { 169 | testDb = await createTestDatabase(); 170 | 171 | // Test with empty database 172 | expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(0); 173 | 174 | const nonExistentNode = testDb.nodeRepository.getNode('nodes-base.doesnotexist'); 175 | expect(nonExistentNode).toBeNull(); 176 | }); 177 | 178 | it('should test behavior with populated database', async () => { 179 | testDb = await createTestDatabase(); 180 | 181 | // Populate with many nodes 182 | const nodes = Array.from({ length: 50 }, (_, i) => ({ 183 | nodeType: `nodes-base.node${i}`, 184 | displayName: `Node ${i}`, 185 | category: i % 2 === 0 ? 'Category A' : 'Category B' 186 | })); 187 | 188 | await seedTestNodes(testDb.nodeRepository, nodes); 189 | 190 | // Test queries on populated database 191 | const allNodes = dbHelpers.getAllNodeTypes(testDb.adapter); 192 | expect(allNodes.length).toBe(53); // 50 custom + 3 default 193 | 194 | // Test filtering by category 195 | const categoryANodes = testDb.adapter 196 | .prepare('SELECT COUNT(*) as count FROM nodes WHERE category = ?') 197 | .get('Category A') as { count: number }; 198 | 199 | expect(categoryANodes.count).toBe(25); 200 | }); 201 | }); 202 | 203 | describe('Testing Error Scenarios', () => { 204 | it('should handle database errors gracefully', async () => { 205 | testDb = await createTestDatabase(); 206 | 207 | // Test saving invalid data 208 | const invalidNode = createTestNode({ 209 | nodeType: '', // Invalid: empty nodeType 210 | displayName: 'Invalid Node' 211 | }); 212 | 213 | // SQLite allows NULL in PRIMARY KEY, so test with empty string instead 214 | // which should violate any business logic constraints 215 | // For now, we'll just verify the save doesn't crash 216 | expect(() => { 217 | testDb.nodeRepository.saveNode(invalidNode); 218 | }).not.toThrow(); 219 | 220 | // Database should still be functional 221 | await seedTestNodes(testDb.nodeRepository); 222 | expect(dbHelpers.countRows(testDb.adapter, 'nodes')).toBe(4); // 3 default nodes + 1 invalid node 223 | }); 224 | }); 225 | 226 | describe('Testing with Transactions', () => { 227 | it('should test transactional behavior', async () => { 228 | testDb = await createTestDatabase(); 229 | 230 | // Seed initial data 231 | await seedTestNodes(testDb.nodeRepository); 232 | const initialCount = dbHelpers.countRows(testDb.adapter, 'nodes'); 233 | 234 | // Use transaction for atomic operations 235 | try { 236 | testDb.adapter.transaction(() => { 237 | // Add multiple nodes atomically 238 | testDb.nodeRepository.saveNode(createTestNode({ nodeType: 'nodes-base.tx1' })); 239 | testDb.nodeRepository.saveNode(createTestNode({ nodeType: 'nodes-base.tx2' })); 240 | 241 | // Simulate error in transaction 242 | throw new Error('Transaction failed'); 243 | }); 244 | } catch (error) { 245 | // Transaction should have rolled back 246 | } 247 | 248 | // Verify no nodes were added 249 | const finalCount = dbHelpers.countRows(testDb.adapter, 'nodes'); 250 | expect(finalCount).toBe(initialCount); 251 | expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.tx1')).toBe(false); 252 | expect(dbHelpers.nodeExists(testDb.adapter, 'nodes-base.tx2')).toBe(false); 253 | }); 254 | }); 255 | }); 256 | 257 | // Helper function for performance measurement 258 | async function measureDatabaseOperation( 259 | name: string, 260 | operation: () => Promise<void> 261 | ): Promise<number> { 262 | const start = performance.now(); 263 | await operation(); 264 | const duration = performance.now() - start; 265 | console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`); 266 | return duration; 267 | } ``` -------------------------------------------------------------------------------- /tests/integration/database/sqljs-memory-leak.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 2 | import { promises as fs } from 'fs'; 3 | import * as path from 'path'; 4 | import * as os from 'os'; 5 | 6 | /** 7 | * Integration tests for sql.js memory leak fix (Issue #330) 8 | * 9 | * These tests verify that the SQLJSAdapter optimizations: 10 | * 1. Use configurable save intervals (default 5000ms) 11 | * 2. Don't trigger saves on read-only operations 12 | * 3. Batch multiple rapid writes into single save 13 | * 4. Clean up resources properly 14 | * 15 | * Note: These tests use actual sql.js adapter behavior patterns 16 | * to verify the fix works under realistic load. 17 | */ 18 | 19 | describe('SQLJSAdapter Memory Leak Prevention (Issue #330)', () => { 20 | let tempDbPath: string; 21 | 22 | beforeEach(async () => { 23 | // Create temporary database file path 24 | const tempDir = os.tmpdir(); 25 | tempDbPath = path.join(tempDir, `test-sqljs-${Date.now()}.db`); 26 | }); 27 | 28 | afterEach(async () => { 29 | // Cleanup temporary file 30 | try { 31 | await fs.unlink(tempDbPath); 32 | } catch (error) { 33 | // File might not exist, ignore error 34 | } 35 | }); 36 | 37 | describe('Save Interval Configuration', () => { 38 | it('should respect SQLJS_SAVE_INTERVAL_MS environment variable', () => { 39 | const originalEnv = process.env.SQLJS_SAVE_INTERVAL_MS; 40 | 41 | try { 42 | // Set custom interval 43 | process.env.SQLJS_SAVE_INTERVAL_MS = '10000'; 44 | 45 | // Verify parsing logic 46 | const envInterval = process.env.SQLJS_SAVE_INTERVAL_MS; 47 | const interval = envInterval ? parseInt(envInterval, 10) : 5000; 48 | 49 | expect(interval).toBe(10000); 50 | } finally { 51 | // Restore environment 52 | if (originalEnv !== undefined) { 53 | process.env.SQLJS_SAVE_INTERVAL_MS = originalEnv; 54 | } else { 55 | delete process.env.SQLJS_SAVE_INTERVAL_MS; 56 | } 57 | } 58 | }); 59 | 60 | it('should use default 5000ms when env var is not set', () => { 61 | const originalEnv = process.env.SQLJS_SAVE_INTERVAL_MS; 62 | 63 | try { 64 | // Ensure env var is not set 65 | delete process.env.SQLJS_SAVE_INTERVAL_MS; 66 | 67 | // Verify default is used 68 | const envInterval = process.env.SQLJS_SAVE_INTERVAL_MS; 69 | const interval = envInterval ? parseInt(envInterval, 10) : 5000; 70 | 71 | expect(interval).toBe(5000); 72 | } finally { 73 | // Restore environment 74 | if (originalEnv !== undefined) { 75 | process.env.SQLJS_SAVE_INTERVAL_MS = originalEnv; 76 | } 77 | } 78 | }); 79 | 80 | it('should validate and reject invalid intervals', () => { 81 | const invalidValues = [ 82 | 'invalid', 83 | '50', // Too low (< 100ms) 84 | '-100', // Negative 85 | '0', // Zero 86 | '', // Empty string 87 | ]; 88 | 89 | invalidValues.forEach((invalidValue) => { 90 | const parsed = parseInt(invalidValue, 10); 91 | const interval = (isNaN(parsed) || parsed < 100) ? 5000 : parsed; 92 | 93 | // All invalid values should fall back to 5000 94 | expect(interval).toBe(5000); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('Save Debouncing Behavior', () => { 100 | it('should debounce multiple rapid write operations', async () => { 101 | const saveCallback = vi.fn(); 102 | let timer: NodeJS.Timeout | null = null; 103 | const saveInterval = 100; // Use short interval for test speed 104 | 105 | // Simulate scheduleSave() logic 106 | const scheduleSave = () => { 107 | if (timer) { 108 | clearTimeout(timer); 109 | } 110 | timer = setTimeout(() => { 111 | saveCallback(); 112 | }, saveInterval); 113 | }; 114 | 115 | // Simulate 10 rapid write operations 116 | for (let i = 0; i < 10; i++) { 117 | scheduleSave(); 118 | } 119 | 120 | // Should not have saved yet (still debouncing) 121 | expect(saveCallback).not.toHaveBeenCalled(); 122 | 123 | // Wait for debounce interval 124 | await new Promise(resolve => setTimeout(resolve, saveInterval + 50)); 125 | 126 | // Should have saved exactly once (all 10 operations batched) 127 | expect(saveCallback).toHaveBeenCalledTimes(1); 128 | 129 | // Cleanup 130 | if (timer) clearTimeout(timer); 131 | }); 132 | 133 | it('should not accumulate save timers (memory leak prevention)', () => { 134 | let timer: NodeJS.Timeout | null = null; 135 | const timers: NodeJS.Timeout[] = []; 136 | 137 | const scheduleSave = () => { 138 | // Critical: clear existing timer before creating new one 139 | if (timer) { 140 | clearTimeout(timer); 141 | } 142 | 143 | timer = setTimeout(() => { 144 | // Save logic 145 | }, 5000); 146 | 147 | timers.push(timer); 148 | }; 149 | 150 | // Simulate 100 rapid operations 151 | for (let i = 0; i < 100; i++) { 152 | scheduleSave(); 153 | } 154 | 155 | // Should have created 100 timers total 156 | expect(timers.length).toBe(100); 157 | 158 | // But only 1 timer should be active (others cleared) 159 | // This is the key to preventing timer leak 160 | 161 | // Cleanup active timer 162 | if (timer) clearTimeout(timer); 163 | }); 164 | }); 165 | 166 | describe('Read vs Write Operation Handling', () => { 167 | it('should not trigger save on SELECT queries', () => { 168 | const saveCallback = vi.fn(); 169 | 170 | // Simulate prepare() for SELECT 171 | // Old code: would call scheduleSave() here (bug) 172 | // New code: does NOT call scheduleSave() 173 | 174 | // prepare() should not trigger save 175 | expect(saveCallback).not.toHaveBeenCalled(); 176 | }); 177 | 178 | it('should trigger save only on write operations', () => { 179 | const saveCallback = vi.fn(); 180 | 181 | // Simulate exec() for INSERT 182 | saveCallback(); // exec() calls scheduleSave() 183 | 184 | // Simulate run() for UPDATE 185 | saveCallback(); // run() calls scheduleSave() 186 | 187 | // Should have scheduled saves for write operations 188 | expect(saveCallback).toHaveBeenCalledTimes(2); 189 | }); 190 | }); 191 | 192 | describe('Memory Allocation Optimization', () => { 193 | it('should not use Buffer.from() for Uint8Array', () => { 194 | // Original code (memory leak): 195 | // const data = db.export(); // 2-5MB Uint8Array 196 | // const buffer = Buffer.from(data); // Another 2-5MB copy! 197 | // fsSync.writeFileSync(path, buffer); 198 | 199 | // Fixed code (no copy): 200 | // const data = db.export(); // 2-5MB Uint8Array 201 | // fsSync.writeFileSync(path, data); // Write directly 202 | 203 | const mockData = new Uint8Array(1024 * 1024 * 2); // 2MB 204 | 205 | // Verify Uint8Array can be used directly (no Buffer.from needed) 206 | expect(mockData).toBeInstanceOf(Uint8Array); 207 | expect(mockData.byteLength).toBe(2 * 1024 * 1024); 208 | 209 | // The fix eliminates the Buffer.from() step entirely 210 | // This saves 50% of temporary memory allocations 211 | }); 212 | 213 | it('should cleanup data reference after save', () => { 214 | let data: Uint8Array | null = null; 215 | let savedSuccessfully = false; 216 | 217 | try { 218 | // Simulate export 219 | data = new Uint8Array(1024); 220 | 221 | // Simulate write 222 | savedSuccessfully = true; 223 | } catch (error) { 224 | savedSuccessfully = false; 225 | } finally { 226 | // Critical: null out reference to help GC 227 | data = null; 228 | } 229 | 230 | expect(savedSuccessfully).toBe(true); 231 | expect(data).toBeNull(); 232 | }); 233 | 234 | it('should cleanup even when save fails', () => { 235 | let data: Uint8Array | null = null; 236 | let errorCaught = false; 237 | 238 | try { 239 | data = new Uint8Array(1024); 240 | throw new Error('Simulated save failure'); 241 | } catch (error) { 242 | errorCaught = true; 243 | } finally { 244 | // Cleanup must happen even on error 245 | data = null; 246 | } 247 | 248 | expect(errorCaught).toBe(true); 249 | expect(data).toBeNull(); 250 | }); 251 | }); 252 | 253 | describe('Load Test Simulation', () => { 254 | it('should handle 100 operations without excessive memory growth', async () => { 255 | const saveCallback = vi.fn(); 256 | let timer: NodeJS.Timeout | null = null; 257 | const saveInterval = 50; // Fast for testing 258 | 259 | const scheduleSave = () => { 260 | if (timer) { 261 | clearTimeout(timer); 262 | } 263 | timer = setTimeout(() => { 264 | saveCallback(); 265 | }, saveInterval); 266 | }; 267 | 268 | // Simulate 100 database operations 269 | for (let i = 0; i < 100; i++) { 270 | scheduleSave(); 271 | 272 | // Simulate varying operation speeds 273 | if (i % 10 === 0) { 274 | await new Promise(resolve => setTimeout(resolve, 10)); 275 | } 276 | } 277 | 278 | // Wait for final save 279 | await new Promise(resolve => setTimeout(resolve, saveInterval + 50)); 280 | 281 | // With old code (100ms interval, save on every operation): 282 | // - Would trigger ~100 saves 283 | // - Each save: 4-10MB temporary allocation 284 | // - Total temporary memory: 400-1000MB 285 | 286 | // With new code (5000ms interval, debounced): 287 | // - Triggers only a few saves (operations batched) 288 | // - Same temporary allocation per save 289 | // - Total temporary memory: ~20-50MB (90-95% reduction) 290 | 291 | // Should have saved much fewer times than operations (batching works) 292 | expect(saveCallback.mock.calls.length).toBeLessThan(10); 293 | 294 | // Cleanup 295 | if (timer) clearTimeout(timer); 296 | }); 297 | }); 298 | 299 | describe('Long-Running Deployment Simulation', () => { 300 | it('should not accumulate references over time', () => { 301 | const operations: any[] = []; 302 | 303 | // Simulate 1000 operations (representing hours of runtime) 304 | for (let i = 0; i < 1000; i++) { 305 | let data: Uint8Array | null = new Uint8Array(1024); 306 | 307 | // Simulate operation 308 | operations.push({ index: i }); 309 | 310 | // Critical: cleanup after each operation 311 | data = null; 312 | } 313 | 314 | expect(operations.length).toBe(1000); 315 | 316 | // Key point: each operation's data reference was nulled 317 | // In old code, these would accumulate in memory 318 | // In new code, GC can reclaim them 319 | }); 320 | }); 321 | }); 322 | ``` -------------------------------------------------------------------------------- /src/scripts/test-execution-filtering.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | /** 3 | * Manual testing script for execution filtering feature 4 | * 5 | * This script demonstrates all modes of the n8n_get_execution tool 6 | * with various filtering options. 7 | * 8 | * Usage: npx tsx src/scripts/test-execution-filtering.ts 9 | */ 10 | 11 | import { 12 | generatePreview, 13 | filterExecutionData, 14 | processExecution, 15 | } from '../services/execution-processor'; 16 | import { ExecutionFilterOptions, Execution, ExecutionStatus } from '../types/n8n-api'; 17 | 18 | console.log('='.repeat(80)); 19 | console.log('Execution Filtering Feature - Manual Test Suite'); 20 | console.log('='.repeat(80)); 21 | console.log(''); 22 | 23 | /** 24 | * Mock execution factory (simplified version for testing) 25 | */ 26 | function createTestExecution(itemCount: number): Execution { 27 | const items = Array.from({ length: itemCount }, (_, i) => ({ 28 | json: { 29 | id: i + 1, 30 | name: `Item ${i + 1}`, 31 | email: `user${i}@example.com`, 32 | value: Math.random() * 1000, 33 | metadata: { 34 | createdAt: new Date().toISOString(), 35 | tags: ['tag1', 'tag2'], 36 | }, 37 | }, 38 | })); 39 | 40 | return { 41 | id: `test-exec-${Date.now()}`, 42 | workflowId: 'workflow-test', 43 | status: ExecutionStatus.SUCCESS, 44 | mode: 'manual', 45 | finished: true, 46 | startedAt: '2024-01-01T10:00:00.000Z', 47 | stoppedAt: '2024-01-01T10:00:05.000Z', 48 | data: { 49 | resultData: { 50 | runData: { 51 | 'HTTP Request': [ 52 | { 53 | startTime: Date.now(), 54 | executionTime: 234, 55 | data: { 56 | main: [items], 57 | }, 58 | }, 59 | ], 60 | 'Filter': [ 61 | { 62 | startTime: Date.now(), 63 | executionTime: 45, 64 | data: { 65 | main: [items.slice(0, Math.floor(itemCount / 2))], 66 | }, 67 | }, 68 | ], 69 | 'Set': [ 70 | { 71 | startTime: Date.now(), 72 | executionTime: 12, 73 | data: { 74 | main: [items.slice(0, 5)], 75 | }, 76 | }, 77 | ], 78 | }, 79 | }, 80 | }, 81 | }; 82 | } 83 | 84 | /** 85 | * Test 1: Preview Mode 86 | */ 87 | console.log('📊 TEST 1: Preview Mode (No Data, Just Structure)'); 88 | console.log('-'.repeat(80)); 89 | 90 | const execution1 = createTestExecution(50); 91 | const { preview, recommendation } = generatePreview(execution1); 92 | 93 | console.log('Preview:', JSON.stringify(preview, null, 2)); 94 | console.log('\nRecommendation:', JSON.stringify(recommendation, null, 2)); 95 | console.log('\n✅ Preview mode shows structure without consuming tokens for data\n'); 96 | 97 | /** 98 | * Test 2: Summary Mode (Default) 99 | */ 100 | console.log('📝 TEST 2: Summary Mode (2 items per node)'); 101 | console.log('-'.repeat(80)); 102 | 103 | const execution2 = createTestExecution(50); 104 | const summaryResult = filterExecutionData(execution2, { mode: 'summary' }); 105 | 106 | console.log('Summary Mode Result:'); 107 | console.log('- Mode:', summaryResult.mode); 108 | console.log('- Summary:', JSON.stringify(summaryResult.summary, null, 2)); 109 | console.log('- HTTP Request items shown:', summaryResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown); 110 | console.log('- HTTP Request truncated:', summaryResult.nodes?.['HTTP Request']?.data?.metadata.truncated); 111 | console.log('\n✅ Summary mode returns 2 items per node (safe default)\n'); 112 | 113 | /** 114 | * Test 3: Filtered Mode with Custom Limit 115 | */ 116 | console.log('🎯 TEST 3: Filtered Mode (Custom itemsLimit: 5)'); 117 | console.log('-'.repeat(80)); 118 | 119 | const execution3 = createTestExecution(100); 120 | const filteredResult = filterExecutionData(execution3, { 121 | mode: 'filtered', 122 | itemsLimit: 5, 123 | }); 124 | 125 | console.log('Filtered Mode Result:'); 126 | console.log('- Items shown per node:', filteredResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown); 127 | console.log('- Total items available:', filteredResult.nodes?.['HTTP Request']?.data?.metadata.totalItems); 128 | console.log('- More data available:', filteredResult.summary?.hasMoreData); 129 | console.log('\n✅ Filtered mode allows custom item limits\n'); 130 | 131 | /** 132 | * Test 4: Node Name Filtering 133 | */ 134 | console.log('🔍 TEST 4: Filter to Specific Nodes'); 135 | console.log('-'.repeat(80)); 136 | 137 | const execution4 = createTestExecution(30); 138 | const nodeFilterResult = filterExecutionData(execution4, { 139 | mode: 'filtered', 140 | nodeNames: ['HTTP Request'], 141 | itemsLimit: 3, 142 | }); 143 | 144 | console.log('Node Filter Result:'); 145 | console.log('- Nodes in result:', Object.keys(nodeFilterResult.nodes || {})); 146 | console.log('- Expected: ["HTTP Request"]'); 147 | console.log('- Executed nodes:', nodeFilterResult.summary?.executedNodes); 148 | console.log('- Total nodes:', nodeFilterResult.summary?.totalNodes); 149 | console.log('\n✅ Can filter to specific nodes only\n'); 150 | 151 | /** 152 | * Test 5: Structure-Only Mode (itemsLimit: 0) 153 | */ 154 | console.log('🏗️ TEST 5: Structure-Only Mode (itemsLimit: 0)'); 155 | console.log('-'.repeat(80)); 156 | 157 | const execution5 = createTestExecution(100); 158 | const structureResult = filterExecutionData(execution5, { 159 | mode: 'filtered', 160 | itemsLimit: 0, 161 | }); 162 | 163 | console.log('Structure-Only Result:'); 164 | console.log('- Items shown:', structureResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown); 165 | console.log('- First item (structure):', JSON.stringify( 166 | structureResult.nodes?.['HTTP Request']?.data?.output?.[0]?.[0], 167 | null, 168 | 2 169 | )); 170 | console.log('\n✅ Structure-only mode shows data shape without values\n'); 171 | 172 | /** 173 | * Test 6: Full Mode 174 | */ 175 | console.log('💾 TEST 6: Full Mode (All Data)'); 176 | console.log('-'.repeat(80)); 177 | 178 | const execution6 = createTestExecution(5); // Small dataset 179 | const fullResult = filterExecutionData(execution6, { mode: 'full' }); 180 | 181 | console.log('Full Mode Result:'); 182 | console.log('- Items shown:', fullResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown); 183 | console.log('- Total items:', fullResult.nodes?.['HTTP Request']?.data?.metadata.totalItems); 184 | console.log('- Truncated:', fullResult.nodes?.['HTTP Request']?.data?.metadata.truncated); 185 | console.log('\n✅ Full mode returns all data (use with caution)\n'); 186 | 187 | /** 188 | * Test 7: Backward Compatibility 189 | */ 190 | console.log('🔄 TEST 7: Backward Compatibility (No Filtering)'); 191 | console.log('-'.repeat(80)); 192 | 193 | const execution7 = createTestExecution(10); 194 | const legacyResult = processExecution(execution7, {}); 195 | 196 | console.log('Legacy Result:'); 197 | console.log('- Returns original execution:', legacyResult === execution7); 198 | console.log('- Type:', typeof legacyResult); 199 | console.log('\n✅ Backward compatible - no options returns original execution\n'); 200 | 201 | /** 202 | * Test 8: Input Data Inclusion 203 | */ 204 | console.log('🔗 TEST 8: Include Input Data'); 205 | console.log('-'.repeat(80)); 206 | 207 | const execution8 = createTestExecution(5); 208 | const inputDataResult = filterExecutionData(execution8, { 209 | mode: 'filtered', 210 | itemsLimit: 2, 211 | includeInputData: true, 212 | }); 213 | 214 | console.log('Input Data Result:'); 215 | console.log('- Has input data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.input); 216 | console.log('- Has output data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.output); 217 | console.log('\n✅ Can include input data for debugging\n'); 218 | 219 | /** 220 | * Test 9: itemsLimit Validation 221 | */ 222 | console.log('⚠️ TEST 9: itemsLimit Validation'); 223 | console.log('-'.repeat(80)); 224 | 225 | const execution9 = createTestExecution(50); 226 | 227 | // Test negative value 228 | const negativeResult = filterExecutionData(execution9, { 229 | mode: 'filtered', 230 | itemsLimit: -5, 231 | }); 232 | console.log('- Negative itemsLimit (-5) handled:', negativeResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown === 2); 233 | 234 | // Test very large value 235 | const largeResult = filterExecutionData(execution9, { 236 | mode: 'filtered', 237 | itemsLimit: 999999, 238 | }); 239 | console.log('- Large itemsLimit (999999) capped:', (largeResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown || 0) <= 1000); 240 | 241 | // Test unlimited (-1) 242 | const unlimitedResult = filterExecutionData(execution9, { 243 | mode: 'filtered', 244 | itemsLimit: -1, 245 | }); 246 | console.log('- Unlimited itemsLimit (-1) works:', unlimitedResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown === 50); 247 | 248 | console.log('\n✅ itemsLimit validation works correctly\n'); 249 | 250 | /** 251 | * Test 10: Recommendation Following 252 | */ 253 | console.log('🎯 TEST 10: Follow Recommendation Workflow'); 254 | console.log('-'.repeat(80)); 255 | 256 | const execution10 = createTestExecution(100); 257 | const { preview: preview10, recommendation: rec10 } = generatePreview(execution10); 258 | 259 | console.log('1. Preview shows:', { 260 | totalItems: preview10.nodes['HTTP Request']?.itemCounts.output, 261 | sizeKB: preview10.estimatedSizeKB, 262 | }); 263 | 264 | console.log('\n2. Recommendation:', { 265 | canFetchFull: rec10.canFetchFull, 266 | suggestedMode: rec10.suggestedMode, 267 | suggestedItemsLimit: rec10.suggestedItemsLimit, 268 | reason: rec10.reason, 269 | }); 270 | 271 | // Follow recommendation 272 | const options: ExecutionFilterOptions = { 273 | mode: rec10.suggestedMode, 274 | itemsLimit: rec10.suggestedItemsLimit, 275 | }; 276 | 277 | const recommendedResult = filterExecutionData(execution10, options); 278 | 279 | console.log('\n3. Following recommendation gives:', { 280 | mode: recommendedResult.mode, 281 | itemsShown: recommendedResult.nodes?.['HTTP Request']?.data?.metadata.itemsShown, 282 | hasMoreData: recommendedResult.summary?.hasMoreData, 283 | }); 284 | 285 | console.log('\n✅ Recommendation workflow helps make optimal choices\n'); 286 | 287 | /** 288 | * Summary 289 | */ 290 | console.log('='.repeat(80)); 291 | console.log('✨ All Tests Completed Successfully!'); 292 | console.log('='.repeat(80)); 293 | console.log('\n🎉 Execution Filtering Feature is Working!\n'); 294 | console.log('Key Takeaways:'); 295 | console.log('1. Always use preview mode first for unknown datasets'); 296 | console.log('2. Follow the recommendation for optimal token usage'); 297 | console.log('3. Use nodeNames to filter to relevant nodes'); 298 | console.log('4. itemsLimit: 0 shows structure without data'); 299 | console.log('5. itemsLimit: -1 returns unlimited items (use with caution)'); 300 | console.log('6. Summary mode (2 items) is a safe default'); 301 | console.log('7. Full mode should only be used for small datasets'); 302 | console.log(''); 303 | ``` -------------------------------------------------------------------------------- /tests/utils/builders/workflow.builder.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | // Type definitions 4 | export interface INodeParameters { 5 | [key: string]: any; 6 | } 7 | 8 | export interface INodeCredentials { 9 | [credentialType: string]: { 10 | id?: string; 11 | name: string; 12 | }; 13 | } 14 | 15 | export interface INode { 16 | id: string; 17 | name: string; 18 | type: string; 19 | typeVersion: number; 20 | position: [number, number]; 21 | parameters: INodeParameters; 22 | credentials?: INodeCredentials; 23 | disabled?: boolean; 24 | notes?: string; 25 | continueOnFail?: boolean; 26 | retryOnFail?: boolean; 27 | maxTries?: number; 28 | waitBetweenTries?: number; 29 | onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow'; 30 | } 31 | 32 | export interface IConnection { 33 | node: string; 34 | type: 'main'; 35 | index: number; 36 | } 37 | 38 | export interface IConnections { 39 | [nodeId: string]: { 40 | [outputType: string]: Array<Array<IConnection | null>>; 41 | }; 42 | } 43 | 44 | export interface IWorkflowSettings { 45 | executionOrder?: 'v0' | 'v1'; 46 | saveDataErrorExecution?: 'all' | 'none'; 47 | saveDataSuccessExecution?: 'all' | 'none'; 48 | saveManualExecutions?: boolean; 49 | saveExecutionProgress?: boolean; 50 | executionTimeout?: number; 51 | errorWorkflow?: string; 52 | timezone?: string; 53 | } 54 | 55 | export interface IWorkflow { 56 | id?: string; 57 | name: string; 58 | nodes: INode[]; 59 | connections: IConnections; 60 | active?: boolean; 61 | settings?: IWorkflowSettings; 62 | staticData?: any; 63 | tags?: string[]; 64 | pinData?: any; 65 | versionId?: string; 66 | meta?: { 67 | instanceId?: string; 68 | }; 69 | } 70 | 71 | // Type guard for INode validation 72 | function isValidNode(node: any): node is INode { 73 | return ( 74 | typeof node === 'object' && 75 | typeof node.id === 'string' && 76 | typeof node.name === 'string' && 77 | typeof node.type === 'string' && 78 | typeof node.typeVersion === 'number' && 79 | Array.isArray(node.position) && 80 | node.position.length === 2 && 81 | typeof node.position[0] === 'number' && 82 | typeof node.position[1] === 'number' && 83 | typeof node.parameters === 'object' 84 | ); 85 | } 86 | 87 | export class WorkflowBuilder { 88 | private workflow: IWorkflow; 89 | private nodeCounter = 0; 90 | private defaultPosition: [number, number] = [250, 300]; 91 | private positionIncrement = 280; 92 | 93 | constructor(name = 'Test Workflow') { 94 | this.workflow = { 95 | name, 96 | nodes: [], 97 | connections: {}, 98 | active: false, 99 | settings: { 100 | executionOrder: 'v1', 101 | saveDataErrorExecution: 'all', 102 | saveDataSuccessExecution: 'all', 103 | saveManualExecutions: true, 104 | saveExecutionProgress: true, 105 | }, 106 | }; 107 | } 108 | 109 | /** 110 | * Add a node to the workflow 111 | */ 112 | addNode(node: Partial<INode> & { type: string; typeVersion: number }): this { 113 | const nodeId = node.id || uuidv4(); 114 | const nodeName = node.name || `${node.type} ${++this.nodeCounter}`; 115 | 116 | const fullNode: INode = { 117 | ...node, // Spread first to allow overrides 118 | id: nodeId, 119 | name: nodeName, 120 | type: node.type, 121 | typeVersion: node.typeVersion, 122 | position: node.position || this.getNextPosition(), 123 | parameters: node.parameters || {}, 124 | }; 125 | 126 | this.workflow.nodes.push(fullNode); 127 | return this; 128 | } 129 | 130 | /** 131 | * Add a webhook node (common trigger) 132 | */ 133 | addWebhookNode(options: Partial<INode> = {}): this { 134 | return this.addNode({ 135 | type: 'n8n-nodes-base.webhook', 136 | typeVersion: 2, 137 | parameters: { 138 | path: 'test-webhook', 139 | method: 'POST', 140 | responseMode: 'onReceived', 141 | responseData: 'allEntries', 142 | responsePropertyName: 'data', 143 | ...options.parameters, 144 | }, 145 | ...options, 146 | }); 147 | } 148 | 149 | /** 150 | * Add a Slack node 151 | */ 152 | addSlackNode(options: Partial<INode> = {}): this { 153 | return this.addNode({ 154 | type: 'n8n-nodes-base.slack', 155 | typeVersion: 2.2, 156 | parameters: { 157 | resource: 'message', 158 | operation: 'post', 159 | channel: '#general', 160 | text: 'Test message', 161 | ...options.parameters, 162 | }, 163 | credentials: { 164 | slackApi: { 165 | name: 'Slack Account', 166 | }, 167 | }, 168 | ...options, 169 | }); 170 | } 171 | 172 | /** 173 | * Add an HTTP Request node 174 | */ 175 | addHttpRequestNode(options: Partial<INode> = {}): this { 176 | return this.addNode({ 177 | type: 'n8n-nodes-base.httpRequest', 178 | typeVersion: 4.2, 179 | parameters: { 180 | method: 'GET', 181 | url: 'https://api.example.com/data', 182 | authentication: 'none', 183 | ...options.parameters, 184 | }, 185 | ...options, 186 | }); 187 | } 188 | 189 | /** 190 | * Add a Code node 191 | */ 192 | addCodeNode(options: Partial<INode> = {}): this { 193 | return this.addNode({ 194 | type: 'n8n-nodes-base.code', 195 | typeVersion: 2, 196 | parameters: { 197 | mode: 'runOnceForAllItems', 198 | language: 'javaScript', 199 | jsCode: 'return items;', 200 | ...options.parameters, 201 | }, 202 | ...options, 203 | }); 204 | } 205 | 206 | /** 207 | * Add an IF node 208 | */ 209 | addIfNode(options: Partial<INode> = {}): this { 210 | return this.addNode({ 211 | type: 'n8n-nodes-base.if', 212 | typeVersion: 2, 213 | parameters: { 214 | conditions: { 215 | options: { 216 | caseSensitive: true, 217 | leftValue: '', 218 | typeValidation: 'strict', 219 | }, 220 | conditions: [ 221 | { 222 | id: uuidv4(), 223 | leftValue: '={{ $json.value }}', 224 | rightValue: 'test', 225 | operator: { 226 | type: 'string', 227 | operation: 'equals', 228 | }, 229 | }, 230 | ], 231 | combinator: 'and', 232 | }, 233 | ...options.parameters, 234 | }, 235 | ...options, 236 | }); 237 | } 238 | 239 | /** 240 | * Add an AI Agent node 241 | */ 242 | addAiAgentNode(options: Partial<INode> = {}): this { 243 | return this.addNode({ 244 | type: '@n8n/n8n-nodes-langchain.agent', 245 | typeVersion: 1.7, 246 | parameters: { 247 | agent: 'conversationalAgent', 248 | promptType: 'define', 249 | text: '={{ $json.prompt }}', 250 | ...options.parameters, 251 | }, 252 | ...options, 253 | }); 254 | } 255 | 256 | /** 257 | * Connect two nodes 258 | * @param sourceNodeId - ID of the source node 259 | * @param targetNodeId - ID of the target node 260 | * @param sourceOutput - Output index on the source node (default: 0) 261 | * @param targetInput - Input index on the target node (default: 0) 262 | * @returns The WorkflowBuilder instance for chaining 263 | * @example 264 | * builder.connect('webhook-1', 'slack-1', 0, 0); 265 | */ 266 | connect( 267 | sourceNodeId: string, 268 | targetNodeId: string, 269 | sourceOutput = 0, 270 | targetInput = 0 271 | ): this { 272 | // Validate that both nodes exist 273 | const sourceNode = this.findNode(sourceNodeId); 274 | const targetNode = this.findNode(targetNodeId); 275 | 276 | if (!sourceNode) { 277 | throw new Error(`Source node not found: ${sourceNodeId}`); 278 | } 279 | if (!targetNode) { 280 | throw new Error(`Target node not found: ${targetNodeId}`); 281 | } 282 | 283 | if (!this.workflow.connections[sourceNodeId]) { 284 | this.workflow.connections[sourceNodeId] = { 285 | main: [], 286 | }; 287 | } 288 | 289 | // Ensure the output array exists 290 | while (this.workflow.connections[sourceNodeId].main.length <= sourceOutput) { 291 | this.workflow.connections[sourceNodeId].main.push([]); 292 | } 293 | 294 | // Add the connection 295 | this.workflow.connections[sourceNodeId].main[sourceOutput].push({ 296 | node: targetNodeId, 297 | type: 'main', 298 | index: targetInput, 299 | }); 300 | 301 | return this; 302 | } 303 | 304 | /** 305 | * Connect nodes in sequence 306 | */ 307 | connectSequentially(nodeIds: string[]): this { 308 | for (let i = 0; i < nodeIds.length - 1; i++) { 309 | this.connect(nodeIds[i], nodeIds[i + 1]); 310 | } 311 | return this; 312 | } 313 | 314 | /** 315 | * Set workflow settings 316 | */ 317 | setSettings(settings: IWorkflowSettings): this { 318 | this.workflow.settings = { 319 | ...this.workflow.settings, 320 | ...settings, 321 | }; 322 | return this; 323 | } 324 | 325 | /** 326 | * Set workflow as active 327 | */ 328 | setActive(active = true): this { 329 | this.workflow.active = active; 330 | return this; 331 | } 332 | 333 | /** 334 | * Add tags to the workflow 335 | */ 336 | addTags(...tags: string[]): this { 337 | this.workflow.tags = [...(this.workflow.tags || []), ...tags]; 338 | return this; 339 | } 340 | 341 | /** 342 | * Set workflow ID 343 | */ 344 | setId(id: string): this { 345 | this.workflow.id = id; 346 | return this; 347 | } 348 | 349 | /** 350 | * Build and return the workflow 351 | */ 352 | build(): IWorkflow { 353 | // Return a deep clone to prevent modifications 354 | return JSON.parse(JSON.stringify(this.workflow)); 355 | } 356 | 357 | /** 358 | * Get the next node position 359 | */ 360 | private getNextPosition(): [number, number] { 361 | const nodeCount = this.workflow.nodes.length; 362 | return [ 363 | this.defaultPosition[0] + (nodeCount * this.positionIncrement), 364 | this.defaultPosition[1], 365 | ]; 366 | } 367 | 368 | /** 369 | * Find a node by name or ID 370 | */ 371 | findNode(nameOrId: string): INode | undefined { 372 | return this.workflow.nodes.find( 373 | node => node.name === nameOrId || node.id === nameOrId 374 | ); 375 | } 376 | 377 | /** 378 | * Get all node IDs 379 | */ 380 | getNodeIds(): string[] { 381 | return this.workflow.nodes.map(node => node.id); 382 | } 383 | 384 | /** 385 | * Add a custom node type 386 | */ 387 | addCustomNode(type: string, typeVersion: number, parameters: INodeParameters, options: Partial<INode> = {}): this { 388 | return this.addNode({ 389 | type, 390 | typeVersion, 391 | parameters, 392 | ...options, 393 | }); 394 | } 395 | 396 | /** 397 | * Clear all nodes and connections 398 | */ 399 | clear(): this { 400 | this.workflow.nodes = []; 401 | this.workflow.connections = {}; 402 | this.nodeCounter = 0; 403 | return this; 404 | } 405 | 406 | /** 407 | * Clone the current workflow builder 408 | */ 409 | clone(): WorkflowBuilder { 410 | const cloned = new WorkflowBuilder(this.workflow.name); 411 | cloned.workflow = JSON.parse(JSON.stringify(this.workflow)); 412 | cloned.nodeCounter = this.nodeCounter; 413 | return cloned; 414 | } 415 | } 416 | 417 | // Export a factory function for convenience 418 | export function createWorkflow(name?: string): WorkflowBuilder { 419 | return new WorkflowBuilder(name); 420 | } ``` -------------------------------------------------------------------------------- /tests/test-database-extraction.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Test node extraction for database storage 5 | * Focus on extracting known nodes with proper structure for DB storage 6 | */ 7 | 8 | const fs = require('fs').promises; 9 | const path = require('path'); 10 | const crypto = require('crypto'); 11 | 12 | // Import our extractor 13 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); 14 | 15 | // Known n8n nodes to test 16 | const KNOWN_NODES = [ 17 | // Core nodes 18 | { type: 'n8n-nodes-base.Function', package: 'n8n-nodes-base', name: 'Function' }, 19 | { type: 'n8n-nodes-base.Webhook', package: 'n8n-nodes-base', name: 'Webhook' }, 20 | { type: 'n8n-nodes-base.HttpRequest', package: 'n8n-nodes-base', name: 'HttpRequest' }, 21 | { type: 'n8n-nodes-base.If', package: 'n8n-nodes-base', name: 'If' }, 22 | { type: 'n8n-nodes-base.SplitInBatches', package: 'n8n-nodes-base', name: 'SplitInBatches' }, 23 | 24 | // AI nodes 25 | { type: '@n8n/n8n-nodes-langchain.Agent', package: '@n8n/n8n-nodes-langchain', name: 'Agent' }, 26 | { type: '@n8n/n8n-nodes-langchain.OpenAiAssistant', package: '@n8n/n8n-nodes-langchain', name: 'OpenAiAssistant' }, 27 | { type: '@n8n/n8n-nodes-langchain.ChainLlm', package: '@n8n/n8n-nodes-langchain', name: 'ChainLlm' }, 28 | 29 | // Integration nodes 30 | { type: 'n8n-nodes-base.Airtable', package: 'n8n-nodes-base', name: 'Airtable' }, 31 | { type: 'n8n-nodes-base.GoogleSheets', package: 'n8n-nodes-base', name: 'GoogleSheets' }, 32 | { type: 'n8n-nodes-base.Slack', package: 'n8n-nodes-base', name: 'Slack' }, 33 | { type: 'n8n-nodes-base.Discord', package: 'n8n-nodes-base', name: 'Discord' }, 34 | ]; 35 | 36 | // Database schema for storing nodes 37 | const DB_SCHEMA = ` 38 | -- Main nodes table 39 | CREATE TABLE IF NOT EXISTS nodes ( 40 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 41 | node_type VARCHAR(255) UNIQUE NOT NULL, 42 | name VARCHAR(255) NOT NULL, 43 | package_name VARCHAR(255) NOT NULL, 44 | display_name VARCHAR(255), 45 | description TEXT, 46 | version VARCHAR(50), 47 | code_hash VARCHAR(64) NOT NULL, 48 | code_length INTEGER NOT NULL, 49 | source_location TEXT NOT NULL, 50 | has_credentials BOOLEAN DEFAULT FALSE, 51 | extracted_at TIMESTAMP NOT NULL DEFAULT NOW(), 52 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 53 | CONSTRAINT idx_node_type UNIQUE (node_type), 54 | INDEX idx_package_name (package_name), 55 | INDEX idx_code_hash (code_hash) 56 | ); 57 | 58 | -- Source code storage 59 | CREATE TABLE IF NOT EXISTS node_source_code ( 60 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 61 | node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, 62 | source_code TEXT NOT NULL, 63 | minified_code TEXT, 64 | source_map TEXT, 65 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 66 | CONSTRAINT idx_node_source UNIQUE (node_id) 67 | ); 68 | 69 | -- Credentials definitions 70 | CREATE TABLE IF NOT EXISTS node_credentials ( 71 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 72 | node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, 73 | credential_type VARCHAR(255) NOT NULL, 74 | credential_code TEXT NOT NULL, 75 | required_fields JSONB, 76 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 77 | INDEX idx_node_credentials (node_id) 78 | ); 79 | 80 | -- Package metadata 81 | CREATE TABLE IF NOT EXISTS node_packages ( 82 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 83 | package_name VARCHAR(255) UNIQUE NOT NULL, 84 | version VARCHAR(50), 85 | description TEXT, 86 | author VARCHAR(255), 87 | license VARCHAR(50), 88 | repository_url TEXT, 89 | metadata JSONB, 90 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 91 | updated_at TIMESTAMP NOT NULL DEFAULT NOW() 92 | ); 93 | 94 | -- Node dependencies 95 | CREATE TABLE IF NOT EXISTS node_dependencies ( 96 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 97 | node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, 98 | depends_on_node_id UUID NOT NULL REFERENCES nodes(id), 99 | dependency_type VARCHAR(50), -- 'extends', 'imports', 'requires' 100 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 101 | CONSTRAINT unique_dependency UNIQUE (node_id, depends_on_node_id) 102 | ); 103 | `; 104 | 105 | async function main() { 106 | console.log('=== n8n Node Extraction for Database Storage Test ===\n'); 107 | 108 | const extractor = new NodeSourceExtractor(); 109 | const results = { 110 | tested: 0, 111 | extracted: 0, 112 | failed: 0, 113 | nodes: [], 114 | errors: [], 115 | totalSize: 0 116 | }; 117 | 118 | // Create output directory 119 | const outputDir = path.join(__dirname, 'extracted-nodes-db'); 120 | await fs.mkdir(outputDir, { recursive: true }); 121 | 122 | console.log(`Testing extraction of ${KNOWN_NODES.length} known nodes...\n`); 123 | 124 | // Extract each node 125 | for (const nodeConfig of KNOWN_NODES) { 126 | console.log(`📦 Extracting: ${nodeConfig.type}`); 127 | results.tested++; 128 | 129 | try { 130 | const startTime = Date.now(); 131 | const nodeInfo = await extractor.extractNodeSource(nodeConfig.type); 132 | const extractTime = Date.now() - startTime; 133 | 134 | // Calculate hash for deduplication 135 | const codeHash = crypto.createHash('sha256').update(nodeInfo.sourceCode).digest('hex'); 136 | 137 | // Prepare database record 138 | const dbRecord = { 139 | // Primary data 140 | node_type: nodeConfig.type, 141 | name: nodeConfig.name, 142 | package_name: nodeConfig.package, 143 | code_hash: codeHash, 144 | code_length: nodeInfo.sourceCode.length, 145 | source_location: nodeInfo.location, 146 | has_credentials: !!nodeInfo.credentialCode, 147 | 148 | // Source code (separate table in real DB) 149 | source_code: nodeInfo.sourceCode, 150 | credential_code: nodeInfo.credentialCode, 151 | 152 | // Package info 153 | package_info: nodeInfo.packageInfo, 154 | 155 | // Metadata 156 | extraction_time_ms: extractTime, 157 | extracted_at: new Date().toISOString() 158 | }; 159 | 160 | results.nodes.push(dbRecord); 161 | results.extracted++; 162 | results.totalSize += nodeInfo.sourceCode.length; 163 | 164 | console.log(` ✅ Success: ${nodeInfo.sourceCode.length} bytes (${extractTime}ms)`); 165 | console.log(` 📍 Location: ${nodeInfo.location}`); 166 | console.log(` 🔑 Hash: ${codeHash.substring(0, 12)}...`); 167 | 168 | if (nodeInfo.credentialCode) { 169 | console.log(` 🔐 Has credentials: ${nodeInfo.credentialCode.length} bytes`); 170 | } 171 | 172 | // Save individual node data 173 | const nodeFile = path.join(outputDir, `${nodeConfig.package}__${nodeConfig.name}.json`); 174 | await fs.writeFile(nodeFile, JSON.stringify(dbRecord, null, 2)); 175 | 176 | } catch (error) { 177 | results.failed++; 178 | results.errors.push({ 179 | node: nodeConfig.type, 180 | error: error.message 181 | }); 182 | console.log(` ❌ Failed: ${error.message}`); 183 | } 184 | 185 | console.log(''); 186 | } 187 | 188 | // Generate summary report 189 | const successRate = ((results.extracted / results.tested) * 100).toFixed(1); 190 | 191 | console.log('='.repeat(60)); 192 | console.log('EXTRACTION SUMMARY'); 193 | console.log('='.repeat(60)); 194 | console.log(`Total nodes tested: ${results.tested}`); 195 | console.log(`Successfully extracted: ${results.extracted} (${successRate}%)`); 196 | console.log(`Failed: ${results.failed}`); 197 | console.log(`Total code size: ${(results.totalSize / 1024).toFixed(2)} KB`); 198 | console.log(`Average node size: ${(results.totalSize / results.extracted / 1024).toFixed(2)} KB`); 199 | 200 | // Test database insertion simulation 201 | console.log('\n📊 Database Storage Simulation:'); 202 | console.log('--------------------------------'); 203 | 204 | if (results.extracted > 0) { 205 | // Group by package 206 | const packages = {}; 207 | results.nodes.forEach(node => { 208 | if (!packages[node.package_name]) { 209 | packages[node.package_name] = { 210 | name: node.package_name, 211 | nodes: [], 212 | totalSize: 0 213 | }; 214 | } 215 | packages[node.package_name].nodes.push(node.name); 216 | packages[node.package_name].totalSize += node.code_length; 217 | }); 218 | 219 | console.log('\nPackages:'); 220 | Object.values(packages).forEach(pkg => { 221 | console.log(` 📦 ${pkg.name}`); 222 | console.log(` Nodes: ${pkg.nodes.length}`); 223 | console.log(` Total size: ${(pkg.totalSize / 1024).toFixed(2)} KB`); 224 | console.log(` Nodes: ${pkg.nodes.join(', ')}`); 225 | }); 226 | 227 | // Save database-ready JSON 228 | const dbData = { 229 | schema: DB_SCHEMA, 230 | extracted_at: new Date().toISOString(), 231 | statistics: { 232 | total_nodes: results.extracted, 233 | total_size_bytes: results.totalSize, 234 | packages: Object.keys(packages).length, 235 | success_rate: successRate 236 | }, 237 | nodes: results.nodes 238 | }; 239 | 240 | const dbFile = path.join(outputDir, 'database-import.json'); 241 | await fs.writeFile(dbFile, JSON.stringify(dbData, null, 2)); 242 | console.log(`\n💾 Database import file saved: ${dbFile}`); 243 | 244 | // Create SQL insert statements 245 | const sqlFile = path.join(outputDir, 'insert-nodes.sql'); 246 | let sql = '-- Auto-generated SQL for n8n nodes\n\n'; 247 | 248 | results.nodes.forEach(node => { 249 | sql += `-- Node: ${node.node_type}\n`; 250 | sql += `INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials)\n`; 251 | sql += `VALUES ('${node.node_type}', '${node.name}', '${node.package_name}', '${node.code_hash}', ${node.code_length}, '${node.source_location}', ${node.has_credentials});\n\n`; 252 | }); 253 | 254 | await fs.writeFile(sqlFile, sql); 255 | console.log(`📝 SQL insert file saved: ${sqlFile}`); 256 | } 257 | 258 | // Save full report 259 | const reportFile = path.join(outputDir, 'extraction-report.json'); 260 | await fs.writeFile(reportFile, JSON.stringify(results, null, 2)); 261 | console.log(`\n📄 Full report saved: ${reportFile}`); 262 | 263 | // Show any errors 264 | if (results.errors.length > 0) { 265 | console.log('\n⚠️ Extraction Errors:'); 266 | results.errors.forEach(err => { 267 | console.log(` - ${err.node}: ${err.error}`); 268 | }); 269 | } 270 | 271 | console.log('\n✨ Database extraction test completed!'); 272 | console.log(`📁 Results saved in: ${outputDir}`); 273 | 274 | // Exit with appropriate code 275 | process.exit(results.failed > 0 ? 1 : 0); 276 | } 277 | 278 | // Run the test 279 | main().catch(error => { 280 | console.error('Fatal error:', error); 281 | process.exit(1); 282 | }); ``` -------------------------------------------------------------------------------- /tests/integration/msw-setup.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; 2 | import { mswTestServer, n8nApiMock, testDataBuilders, integrationTestServer } from './setup/msw-test-server'; 3 | import { http, HttpResponse } from 'msw'; 4 | import axios from 'axios'; 5 | import { server } from './setup/integration-setup'; 6 | 7 | describe('MSW Setup Verification', () => { 8 | const baseUrl = 'http://localhost:5678'; 9 | 10 | describe('Global MSW Setup', () => { 11 | it('should intercept n8n API requests with default handlers', async () => { 12 | // This uses the global MSW setup from vitest.config.ts 13 | const response = await axios.get(`${baseUrl}/api/v1/health`); 14 | 15 | expect(response.status).toBe(200); 16 | expect(response.data).toEqual({ 17 | status: 'ok', 18 | version: '1.103.2', 19 | features: { 20 | workflows: true, 21 | executions: true, 22 | credentials: true, 23 | webhooks: true, 24 | } 25 | }); 26 | }); 27 | 28 | it('should allow custom handlers for specific tests', async () => { 29 | // Add a custom handler just for this test using the global server 30 | server.use( 31 | http.get('*/api/v1/custom-endpoint', () => { 32 | return HttpResponse.json({ custom: true }); 33 | }) 34 | ); 35 | 36 | const response = await axios.get(`${baseUrl}/api/v1/custom-endpoint`); 37 | 38 | expect(response.status).toBe(200); 39 | expect(response.data).toEqual({ custom: true }); 40 | }); 41 | 42 | it('should return mock workflows', async () => { 43 | const response = await axios.get(`${baseUrl}/api/v1/workflows`); 44 | 45 | expect(response.status).toBe(200); 46 | expect(response.data).toHaveProperty('data'); 47 | expect(Array.isArray(response.data.data)).toBe(true); 48 | expect(response.data.data.length).toBeGreaterThan(0); 49 | }); 50 | }); 51 | 52 | describe('Integration Test Server', () => { 53 | // Use the global MSW server instance for these tests 54 | afterEach(() => { 55 | // Reset handlers after each test to ensure clean state 56 | server.resetHandlers(); 57 | }); 58 | 59 | it('should handle workflow creation with custom response', async () => { 60 | // Use the global server instance to add custom handler 61 | server.use( 62 | http.post('*/api/v1/workflows', async ({ request }) => { 63 | const body = await request.json() as any; 64 | return HttpResponse.json({ 65 | data: { 66 | id: 'custom-workflow-123', 67 | name: 'Test Workflow from MSW', 68 | active: body.active || false, 69 | nodes: body.nodes, 70 | connections: body.connections, 71 | settings: body.settings || {}, 72 | tags: body.tags || [], 73 | createdAt: new Date().toISOString(), 74 | updatedAt: new Date().toISOString(), 75 | versionId: '1' 76 | } 77 | }, { status: 201 }); 78 | }) 79 | ); 80 | 81 | const workflowData = testDataBuilders.workflow({ 82 | name: 'My Test Workflow' 83 | }); 84 | 85 | const response = await axios.post(`${baseUrl}/api/v1/workflows`, workflowData); 86 | 87 | expect(response.status).toBe(201); 88 | expect(response.data.data).toMatchObject({ 89 | id: 'custom-workflow-123', 90 | name: 'Test Workflow from MSW', 91 | nodes: workflowData.nodes, 92 | connections: workflowData.connections 93 | }); 94 | }); 95 | 96 | it('should handle error responses', async () => { 97 | server.use( 98 | http.get('*/api/v1/workflows/missing', () => { 99 | return HttpResponse.json( 100 | { 101 | message: 'Workflow not found', 102 | code: 'NOT_FOUND', 103 | timestamp: new Date().toISOString() 104 | }, 105 | { status: 404 } 106 | ); 107 | }) 108 | ); 109 | 110 | try { 111 | await axios.get(`${baseUrl}/api/v1/workflows/missing`); 112 | expect.fail('Should have thrown an error'); 113 | } catch (error: any) { 114 | expect(error.response.status).toBe(404); 115 | expect(error.response.data).toEqual({ 116 | message: 'Workflow not found', 117 | code: 'NOT_FOUND', 118 | timestamp: expect.any(String) 119 | }); 120 | } 121 | }); 122 | 123 | it('should simulate rate limiting', async () => { 124 | let requestCount = 0; 125 | const limit = 5; 126 | 127 | server.use( 128 | http.get('*/api/v1/rate-limited', () => { 129 | requestCount++; 130 | 131 | if (requestCount > limit) { 132 | return HttpResponse.json( 133 | { 134 | message: 'Rate limit exceeded', 135 | code: 'RATE_LIMIT', 136 | retryAfter: 60 137 | }, 138 | { 139 | status: 429, 140 | headers: { 141 | 'X-RateLimit-Limit': String(limit), 142 | 'X-RateLimit-Remaining': '0', 143 | 'X-RateLimit-Reset': String(Date.now() + 60000) 144 | } 145 | } 146 | ); 147 | } 148 | 149 | return HttpResponse.json({ success: true }); 150 | }) 151 | ); 152 | 153 | // Make requests up to the limit 154 | for (let i = 0; i < 5; i++) { 155 | const response = await axios.get(`${baseUrl}/api/v1/rate-limited`); 156 | expect(response.status).toBe(200); 157 | } 158 | 159 | // Next request should be rate limited 160 | try { 161 | await axios.get(`${baseUrl}/api/v1/rate-limited`); 162 | expect.fail('Should have been rate limited'); 163 | } catch (error: any) { 164 | expect(error.response.status).toBe(429); 165 | expect(error.response.data.code).toBe('RATE_LIMIT'); 166 | expect(error.response.headers['x-ratelimit-remaining']).toBe('0'); 167 | } 168 | }); 169 | 170 | it('should handle webhook execution', async () => { 171 | server.use( 172 | http.post('*/webhook/test-webhook', async ({ request }) => { 173 | const body = await request.json(); 174 | 175 | return HttpResponse.json({ 176 | processed: true, 177 | result: 'success', 178 | webhookReceived: { 179 | path: 'test-webhook', 180 | method: 'POST', 181 | body, 182 | timestamp: new Date().toISOString() 183 | } 184 | }); 185 | }) 186 | ); 187 | 188 | const webhookData = { message: 'Test webhook payload' }; 189 | const response = await axios.post(`${baseUrl}/webhook/test-webhook`, webhookData); 190 | 191 | expect(response.status).toBe(200); 192 | expect(response.data).toMatchObject({ 193 | processed: true, 194 | result: 'success', 195 | webhookReceived: { 196 | path: 'test-webhook', 197 | method: 'POST', 198 | body: webhookData, 199 | timestamp: expect.any(String) 200 | } 201 | }); 202 | }); 203 | 204 | it('should wait for specific requests', async () => { 205 | // Since the global server is already handling these endpoints, 206 | // we'll just make the requests and verify they succeed 207 | const responses = await Promise.all([ 208 | axios.get(`${baseUrl}/api/v1/workflows`), 209 | axios.get(`${baseUrl}/api/v1/executions`) 210 | ]); 211 | 212 | expect(responses).toHaveLength(2); 213 | expect(responses[0].status).toBe(200); 214 | expect(responses[0].config.url).toContain('/api/v1/workflows'); 215 | expect(responses[1].status).toBe(200); 216 | expect(responses[1].config.url).toContain('/api/v1/executions'); 217 | }, { timeout: 10000 }); // Increase timeout for this specific test 218 | 219 | it('should work with scoped handlers', async () => { 220 | // First add the scoped handler 221 | server.use( 222 | http.get('*/api/v1/scoped', () => { 223 | return HttpResponse.json({ scoped: true }); 224 | }) 225 | ); 226 | 227 | // Make the request while handler is active 228 | const response = await axios.get(`${baseUrl}/api/v1/scoped`); 229 | expect(response.data).toEqual({ scoped: true }); 230 | 231 | // Reset handlers to remove the scoped handler 232 | server.resetHandlers(); 233 | 234 | // Verify the scoped handler is no longer active 235 | // Since there's no handler for this endpoint now, it should fall through to the catch-all 236 | try { 237 | await axios.get(`${baseUrl}/api/v1/scoped`); 238 | expect.fail('Should have returned 501'); 239 | } catch (error: any) { 240 | expect(error.response.status).toBe(501); 241 | } 242 | }); 243 | }); 244 | 245 | describe('Factory Functions', () => { 246 | it('should create workflows using factory', async () => { 247 | const { workflowFactory } = await import('../mocks/n8n-api/data/workflows'); 248 | 249 | const simpleWorkflow = workflowFactory.simple('n8n-nodes-base.slack', { 250 | resource: 'message', 251 | operation: 'post', 252 | channel: '#general', 253 | text: 'Hello from test' 254 | }); 255 | 256 | expect(simpleWorkflow).toMatchObject({ 257 | id: expect.stringMatching(/^workflow_\d+$/), 258 | name: 'Test n8n-nodes-base.slack Workflow', // Factory uses nodeType in the name 259 | active: true, 260 | nodes: expect.arrayContaining([ 261 | expect.objectContaining({ type: 'n8n-nodes-base.start' }), 262 | expect.objectContaining({ 263 | type: 'n8n-nodes-base.slack', 264 | parameters: { 265 | resource: 'message', 266 | operation: 'post', 267 | channel: '#general', 268 | text: 'Hello from test' 269 | } 270 | }) 271 | ]) 272 | }); 273 | }); 274 | 275 | it('should create executions using factory', async () => { 276 | const { executionFactory } = await import('../mocks/n8n-api/data/executions'); 277 | 278 | const successExecution = executionFactory.success('workflow_123'); 279 | const errorExecution = executionFactory.error('workflow_456', { 280 | message: 'Connection timeout', 281 | node: 'http_request_1' 282 | }); 283 | 284 | expect(successExecution).toMatchObject({ 285 | workflowId: 'workflow_123', 286 | status: 'success', 287 | mode: 'manual' 288 | }); 289 | 290 | expect(errorExecution).toMatchObject({ 291 | workflowId: 'workflow_456', 292 | status: 'error', 293 | error: { 294 | message: 'Connection timeout', 295 | node: 'http_request_1' 296 | } 297 | }); 298 | }); 299 | }); 300 | }); ``` -------------------------------------------------------------------------------- /tests/integration/ai-validation/chat-trigger-validation.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Tests: Chat Trigger Validation 3 | * 4 | * Tests Chat Trigger validation against real n8n instance. 5 | */ 6 | 7 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; 8 | import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context'; 9 | import { getTestN8nClient } from '../n8n-api/utils/n8n-client'; 10 | import { N8nApiClient } from '../../../src/services/n8n-api-client'; 11 | import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers'; 12 | import { createMcpContext } from '../n8n-api/utils/mcp-context'; 13 | import { InstanceContext } from '../../../src/types/instance-context'; 14 | import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager'; 15 | import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository'; 16 | import { NodeRepository } from '../../../src/database/node-repository'; 17 | import { ValidationResponse } from '../n8n-api/types/mcp-responses'; 18 | import { 19 | createChatTriggerNode, 20 | createAIAgentNode, 21 | createLanguageModelNode, 22 | createRespondNode, 23 | createAIConnection, 24 | createMainConnection, 25 | mergeConnections, 26 | createAIWorkflow 27 | } from './helpers'; 28 | import { WorkflowNode } from '../../../src/types/n8n-api'; 29 | 30 | describe('Integration: Chat Trigger Validation', () => { 31 | let context: TestContext; 32 | let client: N8nApiClient; 33 | let mcpContext: InstanceContext; 34 | let repository: NodeRepository; 35 | 36 | beforeEach(async () => { 37 | context = createTestContext(); 38 | client = getTestN8nClient(); 39 | mcpContext = createMcpContext(); 40 | repository = await getNodeRepository(); 41 | }); 42 | 43 | afterEach(async () => { 44 | await context.cleanup(); 45 | }); 46 | 47 | afterAll(async () => { 48 | await closeNodeRepository(); 49 | if (!process.env.CI) { 50 | await cleanupOrphanedWorkflows(); 51 | } 52 | }); 53 | 54 | // ====================================================================== 55 | // TEST 1: Streaming to Non-AI-Agent 56 | // ====================================================================== 57 | 58 | it('should detect streaming to non-AI-Agent', async () => { 59 | const chatTrigger = createChatTriggerNode({ 60 | name: 'Chat Trigger', 61 | responseMode: 'streaming' 62 | }); 63 | 64 | // Regular node (not AI Agent) 65 | const regularNode: WorkflowNode = { 66 | id: 'set-1', 67 | name: 'Set', 68 | type: 'n8n-nodes-base.set', 69 | typeVersion: 3.4, 70 | position: [450, 300], 71 | parameters: { 72 | assignments: { 73 | assignments: [] 74 | } 75 | } 76 | }; 77 | 78 | const workflow = createAIWorkflow( 79 | [chatTrigger, regularNode], 80 | createMainConnection('Chat Trigger', 'Set'), 81 | { 82 | name: createTestWorkflowName('Chat Trigger - Wrong Target'), 83 | tags: ['mcp-integration-test', 'ai-validation'] 84 | } 85 | ); 86 | 87 | const created = await client.createWorkflow(workflow); 88 | context.trackWorkflow(created.id!); 89 | 90 | const response = await handleValidateWorkflow( 91 | { id: created.id }, 92 | repository, 93 | mcpContext 94 | ); 95 | 96 | expect(response.success).toBe(true); 97 | const data = response.data as ValidationResponse; 98 | 99 | expect(data.valid).toBe(false); 100 | expect(data.errors).toBeDefined(); 101 | 102 | const errorCodes = data.errors!.map(e => e.details?.code || e.code); 103 | expect(errorCodes).toContain('STREAMING_WRONG_TARGET'); 104 | 105 | const errorMessages = data.errors!.map(e => e.message).join(' '); 106 | expect(errorMessages).toMatch(/streaming.*AI Agent/i); 107 | }); 108 | 109 | // ====================================================================== 110 | // TEST 2: Missing Connections 111 | // ====================================================================== 112 | 113 | it('should detect missing connections', async () => { 114 | const chatTrigger = createChatTriggerNode({ 115 | name: 'Chat Trigger' 116 | }); 117 | 118 | const workflow = createAIWorkflow( 119 | [chatTrigger], 120 | {}, // No connections 121 | { 122 | name: createTestWorkflowName('Chat Trigger - No Connections'), 123 | tags: ['mcp-integration-test', 'ai-validation'] 124 | } 125 | ); 126 | 127 | const created = await client.createWorkflow(workflow); 128 | context.trackWorkflow(created.id!); 129 | 130 | const response = await handleValidateWorkflow( 131 | { id: created.id }, 132 | repository, 133 | mcpContext 134 | ); 135 | 136 | expect(response.success).toBe(true); 137 | const data = response.data as ValidationResponse; 138 | 139 | expect(data.valid).toBe(false); 140 | expect(data.errors).toBeDefined(); 141 | 142 | const errorCodes = data.errors!.map(e => e.details?.code || e.code); 143 | expect(errorCodes).toContain('MISSING_CONNECTIONS'); 144 | }); 145 | 146 | // ====================================================================== 147 | // TEST 3: Valid Streaming Setup 148 | // ====================================================================== 149 | 150 | it('should validate valid streaming setup', async () => { 151 | const chatTrigger = createChatTriggerNode({ 152 | name: 'Chat Trigger', 153 | responseMode: 'streaming' 154 | }); 155 | 156 | const languageModel = createLanguageModelNode('openai', { 157 | name: 'OpenAI Chat Model' 158 | }); 159 | 160 | const agent = createAIAgentNode({ 161 | name: 'AI Agent', 162 | text: 'You are a helpful assistant' 163 | // No main output connections - streaming mode 164 | }); 165 | 166 | const workflow = createAIWorkflow( 167 | [chatTrigger, languageModel, agent], 168 | mergeConnections( 169 | createMainConnection('Chat Trigger', 'AI Agent'), 170 | createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel') 171 | // NO main output from AI Agent 172 | ), 173 | { 174 | name: createTestWorkflowName('Chat Trigger - Valid Streaming'), 175 | tags: ['mcp-integration-test', 'ai-validation'] 176 | } 177 | ); 178 | 179 | const created = await client.createWorkflow(workflow); 180 | context.trackWorkflow(created.id!); 181 | 182 | const response = await handleValidateWorkflow( 183 | { id: created.id }, 184 | repository, 185 | mcpContext 186 | ); 187 | 188 | expect(response.success).toBe(true); 189 | const data = response.data as ValidationResponse; 190 | 191 | expect(data.valid).toBe(true); 192 | expect(data.errors).toBeUndefined(); 193 | expect(data.summary.errorCount).toBe(0); 194 | }); 195 | 196 | // ====================================================================== 197 | // TEST 4: LastNode Mode (Default) 198 | // ====================================================================== 199 | 200 | it('should validate lastNode mode with AI Agent', async () => { 201 | const chatTrigger = createChatTriggerNode({ 202 | name: 'Chat Trigger', 203 | responseMode: 'lastNode' 204 | }); 205 | 206 | const languageModel = createLanguageModelNode('openai', { 207 | name: 'OpenAI Chat Model' 208 | }); 209 | 210 | const agent = createAIAgentNode({ 211 | name: 'AI Agent', 212 | text: 'You are a helpful assistant' 213 | }); 214 | 215 | const respond = createRespondNode({ 216 | name: 'Respond to Webhook' 217 | }); 218 | 219 | const workflow = createAIWorkflow( 220 | [chatTrigger, languageModel, agent, respond], 221 | mergeConnections( 222 | createMainConnection('Chat Trigger', 'AI Agent'), 223 | createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel'), 224 | createMainConnection('AI Agent', 'Respond to Webhook') 225 | ), 226 | { 227 | name: createTestWorkflowName('Chat Trigger - LastNode Mode'), 228 | tags: ['mcp-integration-test', 'ai-validation'] 229 | } 230 | ); 231 | 232 | const created = await client.createWorkflow(workflow); 233 | context.trackWorkflow(created.id!); 234 | 235 | const response = await handleValidateWorkflow( 236 | { id: created.id }, 237 | repository, 238 | mcpContext 239 | ); 240 | 241 | expect(response.success).toBe(true); 242 | const data = response.data as ValidationResponse; 243 | 244 | // Should be valid (lastNode mode allows main output) 245 | expect(data.valid).toBe(true); 246 | 247 | // May have info suggestion about using streaming 248 | if (data.info) { 249 | const streamingSuggestion = data.info.find((i: any) => 250 | i.message.toLowerCase().includes('streaming') 251 | ); 252 | // This is optional - just checking the suggestion exists if present 253 | if (streamingSuggestion) { 254 | expect(streamingSuggestion.severity).toBe('info'); 255 | } 256 | } 257 | }); 258 | 259 | // ====================================================================== 260 | // TEST 5: Streaming Agent with Output Connection (Error) 261 | // ====================================================================== 262 | 263 | it('should detect streaming agent with output connection', async () => { 264 | const chatTrigger = createChatTriggerNode({ 265 | name: 'Chat Trigger', 266 | responseMode: 'streaming' 267 | }); 268 | 269 | const languageModel = createLanguageModelNode('openai', { 270 | name: 'OpenAI Chat Model' 271 | }); 272 | 273 | const agent = createAIAgentNode({ 274 | name: 'AI Agent', 275 | text: 'You are a helpful assistant' 276 | }); 277 | 278 | const respond = createRespondNode({ 279 | name: 'Respond to Webhook' 280 | }); 281 | 282 | const workflow = createAIWorkflow( 283 | [chatTrigger, languageModel, agent, respond], 284 | mergeConnections( 285 | createMainConnection('Chat Trigger', 'AI Agent'), 286 | createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel'), 287 | createMainConnection('AI Agent', 'Respond to Webhook') // ERROR in streaming mode 288 | ), 289 | { 290 | name: createTestWorkflowName('Chat Trigger - Streaming With Output'), 291 | tags: ['mcp-integration-test', 'ai-validation'] 292 | } 293 | ); 294 | 295 | const created = await client.createWorkflow(workflow); 296 | context.trackWorkflow(created.id!); 297 | 298 | const response = await handleValidateWorkflow( 299 | { id: created.id }, 300 | repository, 301 | mcpContext 302 | ); 303 | 304 | expect(response.success).toBe(true); 305 | const data = response.data as ValidationResponse; 306 | 307 | expect(data.valid).toBe(false); 308 | expect(data.errors).toBeDefined(); 309 | 310 | // Should detect streaming agent has output 311 | const streamingErrors = data.errors!.filter(e => { 312 | const code = e.details?.code || e.code; 313 | return code === 'STREAMING_AGENT_HAS_OUTPUT' || 314 | e.message.toLowerCase().includes('streaming'); 315 | }); 316 | expect(streamingErrors.length).toBeGreaterThan(0); 317 | }); 318 | }); 319 | ``` -------------------------------------------------------------------------------- /tests/unit/flexible-instance-security.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Unit tests for flexible instance configuration security improvements 3 | */ 4 | 5 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 6 | import { InstanceContext, isInstanceContext, validateInstanceContext } from '../../src/types/instance-context'; 7 | import { getN8nApiClient } from '../../src/mcp/handlers-n8n-manager'; 8 | import { createHash } from 'crypto'; 9 | 10 | describe('Flexible Instance Security', () => { 11 | beforeEach(() => { 12 | // Clear module cache to reset singleton state 13 | vi.resetModules(); 14 | }); 15 | 16 | afterEach(() => { 17 | vi.clearAllMocks(); 18 | }); 19 | 20 | describe('Input Validation', () => { 21 | describe('URL Validation', () => { 22 | it('should accept valid HTTP and HTTPS URLs', () => { 23 | const validContext: InstanceContext = { 24 | n8nApiUrl: 'https://api.n8n.cloud', 25 | n8nApiKey: 'valid-key' 26 | }; 27 | expect(isInstanceContext(validContext)).toBe(true); 28 | 29 | const httpContext: InstanceContext = { 30 | n8nApiUrl: 'http://localhost:5678', 31 | n8nApiKey: 'valid-key' 32 | }; 33 | expect(isInstanceContext(httpContext)).toBe(true); 34 | }); 35 | 36 | it('should reject invalid URL formats', () => { 37 | const invalidUrls = [ 38 | 'not-a-url', 39 | 'ftp://invalid-protocol.com', 40 | 'javascript:alert(1)', 41 | '//missing-protocol.com', 42 | 'https://', 43 | '' 44 | ]; 45 | 46 | invalidUrls.forEach(url => { 47 | const context = { 48 | n8nApiUrl: url, 49 | n8nApiKey: 'key' 50 | }; 51 | const validation = validateInstanceContext(context); 52 | expect(validation.valid).toBe(false); 53 | expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('API Key Validation', () => { 59 | it('should accept valid API keys', () => { 60 | const validKeys = [ 61 | 'abc123def456', 62 | 'sk_live_abcdefghijklmnop', 63 | 'token_1234567890', 64 | 'a'.repeat(100) // Long key 65 | ]; 66 | 67 | validKeys.forEach(key => { 68 | const context: InstanceContext = { 69 | n8nApiUrl: 'https://api.n8n.cloud', 70 | n8nApiKey: key 71 | }; 72 | const validation = validateInstanceContext(context); 73 | expect(validation.valid).toBe(true); 74 | }); 75 | }); 76 | 77 | it('should reject placeholder or invalid API keys', () => { 78 | const invalidKeys = [ 79 | 'YOUR_API_KEY', 80 | 'placeholder', 81 | 'example', 82 | 'YOUR_API_KEY_HERE', 83 | 'example-key', 84 | 'placeholder-token' 85 | ]; 86 | 87 | invalidKeys.forEach(key => { 88 | const context: InstanceContext = { 89 | n8nApiUrl: 'https://api.n8n.cloud', 90 | n8nApiKey: key 91 | }; 92 | const validation = validateInstanceContext(context); 93 | expect(validation.valid).toBe(false); 94 | expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiKey:'))).toBe(true); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('Timeout and Retry Validation', () => { 100 | it('should validate timeout values', () => { 101 | const invalidTimeouts = [0, -1, -1000]; 102 | 103 | invalidTimeouts.forEach(timeout => { 104 | const context: InstanceContext = { 105 | n8nApiUrl: 'https://api.n8n.cloud', 106 | n8nApiKey: 'key', 107 | n8nApiTimeout: timeout 108 | }; 109 | const validation = validateInstanceContext(context); 110 | expect(validation.valid).toBe(false); 111 | expect(validation.errors?.some(error => error.includes('Must be positive (greater than 0)'))).toBe(true); 112 | }); 113 | 114 | // NaN and Infinity are handled differently 115 | const nanContext: InstanceContext = { 116 | n8nApiUrl: 'https://api.n8n.cloud', 117 | n8nApiKey: 'key', 118 | n8nApiTimeout: NaN 119 | }; 120 | const nanValidation = validateInstanceContext(nanContext); 121 | expect(nanValidation.valid).toBe(false); 122 | 123 | // Valid timeout 124 | const validContext: InstanceContext = { 125 | n8nApiUrl: 'https://api.n8n.cloud', 126 | n8nApiKey: 'key', 127 | n8nApiTimeout: 30000 128 | }; 129 | const validation = validateInstanceContext(validContext); 130 | expect(validation.valid).toBe(true); 131 | }); 132 | 133 | it('should validate retry values', () => { 134 | const invalidRetries = [-1, -10]; 135 | 136 | invalidRetries.forEach(retries => { 137 | const context: InstanceContext = { 138 | n8nApiUrl: 'https://api.n8n.cloud', 139 | n8nApiKey: 'key', 140 | n8nApiMaxRetries: retries 141 | }; 142 | const validation = validateInstanceContext(context); 143 | expect(validation.valid).toBe(false); 144 | expect(validation.errors?.some(error => error.includes('Must be non-negative (0 or greater)'))).toBe(true); 145 | }); 146 | 147 | // Valid retries (including 0) 148 | [0, 1, 3, 10].forEach(retries => { 149 | const context: InstanceContext = { 150 | n8nApiUrl: 'https://api.n8n.cloud', 151 | n8nApiKey: 'key', 152 | n8nApiMaxRetries: retries 153 | }; 154 | const validation = validateInstanceContext(context); 155 | expect(validation.valid).toBe(true); 156 | }); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('Cache Key Security', () => { 162 | it('should hash cache keys instead of using raw credentials', () => { 163 | const context: InstanceContext = { 164 | n8nApiUrl: 'https://api.n8n.cloud', 165 | n8nApiKey: 'super-secret-key', 166 | instanceId: 'instance-1' 167 | }; 168 | 169 | // Calculate expected hash 170 | const expectedHash = createHash('sha256') 171 | .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`) 172 | .digest('hex'); 173 | 174 | // The actual cache key should be hashed, not contain raw values 175 | // We can't directly test the internal cache key, but we can verify 176 | // that the function doesn't throw and returns a client 177 | const client = getN8nApiClient(context); 178 | 179 | // If validation passes, client could be created (or null if no env vars) 180 | // The important part is that raw credentials aren't exposed 181 | expect(() => getN8nApiClient(context)).not.toThrow(); 182 | }); 183 | 184 | it('should not expose API keys in any form', () => { 185 | const sensitiveKey = 'super-secret-api-key-12345'; 186 | const context: InstanceContext = { 187 | n8nApiUrl: 'https://api.n8n.cloud', 188 | n8nApiKey: sensitiveKey, 189 | instanceId: 'test' 190 | }; 191 | 192 | // Mock console methods to capture any output 193 | const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); 194 | const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 195 | const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); 196 | 197 | getN8nApiClient(context); 198 | 199 | // Verify the sensitive key is never logged 200 | const allLogs = [ 201 | ...consoleSpy.mock.calls, 202 | ...consoleWarnSpy.mock.calls, 203 | ...consoleErrorSpy.mock.calls 204 | ].flat().join(' '); 205 | 206 | expect(allLogs).not.toContain(sensitiveKey); 207 | 208 | consoleSpy.mockRestore(); 209 | consoleWarnSpy.mockRestore(); 210 | consoleErrorSpy.mockRestore(); 211 | }); 212 | }); 213 | 214 | describe('Error Message Sanitization', () => { 215 | it('should not expose sensitive data in error messages', () => { 216 | const context: InstanceContext = { 217 | n8nApiUrl: 'invalid-url', 218 | n8nApiKey: 'secret-key-that-should-not-appear', 219 | instanceId: 'test-instance' 220 | }; 221 | 222 | const validation = validateInstanceContext(context); 223 | 224 | // Error messages should be generic, not include actual values 225 | expect(validation.errors).toBeDefined(); 226 | expect(validation.errors!.join(' ')).not.toContain('secret-key'); 227 | expect(validation.errors!.join(' ')).not.toContain(context.n8nApiKey); 228 | }); 229 | }); 230 | 231 | describe('Type Guard Security', () => { 232 | it('should safely handle malicious input', () => { 233 | // Test specific malicious inputs 234 | const objectAsUrl = { n8nApiUrl: { toString: () => { throw new Error('XSS'); } } }; 235 | expect(() => isInstanceContext(objectAsUrl)).not.toThrow(); 236 | expect(isInstanceContext(objectAsUrl)).toBe(false); 237 | 238 | const arrayAsKey = { n8nApiKey: ['array', 'instead', 'of', 'string'] }; 239 | expect(() => isInstanceContext(arrayAsKey)).not.toThrow(); 240 | expect(isInstanceContext(arrayAsKey)).toBe(false); 241 | 242 | // These are actually valid objects with extra properties 243 | const protoObj = { __proto__: { isAdmin: true } }; 244 | expect(() => isInstanceContext(protoObj)).not.toThrow(); 245 | // This is actually a valid object, just has __proto__ property 246 | expect(isInstanceContext(protoObj)).toBe(true); 247 | 248 | const constructorObj = { constructor: { name: 'Evil' } }; 249 | expect(() => isInstanceContext(constructorObj)).not.toThrow(); 250 | // This is also a valid object with constructor property 251 | expect(isInstanceContext(constructorObj)).toBe(true); 252 | 253 | // Object.create(null) creates an object without prototype 254 | const nullProto = Object.create(null); 255 | expect(() => isInstanceContext(nullProto)).not.toThrow(); 256 | // This is actually a valid empty object, so it passes 257 | expect(isInstanceContext(nullProto)).toBe(true); 258 | }); 259 | 260 | it('should handle circular references safely', () => { 261 | const circular: any = { n8nApiUrl: 'https://api.n8n.cloud' }; 262 | circular.self = circular; 263 | 264 | expect(() => isInstanceContext(circular)).not.toThrow(); 265 | }); 266 | }); 267 | 268 | describe('Memory Management', () => { 269 | it('should validate LRU cache configuration', () => { 270 | // This is more of a configuration test 271 | // In real implementation, we'd test that the cache has proper limits 272 | const MAX_CACHE_SIZE = 100; 273 | const TTL_MINUTES = 30; 274 | 275 | // Verify reasonable limits are in place 276 | expect(MAX_CACHE_SIZE).toBeLessThanOrEqual(1000); // Not too many 277 | expect(TTL_MINUTES).toBeLessThanOrEqual(60); // Not too long 278 | }); 279 | }); 280 | }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-execution.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nGetExecutionDoc: ToolDocumentation = { 4 | name: 'n8n_get_execution', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Get execution details with smart filtering to avoid token limits. Use preview mode first to assess data size, then fetch appropriately.', 8 | keyParameters: ['id', 'mode', 'itemsLimit', 'nodeNames'], 9 | example: ` 10 | // RECOMMENDED WORKFLOW: 11 | // 1. Preview first 12 | n8n_get_execution({id: "12345", mode: "preview"}) 13 | // Returns: structure, counts, size estimate, recommendation 14 | 15 | // 2. Based on recommendation, fetch data: 16 | n8n_get_execution({id: "12345", mode: "summary"}) // 2 items per node 17 | n8n_get_execution({id: "12345", mode: "filtered", itemsLimit: 5}) // 5 items 18 | n8n_get_execution({id: "12345", nodeNames: ["HTTP Request"]}) // Specific node 19 | `, 20 | performance: 'Preview: <50ms, Summary: <200ms, Full: depends on data size', 21 | tips: [ 22 | 'ALWAYS use preview mode first for large datasets', 23 | 'Preview shows structure + counts without consuming tokens for data', 24 | 'Summary mode (2 items per node) is safe default', 25 | 'Use nodeNames to focus on specific nodes only', 26 | 'itemsLimit: 0 = structure only, -1 = unlimited', 27 | 'Check recommendation.suggestedMode from preview' 28 | ] 29 | }, 30 | full: { 31 | description: `Retrieves and intelligently filters execution data to enable inspection without exceeding token limits. This tool provides multiple modes for different use cases, from quick previews to complete data retrieval. 32 | 33 | **The Problem**: Workflows processing large datasets (50+ database records) generate execution data that exceeds token/response limits, making traditional full-data fetching impossible. 34 | 35 | **The Solution**: Four retrieval modes with smart filtering: 36 | 1. **Preview**: Structure + counts only (no actual data) 37 | 2. **Summary**: 2 sample items per node (safe default) 38 | 3. **Filtered**: Custom limits and node selection 39 | 4. **Full**: Complete data (use with caution) 40 | 41 | **Recommended Workflow**: 42 | 1. Start with preview mode to assess size 43 | 2. Use recommendation to choose appropriate mode 44 | 3. Fetch filtered data as needed`, 45 | 46 | parameters: { 47 | id: { 48 | type: 'string', 49 | required: true, 50 | description: 'The execution ID to retrieve. Obtained from list_executions or webhook trigger responses' 51 | }, 52 | mode: { 53 | type: 'string', 54 | required: false, 55 | description: `Retrieval mode (default: auto-detect from other params): 56 | - 'preview': Structure, counts, size estimates - NO actual data (fastest) 57 | - 'summary': Metadata + 2 sample items per node (safe default) 58 | - 'filtered': Custom filtering with itemsLimit/nodeNames 59 | - 'full': Complete execution data (use with caution)` 60 | }, 61 | nodeNames: { 62 | type: 'array', 63 | required: false, 64 | description: 'Filter to specific nodes by name. Example: ["HTTP Request", "Filter"]. Useful when you only need to inspect specific nodes.' 65 | }, 66 | itemsLimit: { 67 | type: 'number', 68 | required: false, 69 | description: `Items to return per node (default: 2): 70 | - 0: Structure only (see data shape without values) 71 | - 1-N: Return N items per node 72 | - -1: Unlimited (return all items) 73 | 74 | Note: Structure-only mode (0) shows JSON schema without actual values.` 75 | }, 76 | includeInputData: { 77 | type: 'boolean', 78 | required: false, 79 | description: 'Include input data in addition to output data (default: false). Useful for debugging data transformations.' 80 | }, 81 | includeData: { 82 | type: 'boolean', 83 | required: false, 84 | description: 'DEPRECATED: Legacy parameter. Use mode instead. If true, maps to mode="summary" for backward compatibility.' 85 | } 86 | }, 87 | 88 | returns: `**Preview Mode Response**: 89 | { 90 | mode: 'preview', 91 | preview: { 92 | totalNodes: number, 93 | executedNodes: number, 94 | estimatedSizeKB: number, 95 | nodes: { 96 | [nodeName]: { 97 | status: 'success' | 'error', 98 | itemCounts: { input: number, output: number }, 99 | dataStructure: {...}, // JSON schema 100 | estimatedSizeKB: number 101 | } 102 | } 103 | }, 104 | recommendation: { 105 | canFetchFull: boolean, 106 | suggestedMode: 'preview'|'summary'|'filtered'|'full', 107 | suggestedItemsLimit?: number, 108 | reason: string 109 | } 110 | } 111 | 112 | **Summary/Filtered/Full Mode Response**: 113 | { 114 | mode: 'summary' | 'filtered' | 'full', 115 | summary: { 116 | totalNodes: number, 117 | executedNodes: number, 118 | totalItems: number, 119 | hasMoreData: boolean // true if truncated 120 | }, 121 | nodes: { 122 | [nodeName]: { 123 | executionTime: number, 124 | itemsInput: number, 125 | itemsOutput: number, 126 | status: 'success' | 'error', 127 | error?: string, 128 | data: { 129 | output: [...], // Actual data items 130 | metadata: { 131 | totalItems: number, 132 | itemsShown: number, 133 | truncated: boolean 134 | } 135 | } 136 | } 137 | } 138 | }`, 139 | 140 | examples: [ 141 | `// Example 1: Preview workflow (RECOMMENDED FIRST STEP) 142 | n8n_get_execution({id: "exec_123", mode: "preview"}) 143 | // Returns structure, counts, size, recommendation 144 | // Use this to decide how to fetch data`, 145 | 146 | `// Example 2: Follow recommendation 147 | const preview = n8n_get_execution({id: "exec_123", mode: "preview"}); 148 | if (preview.recommendation.canFetchFull) { 149 | n8n_get_execution({id: "exec_123", mode: "full"}); 150 | } else { 151 | n8n_get_execution({ 152 | id: "exec_123", 153 | mode: "filtered", 154 | itemsLimit: preview.recommendation.suggestedItemsLimit 155 | }); 156 | }`, 157 | 158 | `// Example 3: Summary mode (safe default for unknown datasets) 159 | n8n_get_execution({id: "exec_123", mode: "summary"}) 160 | // Gets 2 items per node - safe for most cases`, 161 | 162 | `// Example 4: Filter to specific node 163 | n8n_get_execution({ 164 | id: "exec_123", 165 | mode: "filtered", 166 | nodeNames: ["HTTP Request"], 167 | itemsLimit: 5 168 | }) 169 | // Gets only HTTP Request node, 5 items`, 170 | 171 | `// Example 5: Structure only (see data shape) 172 | n8n_get_execution({ 173 | id: "exec_123", 174 | mode: "filtered", 175 | itemsLimit: 0 176 | }) 177 | // Returns JSON schema without actual values`, 178 | 179 | `// Example 6: Debug with input data 180 | n8n_get_execution({ 181 | id: "exec_123", 182 | mode: "filtered", 183 | nodeNames: ["Transform"], 184 | itemsLimit: 2, 185 | includeInputData: true 186 | }) 187 | // See both input and output for debugging`, 188 | 189 | `// Example 7: Backward compatibility (legacy) 190 | n8n_get_execution({id: "exec_123"}) // Minimal data 191 | n8n_get_execution({id: "exec_123", includeData: true}) // Maps to summary mode` 192 | ], 193 | 194 | useCases: [ 195 | 'Monitor status of triggered workflows', 196 | 'Debug failed workflows by examining error messages and partial data', 197 | 'Inspect large datasets without exceeding token limits', 198 | 'Validate data transformations between nodes', 199 | 'Understand execution flow and timing', 200 | 'Track workflow performance metrics', 201 | 'Verify successful completion before proceeding', 202 | 'Extract specific data from execution results' 203 | ], 204 | 205 | performance: `**Response Times** (approximate): 206 | - Preview mode: <50ms (no data, just structure) 207 | - Summary mode: <200ms (2 items per node) 208 | - Filtered mode: 50-500ms (depends on filters) 209 | - Full mode: 200ms-5s (depends on data size) 210 | 211 | **Token Consumption**: 212 | - Preview: ~500 tokens (no data values) 213 | - Summary (2 items): ~2-5K tokens 214 | - Filtered (5 items): ~5-15K tokens 215 | - Full (50+ items): 50K+ tokens (may exceed limits) 216 | 217 | **Optimization Tips**: 218 | - Use preview for all large datasets 219 | - Use nodeNames to focus on relevant nodes only 220 | - Start with small itemsLimit and increase if needed 221 | - Use itemsLimit: 0 to see structure without data`, 222 | 223 | bestPractices: [ 224 | 'ALWAYS use preview mode first for unknown datasets', 225 | 'Trust the recommendation.suggestedMode from preview', 226 | 'Use nodeNames to filter to relevant nodes only', 227 | 'Start with summary mode if preview indicates moderate size', 228 | 'Use itemsLimit: 0 to understand data structure', 229 | 'Check hasMoreData to know if results are truncated', 230 | 'Store execution IDs from triggers for later inspection', 231 | 'Use mode="filtered" with custom limits for large datasets', 232 | 'Include input data only when debugging transformations', 233 | 'Monitor summary.totalItems to understand dataset size' 234 | ], 235 | 236 | pitfalls: [ 237 | 'DON\'T fetch full mode without previewing first - may timeout', 238 | 'DON\'T assume all data fits - always check hasMoreData', 239 | 'DON\'T ignore the recommendation from preview mode', 240 | 'Execution data is retained based on n8n settings - old executions may be purged', 241 | 'Binary data (files, images) is not fully included - only metadata', 242 | 'Status "waiting" indicates execution is still running', 243 | 'Error executions may have partial data from successful nodes', 244 | 'Very large individual items (>1MB) may be truncated', 245 | 'Preview mode estimates may be off by 10-20% for complex structures', 246 | 'Node names are case-sensitive in nodeNames filter' 247 | ], 248 | 249 | modeComparison: `**When to use each mode**: 250 | 251 | **Preview**: 252 | - ALWAYS use first for unknown datasets 253 | - When you need to know if data is safe to fetch 254 | - To see data structure without consuming tokens 255 | - To get size estimates and recommendations 256 | 257 | **Summary** (default): 258 | - Safe default for most cases 259 | - When you need representative samples 260 | - When preview recommends it 261 | - For quick data inspection 262 | 263 | **Filtered**: 264 | - When you need specific nodes only 265 | - When you need more than 2 items but not all 266 | - When preview recommends it with itemsLimit 267 | - For targeted data extraction 268 | 269 | **Full**: 270 | - ONLY when preview says canFetchFull: true 271 | - For small executions (< 20 items total) 272 | - When you genuinely need all data 273 | - When you're certain data fits in token limit`, 274 | 275 | relatedTools: [ 276 | 'n8n_list_executions - Find execution IDs', 277 | 'n8n_trigger_webhook_workflow - Trigger and get execution ID', 278 | 'n8n_delete_execution - Clean up old executions', 279 | 'n8n_get_workflow - Get workflow structure', 280 | 'validate_workflow - Validate before executing' 281 | ] 282 | } 283 | }; 284 | ```