diff --git a/tests/integration/database/performance.test.ts b/tests/integration/database/performance.test.ts new file mode 100644 index 0000000..878273f --- /dev/null +++ b/tests/integration/database/performance.test.ts @@ -0,0 +1,421 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import * as Database from 'better-sqlite3'; +import { NodeRepository } from '../../../src/database/node-repository'; +import { TemplateRepository } from '../../../src/templates/template-repository'; +import { DatabaseAdapter } from '../../../src/database/database-adapter'; +import { TestDatabase, TestDataGenerator, PerformanceMonitor } from './test-utils'; +import { ParsedNode } from '../../../src/parsers/node-parser'; + +describe('Database Performance Tests', () => { + let testDb: TestDatabase; + let db: Database; + let nodeRepo: NodeRepository; + let templateRepo: TemplateRepository; + let adapter: DatabaseAdapter; + let monitor: PerformanceMonitor; + + beforeEach(async () => { + testDb = new TestDatabase({ mode: 'file', name: 'performance-test.db', enableFTS5: true }); + db = await testDb.initialize(); + adapter = new DatabaseAdapter(db); + nodeRepo = new NodeRepository(adapter); + templateRepo = new TemplateRepository(adapter); + monitor = new PerformanceMonitor(); + }); + + afterEach(async () => { + monitor.clear(); + await testDb.cleanup(); + }); + + describe('Node Repository Performance', () => { + it('should handle bulk inserts efficiently', () => { + const nodeCounts = [100, 1000, 5000]; + + nodeCounts.forEach(count => { + const nodes = generateNodes(count); + + const stop = monitor.start(`insert_${count}_nodes`); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + stop(); + }); + + // Check performance metrics + const stats100 = monitor.getStats('insert_100_nodes'); + const stats1000 = monitor.getStats('insert_1000_nodes'); + const stats5000 = monitor.getStats('insert_5000_nodes'); + + expect(stats100!.average).toBeLessThan(100); // 100 nodes in under 100ms + expect(stats1000!.average).toBeLessThan(500); // 1000 nodes in under 500ms + expect(stats5000!.average).toBeLessThan(2000); // 5000 nodes in under 2s + + // Performance should scale sub-linearly + const ratio1000to100 = stats1000!.average / stats100!.average; + const ratio5000to1000 = stats5000!.average / stats1000!.average; + expect(ratio1000to100).toBeLessThan(10); // Should be better than linear scaling + expect(ratio5000to1000).toBeLessThan(5); + }); + + it('should search nodes quickly with indexes', () => { + // Insert test data + const nodes = generateNodes(10000); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + + // Test different search scenarios + const searchTests = [ + { query: 'webhook', mode: 'OR' as const }, + { query: 'http request', mode: 'AND' as const }, + { query: 'automation data', mode: 'OR' as const }, + { query: 'HTT', mode: 'FUZZY' as const } + ]; + + searchTests.forEach(test => { + const stop = monitor.start(`search_${test.query}_${test.mode}`); + const results = nodeRepo.searchNodes(test.query, test.mode, 100); + stop(); + + expect(results.length).toBeGreaterThan(0); + }); + + // All searches should be fast + searchTests.forEach(test => { + const stats = monitor.getStats(`search_${test.query}_${test.mode}`); + expect(stats!.average).toBeLessThan(50); // Each search under 50ms + }); + }); + + it('should handle concurrent reads efficiently', () => { + // Insert initial data + const nodes = generateNodes(1000); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + + // Simulate concurrent reads + const readOperations = 100; + const promises: Promise[] = []; + + const stop = monitor.start('concurrent_reads'); + + for (let i = 0; i < readOperations; i++) { + promises.push( + Promise.resolve(nodeRepo.getNode(`n8n-nodes-base.node${i % 1000}`)) + ); + } + + Promise.all(promises); + stop(); + + const stats = monitor.getStats('concurrent_reads'); + expect(stats!.average).toBeLessThan(100); // 100 reads in under 100ms + + // Average per read should be very low + const avgPerRead = stats!.average / readOperations; + expect(avgPerRead).toBeLessThan(1); // Less than 1ms per read + }); + }); + + describe('Template Repository Performance with FTS5', () => { + it('should perform FTS5 searches efficiently', () => { + // Insert templates with varied content + const templates = Array.from({ length: 10000 }, (_, i) => ({ + id: i + 1, + name: `${['Webhook', 'HTTP', 'Automation', 'Data Processing'][i % 4]} Workflow ${i}`, + description: generateDescription(i), + workflow: { + nodes: [ + { + id: 'node1', + name: 'Start', + type: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.set'][i % 3], + typeVersion: 1, + position: [100, 100], + parameters: {} + } + ], + connections: {}, + settings: {} + }, + user: { username: 'user' }, + views: Math.floor(Math.random() * 1000), + totalViews: Math.floor(Math.random() * 1000), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + })); + + const stop1 = monitor.start('insert_templates_with_fts'); + const transaction = db.transaction((templates: any[]) => { + templates.forEach(t => templateRepo.saveTemplate(t)); + }); + transaction(templates); + stop1(); + + // Test various FTS5 searches + const searchTests = [ + 'webhook', + 'data processing', + 'automat*', + '"HTTP Workflow"', + 'webhook OR http', + 'processing NOT webhook' + ]; + + searchTests.forEach(query => { + const stop = monitor.start(`fts5_search_${query}`); + const results = templateRepo.searchTemplates(query, 100); + stop(); + + expect(results.length).toBeGreaterThan(0); + }); + + // All FTS5 searches should be very fast + searchTests.forEach(query => { + const stats = monitor.getStats(`fts5_search_${query}`); + expect(stats!.average).toBeLessThan(20); // FTS5 searches under 20ms + }); + }); + + it('should handle complex node type searches efficiently', () => { + // Insert templates with various node combinations + const nodeTypes = [ + 'n8n-nodes-base.webhook', + 'n8n-nodes-base.httpRequest', + 'n8n-nodes-base.slack', + 'n8n-nodes-base.googleSheets', + 'n8n-nodes-base.mongodb' + ]; + + const templates = Array.from({ length: 5000 }, (_, i) => ({ + id: i + 1, + name: `Template ${i}`, + workflow: { + nodes: Array.from({ length: 3 }, (_, j) => ({ + id: `node${j}`, + name: `Node ${j}`, + type: nodeTypes[(i + j) % nodeTypes.length], + typeVersion: 1, + position: [100 * j, 100], + parameters: {} + })), + connections: {}, + settings: {} + }, + user: { username: 'user' }, + views: 100, + totalViews: 100, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + })); + + const insertTransaction = db.transaction((templates: any[]) => { + templates.forEach(t => templateRepo.saveTemplate(t)); + }); + insertTransaction(templates); + + // Test searching by node types + const stop = monitor.start('search_by_node_types'); + const results = templateRepo.getTemplatesByNodeTypes([ + 'n8n-nodes-base.webhook', + 'n8n-nodes-base.slack' + ], 100); + stop(); + + expect(results.length).toBeGreaterThan(0); + + const stats = monitor.getStats('search_by_node_types'); + expect(stats!.average).toBeLessThan(50); // Complex JSON searches under 50ms + }); + }); + + describe('Database Optimization', () => { + it('should benefit from proper indexing', () => { + // Insert data + const nodes = generateNodes(5000); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + + // Test queries that use indexes + const indexedQueries = [ + () => nodeRepo.getNodesByPackage('n8n-nodes-base'), + () => nodeRepo.getNodesByCategory('trigger'), + () => nodeRepo.getAITools() + ]; + + indexedQueries.forEach((query, i) => { + const stop = monitor.start(`indexed_query_${i}`); + const results = query(); + stop(); + + expect(Array.isArray(results)).toBe(true); + }); + + // All indexed queries should be fast + indexedQueries.forEach((_, i) => { + const stats = monitor.getStats(`indexed_query_${i}`); + expect(stats!.average).toBeLessThan(20); // Indexed queries under 20ms + }); + }); + + it('should handle VACUUM operation efficiently', () => { + // Insert and delete data to create fragmentation + const nodes = generateNodes(1000); + + // Insert + const insertTx = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + insertTx(nodes); + + // Delete half + db.prepare('DELETE FROM nodes WHERE ROWID % 2 = 0').run(); + + // Measure VACUUM performance + const stop = monitor.start('vacuum'); + db.exec('VACUUM'); + stop(); + + const stats = monitor.getStats('vacuum'); + expect(stats!.average).toBeLessThan(1000); // VACUUM under 1 second + + // Verify database still works + const remaining = nodeRepo.getAllNodes(); + expect(remaining.length).toBeGreaterThan(0); + }); + + it('should maintain performance with WAL mode', () => { + // Verify WAL mode is enabled + const mode = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string }; + expect(mode.journal_mode).toBe('wal'); + + // Perform mixed read/write operations + const operations = 1000; + + const stop = monitor.start('wal_mixed_operations'); + + for (let i = 0; i < operations; i++) { + if (i % 10 === 0) { + // Write operation + const node = generateNodes(1)[0]; + nodeRepo.saveNode(node); + } else { + // Read operation + nodeRepo.getAllNodes(10); + } + } + + stop(); + + const stats = monitor.getStats('wal_mixed_operations'); + expect(stats!.average).toBeLessThan(500); // Mixed operations under 500ms + }); + }); + + describe('Memory Usage', () => { + it('should handle large result sets without excessive memory', () => { + // Insert large dataset + const nodes = generateNodes(10000); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + + // Measure memory before + const memBefore = process.memoryUsage().heapUsed; + + // Fetch large result set + const stop = monitor.start('large_result_set'); + const results = nodeRepo.getAllNodes(); + stop(); + + // Measure memory after + const memAfter = process.memoryUsage().heapUsed; + const memIncrease = (memAfter - memBefore) / 1024 / 1024; // MB + + expect(results).toHaveLength(10000); + expect(memIncrease).toBeLessThan(100); // Less than 100MB increase + + const stats = monitor.getStats('large_result_set'); + expect(stats!.average).toBeLessThan(200); // Fetch 10k records under 200ms + }); + }); + + describe('Concurrent Write Performance', () => { + it('should handle concurrent writes with transactions', () => { + const writeBatches = 10; + const nodesPerBatch = 100; + + const stop = monitor.start('concurrent_writes'); + + // Simulate concurrent write batches + const promises = Array.from({ length: writeBatches }, (_, i) => { + return new Promise((resolve) => { + const nodes = generateNodes(nodesPerBatch, i * nodesPerBatch); + const transaction = db.transaction((nodes: ParsedNode[]) => { + nodes.forEach(node => nodeRepo.saveNode(node)); + }); + transaction(nodes); + resolve(); + }); + }); + + Promise.all(promises); + stop(); + + const stats = monitor.getStats('concurrent_writes'); + expect(stats!.average).toBeLessThan(500); // All writes under 500ms + + // Verify all nodes were written + const count = nodeRepo.getNodeCount(); + expect(count).toBe(writeBatches * nodesPerBatch); + }); + }); +}); + +// Helper functions +function generateNodes(count: number, startId: number = 0): ParsedNode[] { + const categories = ['trigger', 'automation', 'transform', 'output']; + const packages = ['n8n-nodes-base', '@n8n/n8n-nodes-langchain']; + + return Array.from({ length: count }, (_, i) => ({ + nodeType: `n8n-nodes-base.node${startId + i}`, + packageName: packages[i % packages.length], + displayName: `Node ${startId + i}`, + description: `Description for node ${startId + i} with ${['webhook', 'http', 'automation', 'data'][i % 4]} functionality`, + category: categories[i % categories.length], + style: 'programmatic' as const, + isAITool: i % 10 === 0, + isTrigger: categories[i % categories.length] === 'trigger', + isWebhook: i % 5 === 0, + isVersioned: true, + version: '1', + documentation: i % 3 === 0 ? `Documentation for node ${i}` : null, + properties: Array.from({ length: 5 }, (_, j) => ({ + displayName: `Property ${j}`, + name: `prop${j}`, + type: 'string', + default: '' + })), + operations: [], + credentials: i % 4 === 0 ? [{ name: 'httpAuth', required: true }] : [] + })); +} + +function generateDescription(index: number): string { + const descriptions = [ + 'Automate your workflow with powerful webhook integrations', + 'Process HTTP requests and transform data efficiently', + 'Connect to external APIs and sync data seamlessly', + 'Build complex automation workflows with ease', + 'Transform and filter data with advanced operations' + ]; + return descriptions[index % descriptions.length] + ` - Version ${index}`; +} \ No newline at end of file diff --git a/tests/integration/database/template-repository.test.ts b/tests/integration/database/template-repository.test.ts new file mode 100644 index 0000000..73fccd8 --- /dev/null +++ b/tests/integration/database/template-repository.test.ts @@ -0,0 +1,439 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import * as Database from 'better-sqlite3'; +import { TemplateRepository } from '../../../src/templates/template-repository'; +import { DatabaseAdapter } from '../../../src/database/database-adapter'; +import { TestDatabase, TestDataGenerator } from './test-utils'; +import { TemplateWorkflow, TemplateDetail } from '../../../src/templates/template-fetcher'; + +describe('TemplateRepository Integration Tests', () => { + let testDb: TestDatabase; + let db: Database; + let repository: TemplateRepository; + let adapter: DatabaseAdapter; + + beforeEach(async () => { + testDb = new TestDatabase({ mode: 'memory', enableFTS5: true }); + db = await testDb.initialize(); + adapter = new DatabaseAdapter(db); + repository = new TemplateRepository(adapter); + }); + + afterEach(async () => { + await testDb.cleanup(); + }); + + describe('saveTemplate', () => { + it('should save single template successfully', () => { + const template = createTemplateWorkflow(); + repository.saveTemplate(template); + + const saved = repository.getTemplate(template.id); + expect(saved).toBeTruthy(); + expect(saved?.workflow_id).toBe(template.id); + expect(saved?.name).toBe(template.name); + }); + + it('should update existing template', () => { + const template = createTemplateWorkflow(); + + // Save initial version + repository.saveTemplate(template); + + // Update and save again + const updated: TemplateWorkflow = { ...template, name: 'Updated Template' }; + repository.saveTemplate(updated); + + const saved = repository.getTemplate(template.id); + expect(saved?.name).toBe('Updated Template'); + + // Should not create duplicate + const all = repository.getAllTemplates(); + expect(all).toHaveLength(1); + }); + + it('should handle templates with complex node types', () => { + const template = createTemplateWorkflow({ + id: 1, + nodes: [ + { + id: 'node1', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + typeVersion: 1, + position: [100, 100], + parameters: {} + }, + { + id: 'node2', + name: 'HTTP Request', + type: 'n8n-nodes-base.httpRequest', + typeVersion: 3, + position: [300, 100], + parameters: { + url: 'https://api.example.com', + method: 'POST' + } + } + ] + }); + + repository.saveTemplate(template); + + const saved = repository.getTemplate(template.id); + expect(saved).toBeTruthy(); + + const nodesUsed = JSON.parse(saved!.nodes_used); + expect(nodesUsed).toContain('n8n-nodes-base.webhook'); + expect(nodesUsed).toContain('n8n-nodes-base.httpRequest'); + }); + + it('should sanitize workflow data before saving', () => { + const template = createTemplateWorkflow({ + workflowInfo: { + nodeCount: 5, + webhookCount: 1, + // Add some data that should be sanitized + executionId: 'should-be-removed', + pinData: { node1: { data: 'sensitive' } } + } + }); + + repository.saveTemplate(template); + + const saved = repository.getTemplate(template.id); + expect(saved).toBeTruthy(); + + const workflowJson = JSON.parse(saved!.workflow_json); + expect(workflowJson.pinData).toBeUndefined(); + }); + }); + + describe('getTemplate', () => { + beforeEach(() => { + const templates = [ + createTemplateWorkflow({ id: 1, name: 'Template 1' }), + createTemplateWorkflow({ id: 2, name: 'Template 2' }) + ]; + templates.forEach(t => repository.saveTemplate(t)); + }); + + it('should retrieve template by id', () => { + const template = repository.getTemplate(1); + expect(template).toBeTruthy(); + expect(template?.name).toBe('Template 1'); + }); + + it('should return null for non-existent template', () => { + const template = repository.getTemplate(999); + expect(template).toBeNull(); + }); + }); + + describe('searchTemplates with FTS5', () => { + beforeEach(() => { + const templates = [ + createTemplateWorkflow({ + id: 1, + name: 'Webhook to Slack', + description: 'Send Slack notifications when webhook received' + }), + createTemplateWorkflow({ + id: 2, + name: 'HTTP Data Processing', + description: 'Process data from HTTP requests' + }), + createTemplateWorkflow({ + id: 3, + name: 'Email Automation', + description: 'Automate email sending workflow' + }) + ]; + templates.forEach(t => repository.saveTemplate(t)); + }); + + it('should search templates by name', () => { + const results = repository.searchTemplates('webhook'); + expect(results).toHaveLength(1); + expect(results[0].name).toBe('Webhook to Slack'); + }); + + it('should search templates by description', () => { + const results = repository.searchTemplates('automate'); + expect(results).toHaveLength(1); + expect(results[0].name).toBe('Email Automation'); + }); + + it('should handle multiple search terms', () => { + const results = repository.searchTemplates('data process'); + expect(results).toHaveLength(1); + expect(results[0].name).toBe('HTTP Data Processing'); + }); + + it('should limit search results', () => { + // Add more templates + for (let i = 4; i <= 20; i++) { + repository.saveTemplate(createTemplateWorkflow({ + id: i, + name: `Test Template ${i}`, + description: 'Test description' + })); + } + + const results = repository.searchTemplates('test', 5); + expect(results).toHaveLength(5); + }); + + it('should handle special characters in search', () => { + repository.saveTemplate(createTemplateWorkflow({ + id: 100, + name: 'Special @ # $ Template', + description: 'Template with special characters' + })); + + const results = repository.searchTemplates('special'); + expect(results.length).toBeGreaterThan(0); + }); + }); + + describe('getTemplatesByNodeTypes', () => { + beforeEach(() => { + const templates = [ + createTemplateWorkflow({ + id: 1, + nodes: [ + { type: 'n8n-nodes-base.webhook' }, + { type: 'n8n-nodes-base.slack' } + ] + }), + createTemplateWorkflow({ + id: 2, + nodes: [ + { type: 'n8n-nodes-base.httpRequest' }, + { type: 'n8n-nodes-base.set' } + ] + }), + createTemplateWorkflow({ + id: 3, + nodes: [ + { type: 'n8n-nodes-base.webhook' }, + { type: 'n8n-nodes-base.httpRequest' } + ] + }) + ]; + templates.forEach(t => repository.saveTemplate(t)); + }); + + it('should find templates using specific node types', () => { + const results = repository.getTemplatesByNodeTypes(['n8n-nodes-base.webhook']); + expect(results).toHaveLength(2); + expect(results.map(r => r.workflow_id)).toContain(1); + expect(results.map(r => r.workflow_id)).toContain(3); + }); + + it('should find templates using multiple node types', () => { + const results = repository.getTemplatesByNodeTypes([ + 'n8n-nodes-base.webhook', + 'n8n-nodes-base.slack' + ]); + expect(results).toHaveLength(1); + expect(results[0].workflow_id).toBe(1); + }); + + it('should return empty array for non-existent node types', () => { + const results = repository.getTemplatesByNodeTypes(['non-existent-node']); + expect(results).toHaveLength(0); + }); + + it('should limit results', () => { + const results = repository.getTemplatesByNodeTypes(['n8n-nodes-base.webhook'], 1); + expect(results).toHaveLength(1); + }); + }); + + describe('getAllTemplates', () => { + it('should return empty array when no templates', () => { + const templates = repository.getAllTemplates(); + expect(templates).toHaveLength(0); + }); + + it('should return all templates with limit', () => { + for (let i = 1; i <= 20; i++) { + repository.saveTemplate(createTemplateWorkflow({ id: i })); + } + + const templates = repository.getAllTemplates(10); + expect(templates).toHaveLength(10); + }); + + it('should order templates by updated_at descending', () => { + // Save templates with slight delay to ensure different timestamps + const template1 = createTemplateWorkflow({ id: 1, name: 'First' }); + repository.saveTemplate(template1); + + // Small delay + const template2 = createTemplateWorkflow({ id: 2, name: 'Second' }); + repository.saveTemplate(template2); + + const templates = repository.getAllTemplates(); + expect(templates).toHaveLength(2); + // Most recent should be first + expect(templates[0].name).toBe('Second'); + }); + }); + + describe('getTemplateDetail', () => { + it('should return template with full workflow data', () => { + const template = createTemplateDetail(); + repository.saveTemplateDetail(template); + + const saved = repository.getTemplateDetail(template.id); + expect(saved).toBeTruthy(); + expect(saved?.workflow).toBeTruthy(); + expect(saved?.workflow.nodes).toHaveLength(template.workflow.nodes.length); + }); + + it('should handle missing workflow gracefully', () => { + const template = createTemplateWorkflow({ id: 1 }); + repository.saveTemplate(template); + + const detail = repository.getTemplateDetail(1); + expect(detail).toBeNull(); + }); + }); + + describe('clearOldTemplates', () => { + it('should remove templates older than specified days', () => { + // Insert old template (30 days ago) + db.prepare(` + INSERT INTO templates ( + id, workflow_id, name, description, + nodes_used, workflow_json, categories, views, + created_at, updated_at + ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now', '-31 days'), datetime('now', '-31 days')) + `).run(1, 1001, 'Old Template', 'Old template'); + + // Insert recent template + repository.saveTemplate(createTemplateWorkflow({ id: 2, name: 'Recent Template' })); + + // Clear templates older than 30 days + const deleted = repository.clearOldTemplates(30); + expect(deleted).toBe(1); + + const remaining = repository.getAllTemplates(); + expect(remaining).toHaveLength(1); + expect(remaining[0].name).toBe('Recent Template'); + }); + }); + + describe('Transaction handling', () => { + it('should rollback on error during bulk save', () => { + const templates = [ + createTemplateWorkflow({ id: 1 }), + createTemplateWorkflow({ id: 2 }), + { id: null } as any // Invalid template + ]; + + expect(() => { + templates.forEach(t => repository.saveTemplate(t)); + }).toThrow(); + + // No templates should be saved due to error + const all = repository.getAllTemplates(); + expect(all).toHaveLength(0); + }); + }); + + describe('FTS5 performance', () => { + it('should handle large dataset searches efficiently', () => { + // Insert 1000 templates + const templates = Array.from({ length: 1000 }, (_, i) => + createTemplateWorkflow({ + id: i + 1, + name: `Template ${i}`, + description: `Description for ${['webhook', 'http', 'automation', 'data'][i % 4]} workflow ${i}` + }) + ); + + const insertMany = db.transaction((templates: TemplateWorkflow[]) => { + templates.forEach(t => repository.saveTemplate(t)); + }); + + const start = Date.now(); + insertMany(templates); + const insertDuration = Date.now() - start; + + expect(insertDuration).toBeLessThan(2000); // Should complete in under 2 seconds + + // Test search performance + const searchStart = Date.now(); + const results = repository.searchTemplates('webhook', 50); + const searchDuration = Date.now() - searchStart; + + expect(searchDuration).toBeLessThan(50); // Search should be very fast + expect(results).toHaveLength(50); + }); + }); +}); + +// Helper functions +function createTemplateWorkflow(overrides: any = {}): TemplateWorkflow { + const id = overrides.id || Math.floor(Math.random() * 10000); + const nodes = overrides.nodes || [ + { + id: 'node1', + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [100, 100], + parameters: {} + } + ]; + + return { + id, + name: overrides.name || `Test Workflow ${id}`, + workflow: { + nodes: nodes.map((n: any) => ({ + id: n.id || 'node1', + name: n.name || 'Node', + type: n.type || 'n8n-nodes-base.start', + typeVersion: n.typeVersion || 1, + position: n.position || [100, 100], + parameters: n.parameters || {} + })), + connections: overrides.connections || {}, + settings: overrides.settings || {} + }, + user: { + username: overrides.username || 'testuser' + }, + views: overrides.views || 100, + totalViews: overrides.totalViews || 100, + createdAt: overrides.createdAt || new Date().toISOString(), + updatedAt: overrides.updatedAt || new Date().toISOString(), + description: overrides.description, + workflowInfo: overrides.workflowInfo || { + nodeCount: nodes.length, + webhookCount: nodes.filter((n: any) => n.type?.includes('webhook')).length + }, + ...overrides + }; +} + +function createTemplateDetail(overrides: any = {}): TemplateDetail { + const base = createTemplateWorkflow(overrides); + return { + ...base, + workflow: { + id: base.id.toString(), + name: base.name, + nodes: base.workflow.nodes, + connections: base.workflow.connections, + settings: base.workflow.settings, + pinData: overrides.pinData + }, + categories: overrides.categories || [ + { id: 1, name: 'automation' } + ] + }; +} \ No newline at end of file