Skip to main content
Glama

n8n-MCP

by 88-888
database-utils.tsβ€’13.7 kB
import { DatabaseAdapter, createDatabaseAdapter } from '../../src/database/database-adapter'; import { NodeRepository } from '../../src/database/node-repository'; import { TemplateRepository } from '../../src/templates/template-repository'; import { ParsedNode } from '../../src/parsers/node-parser'; import { TemplateWorkflow, TemplateNode, TemplateUser, TemplateDetail } from '../../src/templates/template-fetcher'; import * as fs from 'fs'; import * as path from 'path'; import { vi } from 'vitest'; /** * Database test utilities for n8n-mcp * Provides helpers for creating, seeding, and managing test databases */ export interface TestDatabaseOptions { /** * Use in-memory database (default: true) * When false, creates a temporary file database */ inMemory?: boolean; /** * Custom database path (only used when inMemory is false) */ dbPath?: string; /** * Initialize with schema (default: true) */ initSchema?: boolean; /** * Enable FTS5 support if available (default: false) */ enableFTS5?: boolean; } export interface TestDatabase { adapter: DatabaseAdapter; nodeRepository: NodeRepository; templateRepository: TemplateRepository; path: string; cleanup: () => Promise<void>; } export interface DatabaseSnapshot { nodes: any[]; templates: any[]; metadata: { createdAt: string; nodeCount: number; templateCount: number; }; } /** * Creates a test database with repositories */ export async function createTestDatabase(options: TestDatabaseOptions = {}): Promise<TestDatabase> { const { inMemory = true, dbPath, initSchema = true, enableFTS5 = false } = options; // Determine database path const finalPath = inMemory ? ':memory:' : dbPath || path.join(__dirname, `../temp/test-${Date.now()}.db`); // Ensure directory exists for file-based databases if (!inMemory) { const dir = path.dirname(finalPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // Create database adapter const adapter = await createDatabaseAdapter(finalPath); // Initialize schema if requested if (initSchema) { await initializeDatabaseSchema(adapter, enableFTS5); } // Create repositories const nodeRepository = new NodeRepository(adapter); const templateRepository = new TemplateRepository(adapter); // Cleanup function const cleanup = async () => { adapter.close(); if (!inMemory && fs.existsSync(finalPath)) { fs.unlinkSync(finalPath); } }; return { adapter, nodeRepository, templateRepository, path: finalPath, cleanup }; } /** * Initializes database schema from SQL file */ export async function initializeDatabaseSchema(adapter: DatabaseAdapter, enableFTS5 = false): Promise<void> { const schemaPath = path.join(__dirname, '../../src/database/schema.sql'); const schema = fs.readFileSync(schemaPath, 'utf-8'); // Execute main schema adapter.exec(schema); // Optionally initialize FTS5 tables if (enableFTS5 && adapter.checkFTS5Support()) { adapter.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5( name, description, content='templates', content_rowid='id' ); -- Trigger to keep FTS index in sync CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON templates BEGIN INSERT INTO templates_fts(rowid, name, description) VALUES (new.id, new.name, new.description); END; CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON templates BEGIN UPDATE templates_fts SET name = new.name, description = new.description WHERE rowid = new.id; END; CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON templates BEGIN DELETE FROM templates_fts WHERE rowid = old.id; END; `); } } /** * Seeds test nodes into the database */ export async function seedTestNodes( nodeRepository: NodeRepository, nodes: Partial<ParsedNode>[] = [] ): Promise<ParsedNode[]> { const defaultNodes: ParsedNode[] = [ createTestNode({ nodeType: 'nodes-base.httpRequest', displayName: 'HTTP Request', description: 'Makes HTTP requests', category: 'Core Nodes', isAITool: true }), createTestNode({ nodeType: 'nodes-base.webhook', displayName: 'Webhook', description: 'Receives webhook calls', category: 'Core Nodes', isTrigger: true, isWebhook: true }), createTestNode({ nodeType: 'nodes-base.slack', displayName: 'Slack', description: 'Send messages to Slack', category: 'Communication', isAITool: true }) ]; const allNodes = [...defaultNodes, ...nodes.map(n => createTestNode(n))]; for (const node of allNodes) { nodeRepository.saveNode(node); } return allNodes; } /** * Seeds test templates into the database */ export async function seedTestTemplates( templateRepository: TemplateRepository, templates: Partial<TemplateWorkflow>[] = [] ): Promise<TemplateWorkflow[]> { const defaultTemplates: TemplateWorkflow[] = [ createTestTemplate({ id: 1, name: 'Simple HTTP Workflow', description: 'Basic HTTP request workflow', nodes: [{ id: 1, name: 'HTTP Request', icon: 'http' }] }), createTestTemplate({ id: 2, name: 'Webhook to Slack', description: 'Webhook that sends to Slack', nodes: [ { id: 1, name: 'Webhook', icon: 'webhook' }, { id: 2, name: 'Slack', icon: 'slack' } ] }) ]; const allTemplates = [...defaultTemplates, ...templates.map(t => createTestTemplate(t))]; for (const template of allTemplates) { // Convert to TemplateDetail format for saving const detail: TemplateDetail = { id: template.id, name: template.name, description: template.description, views: template.totalViews, createdAt: template.createdAt, workflow: { nodes: template.nodes?.map((n, i) => ({ id: `node_${i}`, name: n.name, type: `n8n-nodes-base.${n.name.toLowerCase()}`, position: [250 + i * 200, 300], parameters: {} })) || [], connections: {}, settings: {} } }; await templateRepository.saveTemplate(template, detail); } return allTemplates; } /** * Creates a test node with defaults */ export function createTestNode(overrides: Partial<ParsedNode> = {}): ParsedNode { return { style: 'programmatic', nodeType: 'nodes-base.test', displayName: 'Test Node', description: 'A test node', category: 'Test', properties: [], credentials: [], isAITool: false, isTrigger: false, isWebhook: false, operations: [], version: '1', isVersioned: false, packageName: 'n8n-nodes-base', documentation: undefined, ...overrides }; } /** * Creates a test template with defaults */ export function createTestTemplate(overrides: Partial<TemplateWorkflow> = {}): TemplateWorkflow { const id = overrides.id || Math.floor(Math.random() * 10000); return { id, name: `Test Template ${id}`, description: 'A test template', nodes: overrides.nodes || [], user: overrides.user || { id: 1, name: 'Test User', username: 'testuser', verified: false }, createdAt: overrides.createdAt || new Date().toISOString(), totalViews: overrides.totalViews || 100, ...overrides }; } /** * Resets database to clean state */ export async function resetDatabase(adapter: DatabaseAdapter): Promise<void> { // Drop all tables adapter.exec(` DROP TABLE IF EXISTS templates_fts; DROP TABLE IF EXISTS templates; DROP TABLE IF EXISTS nodes; `); // Reinitialize schema await initializeDatabaseSchema(adapter); } /** * Creates a database snapshot */ export async function createDatabaseSnapshot(adapter: DatabaseAdapter): Promise<DatabaseSnapshot> { const nodes = adapter.prepare('SELECT * FROM nodes').all(); const templates = adapter.prepare('SELECT * FROM templates').all(); return { nodes, templates, metadata: { createdAt: new Date().toISOString(), nodeCount: nodes.length, templateCount: templates.length } }; } /** * Restores database from snapshot */ export async function restoreDatabaseSnapshot( adapter: DatabaseAdapter, snapshot: DatabaseSnapshot ): Promise<void> { // Reset database first await resetDatabase(adapter); // Restore nodes const nodeStmt = adapter.prepare(` INSERT INTO nodes ( node_type, package_name, display_name, description, category, development_style, is_ai_tool, is_trigger, is_webhook, is_versioned, version, documentation, properties_schema, operations, credentials_required ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const node of snapshot.nodes) { nodeStmt.run( node.node_type, node.package_name, node.display_name, node.description, node.category, node.development_style, node.is_ai_tool, node.is_trigger, node.is_webhook, node.is_versioned, node.version, node.documentation, node.properties_schema, node.operations, node.credentials_required ); } // Restore templates const templateStmt = adapter.prepare(` INSERT INTO templates ( id, workflow_id, name, description, author_name, author_username, author_verified, nodes_used, workflow_json, categories, views, created_at, updated_at, url ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const template of snapshot.templates) { templateStmt.run( template.id, template.workflow_id, template.name, template.description, template.author_name, template.author_username, template.author_verified, template.nodes_used, template.workflow_json, template.categories, template.views, template.created_at, template.updated_at, template.url ); } } /** * Loads JSON fixtures into database */ export async function loadFixtures( adapter: DatabaseAdapter, fixturePath: string ): Promise<void> { const fixtures = JSON.parse(fs.readFileSync(fixturePath, 'utf-8')); if (fixtures.nodes) { const nodeRepo = new NodeRepository(adapter); for (const node of fixtures.nodes) { nodeRepo.saveNode(node); } } if (fixtures.templates) { const templateRepo = new TemplateRepository(adapter); for (const template of fixtures.templates) { // Convert to proper format const detail: TemplateDetail = { id: template.id, name: template.name, description: template.description, views: template.views || template.totalViews || 0, createdAt: template.createdAt, workflow: template.workflow || { nodes: template.nodes?.map((n: any, i: number) => ({ id: `node_${i}`, name: n.name, type: `n8n-nodes-base.${n.name.toLowerCase()}`, position: [250 + i * 200, 300], parameters: {} })) || [], connections: {}, settings: {} } }; await templateRepo.saveTemplate(template, detail); } } } /** * Database test helpers for common operations */ export const dbHelpers = { /** * Counts rows in a table */ countRows(adapter: DatabaseAdapter, table: string): number { const result = adapter.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as { count: number }; return result.count; }, /** * Checks if a node exists */ nodeExists(adapter: DatabaseAdapter, nodeType: string): boolean { const result = adapter.prepare('SELECT 1 FROM nodes WHERE node_type = ?').get(nodeType); return !!result; }, /** * Gets all node types */ getAllNodeTypes(adapter: DatabaseAdapter): string[] { const rows = adapter.prepare('SELECT node_type FROM nodes').all() as { node_type: string }[]; return rows.map(r => r.node_type); }, /** * Clears a specific table */ clearTable(adapter: DatabaseAdapter, table: string): void { adapter.exec(`DELETE FROM ${table}`); }, /** * Executes raw SQL */ executeSql(adapter: DatabaseAdapter, sql: string): void { adapter.exec(sql); } }; /** * Creates a mock database adapter for unit tests */ export function createMockDatabaseAdapter(): DatabaseAdapter { const mockDb = { prepare: vi.fn(), exec: vi.fn(), close: vi.fn(), pragma: vi.fn(), inTransaction: false, transaction: vi.fn((fn) => fn()), checkFTS5Support: vi.fn(() => false) }; return mockDb as unknown as DatabaseAdapter; } /** * Transaction test helper * Note: better-sqlite3 transactions are synchronous */ export async function withTransaction<T>( adapter: DatabaseAdapter, fn: () => Promise<T> ): Promise<T | null> { try { adapter.exec('BEGIN'); const result = await fn(); // Always rollback for testing adapter.exec('ROLLBACK'); return null; // Indicate rollback happened } catch (error) { adapter.exec('ROLLBACK'); throw error; } } /** * Performance test helper */ export async function measureDatabaseOperation( name: string, operation: () => Promise<void> ): Promise<number> { const start = performance.now(); await operation(); const duration = performance.now() - start; console.log(`[DB Performance] ${name}: ${duration.toFixed(2)}ms`); return duration; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server