test: add template repository and performance integration tests

- Add comprehensive TemplateRepository integration tests
- Test FTS5 functionality with templates
- Add performance benchmarks for database operations
- Test concurrent read/write operations
- Measure memory usage and query performance
- Verify index optimization and WAL mode benefits
- Include bulk operation performance tests

Part of Phase 4: Integration Testing
This commit is contained in:
czlonkowski
2025-07-29 09:51:13 +02:00
parent 253b51f5c6
commit 9470986650
2 changed files with 860 additions and 0 deletions

View File

@@ -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<any>[] = [];
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<void>((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}`;
}

View File

@@ -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' }
]
};
}