Files
n8n-mcp/tests/test-database-extraction.js
czlonkowski 078b67ff35 Implement SQLite database with full-text search for n8n node documentation
Major features implemented:
- SQLite storage service with FTS5 for fast node search
- Database rebuild mechanism for bulk node extraction
- MCP tools: search_nodes, extract_all_nodes, get_node_statistics
- Production Docker deployment with persistent storage
- Management scripts for database operations
- Comprehensive test suite for all functionality

Database capabilities:
- Stores node source code and metadata
- Full-text search by node name or content
- No versioning (stores latest only as per requirements)
- Supports complete database rebuilds
- ~4.5MB database with 500+ nodes indexed

Production features:
- Automated deployment script
- Docker Compose production configuration
- Database initialization on first run
- Volume persistence for data
- Management utilities for operations

Documentation:
- Updated README with complete instructions
- Production deployment guide
- Clear troubleshooting section
- API reference for all new tools

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-07 21:12:17 +00:00

282 lines
9.6 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Test node extraction for database storage
* Focus on extracting known nodes with proper structure for DB storage
*/
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
// Import our extractor
const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor');
// Known n8n nodes to test
const KNOWN_NODES = [
// Core nodes
{ type: 'n8n-nodes-base.Function', package: 'n8n-nodes-base', name: 'Function' },
{ type: 'n8n-nodes-base.Webhook', package: 'n8n-nodes-base', name: 'Webhook' },
{ type: 'n8n-nodes-base.HttpRequest', package: 'n8n-nodes-base', name: 'HttpRequest' },
{ type: 'n8n-nodes-base.If', package: 'n8n-nodes-base', name: 'If' },
{ type: 'n8n-nodes-base.SplitInBatches', package: 'n8n-nodes-base', name: 'SplitInBatches' },
// AI nodes
{ type: '@n8n/n8n-nodes-langchain.Agent', package: '@n8n/n8n-nodes-langchain', name: 'Agent' },
{ type: '@n8n/n8n-nodes-langchain.OpenAiAssistant', package: '@n8n/n8n-nodes-langchain', name: 'OpenAiAssistant' },
{ type: '@n8n/n8n-nodes-langchain.ChainLlm', package: '@n8n/n8n-nodes-langchain', name: 'ChainLlm' },
// Integration nodes
{ type: 'n8n-nodes-base.Airtable', package: 'n8n-nodes-base', name: 'Airtable' },
{ type: 'n8n-nodes-base.GoogleSheets', package: 'n8n-nodes-base', name: 'GoogleSheets' },
{ type: 'n8n-nodes-base.Slack', package: 'n8n-nodes-base', name: 'Slack' },
{ type: 'n8n-nodes-base.Discord', package: 'n8n-nodes-base', name: 'Discord' },
];
// Database schema for storing nodes
const DB_SCHEMA = `
-- Main nodes table
CREATE TABLE IF NOT EXISTS nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_type VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
package_name VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
description TEXT,
version VARCHAR(50),
code_hash VARCHAR(64) NOT NULL,
code_length INTEGER NOT NULL,
source_location TEXT NOT NULL,
has_credentials BOOLEAN DEFAULT FALSE,
extracted_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT idx_node_type UNIQUE (node_type),
INDEX idx_package_name (package_name),
INDEX idx_code_hash (code_hash)
);
-- Source code storage
CREATE TABLE IF NOT EXISTS node_source_code (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
source_code TEXT NOT NULL,
minified_code TEXT,
source_map TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT idx_node_source UNIQUE (node_id)
);
-- Credentials definitions
CREATE TABLE IF NOT EXISTS node_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
credential_type VARCHAR(255) NOT NULL,
credential_code TEXT NOT NULL,
required_fields JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
INDEX idx_node_credentials (node_id)
);
-- Package metadata
CREATE TABLE IF NOT EXISTS node_packages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
package_name VARCHAR(255) UNIQUE NOT NULL,
version VARCHAR(50),
description TEXT,
author VARCHAR(255),
license VARCHAR(50),
repository_url TEXT,
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Node dependencies
CREATE TABLE IF NOT EXISTS node_dependencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
depends_on_node_id UUID NOT NULL REFERENCES nodes(id),
dependency_type VARCHAR(50), -- 'extends', 'imports', 'requires'
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT unique_dependency UNIQUE (node_id, depends_on_node_id)
);
`;
async function main() {
console.log('=== n8n Node Extraction for Database Storage Test ===\n');
const extractor = new NodeSourceExtractor();
const results = {
tested: 0,
extracted: 0,
failed: 0,
nodes: [],
errors: [],
totalSize: 0
};
// Create output directory
const outputDir = path.join(__dirname, 'extracted-nodes-db');
await fs.mkdir(outputDir, { recursive: true });
console.log(`Testing extraction of ${KNOWN_NODES.length} known nodes...\n`);
// Extract each node
for (const nodeConfig of KNOWN_NODES) {
console.log(`📦 Extracting: ${nodeConfig.type}`);
results.tested++;
try {
const startTime = Date.now();
const nodeInfo = await extractor.extractNodeSource(nodeConfig.type);
const extractTime = Date.now() - startTime;
// Calculate hash for deduplication
const codeHash = crypto.createHash('sha256').update(nodeInfo.sourceCode).digest('hex');
// Prepare database record
const dbRecord = {
// Primary data
node_type: nodeConfig.type,
name: nodeConfig.name,
package_name: nodeConfig.package,
code_hash: codeHash,
code_length: nodeInfo.sourceCode.length,
source_location: nodeInfo.location,
has_credentials: !!nodeInfo.credentialCode,
// Source code (separate table in real DB)
source_code: nodeInfo.sourceCode,
credential_code: nodeInfo.credentialCode,
// Package info
package_info: nodeInfo.packageInfo,
// Metadata
extraction_time_ms: extractTime,
extracted_at: new Date().toISOString()
};
results.nodes.push(dbRecord);
results.extracted++;
results.totalSize += nodeInfo.sourceCode.length;
console.log(` ✅ Success: ${nodeInfo.sourceCode.length} bytes (${extractTime}ms)`);
console.log(` 📍 Location: ${nodeInfo.location}`);
console.log(` 🔑 Hash: ${codeHash.substring(0, 12)}...`);
if (nodeInfo.credentialCode) {
console.log(` 🔐 Has credentials: ${nodeInfo.credentialCode.length} bytes`);
}
// Save individual node data
const nodeFile = path.join(outputDir, `${nodeConfig.package}__${nodeConfig.name}.json`);
await fs.writeFile(nodeFile, JSON.stringify(dbRecord, null, 2));
} catch (error) {
results.failed++;
results.errors.push({
node: nodeConfig.type,
error: error.message
});
console.log(` ❌ Failed: ${error.message}`);
}
console.log('');
}
// Generate summary report
const successRate = ((results.extracted / results.tested) * 100).toFixed(1);
console.log('='.repeat(60));
console.log('EXTRACTION SUMMARY');
console.log('='.repeat(60));
console.log(`Total nodes tested: ${results.tested}`);
console.log(`Successfully extracted: ${results.extracted} (${successRate}%)`);
console.log(`Failed: ${results.failed}`);
console.log(`Total code size: ${(results.totalSize / 1024).toFixed(2)} KB`);
console.log(`Average node size: ${(results.totalSize / results.extracted / 1024).toFixed(2)} KB`);
// Test database insertion simulation
console.log('\n📊 Database Storage Simulation:');
console.log('--------------------------------');
if (results.extracted > 0) {
// Group by package
const packages = {};
results.nodes.forEach(node => {
if (!packages[node.package_name]) {
packages[node.package_name] = {
name: node.package_name,
nodes: [],
totalSize: 0
};
}
packages[node.package_name].nodes.push(node.name);
packages[node.package_name].totalSize += node.code_length;
});
console.log('\nPackages:');
Object.values(packages).forEach(pkg => {
console.log(` 📦 ${pkg.name}`);
console.log(` Nodes: ${pkg.nodes.length}`);
console.log(` Total size: ${(pkg.totalSize / 1024).toFixed(2)} KB`);
console.log(` Nodes: ${pkg.nodes.join(', ')}`);
});
// Save database-ready JSON
const dbData = {
schema: DB_SCHEMA,
extracted_at: new Date().toISOString(),
statistics: {
total_nodes: results.extracted,
total_size_bytes: results.totalSize,
packages: Object.keys(packages).length,
success_rate: successRate
},
nodes: results.nodes
};
const dbFile = path.join(outputDir, 'database-import.json');
await fs.writeFile(dbFile, JSON.stringify(dbData, null, 2));
console.log(`\n💾 Database import file saved: ${dbFile}`);
// Create SQL insert statements
const sqlFile = path.join(outputDir, 'insert-nodes.sql');
let sql = '-- Auto-generated SQL for n8n nodes\n\n';
results.nodes.forEach(node => {
sql += `-- Node: ${node.node_type}\n`;
sql += `INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials)\n`;
sql += `VALUES ('${node.node_type}', '${node.name}', '${node.package_name}', '${node.code_hash}', ${node.code_length}, '${node.source_location}', ${node.has_credentials});\n\n`;
});
await fs.writeFile(sqlFile, sql);
console.log(`📝 SQL insert file saved: ${sqlFile}`);
}
// Save full report
const reportFile = path.join(outputDir, 'extraction-report.json');
await fs.writeFile(reportFile, JSON.stringify(results, null, 2));
console.log(`\n📄 Full report saved: ${reportFile}`);
// Show any errors
if (results.errors.length > 0) {
console.log('\n⚠ Extraction Errors:');
results.errors.forEach(err => {
console.log(` - ${err.node}: ${err.error}`);
});
}
console.log('\n✨ Database extraction test completed!');
console.log(`📁 Results saved in: ${outputDir}`);
// Exit with appropriate code
process.exit(results.failed > 0 ? 1 : 0);
}
// Run the test
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});