From 253b51f5c6024fe94bb84a6560e591a6bc25857f Mon Sep 17 00:00:00 2001
From: czlonkowski <56956555+czlonkowski@users.noreply.github.com>
Date: Tue, 29 Jul 2025 09:47:44 +0200
Subject: [PATCH] fix: resolve database integration test issues
- Fix better-sqlite3 import statements to use namespace import
- Update test schemas to match actual database schema
- Align NodeRepository tests with actual API implementation
- Fix FTS5 tests to work with templates instead of nodes
- Update mock data to match ParsedNode interface
- Fix column names to match actual schema (node_type, package_name, etc)
- Add proper ParsedNode creation helper function
- Remove tests for non-existent foreign key constraints
---
.../database/connection-management.test.ts | 28 +-
.../integration/database/fts5-search.test.ts | 639 ++++++++++++++++++
.../database/node-repository.test.ts | 615 +++++++++++++++++
tests/integration/database/test-utils.ts | 96 +--
.../integration/database/transactions.test.ts | 16 +-
5 files changed, 1328 insertions(+), 66 deletions(-)
create mode 100644 tests/integration/database/fts5-search.test.ts
create mode 100644 tests/integration/database/node-repository.test.ts
diff --git a/tests/integration/database/connection-management.test.ts b/tests/integration/database/connection-management.test.ts
index 3118393..7e784c4 100644
--- a/tests/integration/database/connection-management.test.ts
+++ b/tests/integration/database/connection-management.test.ts
@@ -311,36 +311,16 @@ describe('Database Connection Management', () => {
expect(mmap.mmap_size).toBeGreaterThan(0);
});
- it('should enforce foreign key constraints', async () => {
+ it('should have foreign key support enabled', async () => {
testDb = new TestDatabase({ mode: 'memory' });
const db = await testDb.initialize();
- // Foreign keys should be enabled by default in our schema
+ // Foreign keys should be enabled by default
const fkEnabled = db.prepare('PRAGMA foreign_keys').get() as { foreign_keys: number };
expect(fkEnabled.foreign_keys).toBe(1);
- // Test foreign key constraint
- const node = TestDataGenerator.generateNode();
- db.prepare(`
- INSERT INTO nodes (name, type, display_name, package, version, type_version, data)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- `).run(
- node.name,
- node.type,
- node.displayName,
- node.package,
- node.version,
- node.typeVersion,
- JSON.stringify(node)
- );
-
- // Try to insert doc for non-existent node (should fail)
- expect(() => {
- db.prepare(`
- INSERT INTO node_docs (node_name, content, examples)
- VALUES ('non-existent-node', 'content', '[]')
- `).run();
- }).toThrow(/FOREIGN KEY constraint failed/);
+ // Note: The current schema doesn't define foreign key constraints,
+ // but the setting is enabled for future use
});
});
});
\ No newline at end of file
diff --git a/tests/integration/database/fts5-search.test.ts b/tests/integration/database/fts5-search.test.ts
new file mode 100644
index 0000000..9b016e4
--- /dev/null
+++ b/tests/integration/database/fts5-search.test.ts
@@ -0,0 +1,639 @@
+import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
+import * as Database from 'better-sqlite3';
+import { TestDatabase, TestDataGenerator, PerformanceMonitor } from './test-utils';
+
+describe('FTS5 Full-Text Search', () => {
+ let testDb: TestDatabase;
+ let db: Database;
+
+ beforeEach(async () => {
+ testDb = new TestDatabase({ mode: 'memory', enableFTS5: true });
+ db = await testDb.initialize();
+ });
+
+ afterEach(async () => {
+ await testDb.cleanup();
+ });
+
+ describe('FTS5 Availability', () => {
+ it('should have FTS5 extension available', () => {
+ // Try to create an FTS5 table
+ expect(() => {
+ db.exec('CREATE VIRTUAL TABLE test_fts USING fts5(content)');
+ db.exec('DROP TABLE test_fts');
+ }).not.toThrow();
+ });
+
+ it('should support FTS5 for template searches', () => {
+ // Create FTS5 table for templates
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ // Verify it was created
+ const tables = db.prepare(`
+ SELECT sql FROM sqlite_master
+ WHERE type = 'table' AND name = 'templates_fts'
+ `).all() as { sql: string }[];
+
+ expect(tables).toHaveLength(1);
+ expect(tables[0].sql).toContain('USING fts5');
+ });
+ });
+
+ describe('Template FTS5 Operations', () => {
+ beforeEach(() => {
+ // Create FTS5 table
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ // Insert test templates
+ const templates = [
+ {
+ id: 1,
+ workflow_id: 1001,
+ name: 'Webhook to Slack Notification',
+ description: 'Send Slack messages when webhook is triggered',
+ nodes_used: JSON.stringify(['n8n-nodes-base.webhook', 'n8n-nodes-base.slack']),
+ workflow_json: JSON.stringify({}),
+ categories: JSON.stringify([{ id: 1, name: 'automation' }]),
+ views: 100
+ },
+ {
+ id: 2,
+ workflow_id: 1002,
+ name: 'HTTP Request Data Processing',
+ description: 'Fetch data from API and process it',
+ nodes_used: JSON.stringify(['n8n-nodes-base.httpRequest', 'n8n-nodes-base.set']),
+ workflow_json: JSON.stringify({}),
+ categories: JSON.stringify([{ id: 2, name: 'data' }]),
+ views: 200
+ },
+ {
+ id: 3,
+ workflow_id: 1003,
+ name: 'Email Automation Workflow',
+ description: 'Automate email sending based on triggers',
+ nodes_used: JSON.stringify(['n8n-nodes-base.emailSend', 'n8n-nodes-base.if']),
+ workflow_json: JSON.stringify({}),
+ categories: JSON.stringify([{ id: 3, name: 'communication' }]),
+ views: 150
+ }
+ ];
+
+ const stmt = db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
+ `);
+
+ templates.forEach(template => {
+ stmt.run(
+ template.id,
+ template.workflow_id,
+ template.name,
+ template.description,
+ template.nodes_used,
+ template.workflow_json,
+ template.categories,
+ template.views
+ );
+ });
+
+ // Populate FTS index
+ db.exec(`
+ INSERT INTO templates_fts(rowid, name, description)
+ SELECT id, name, description FROM templates
+ `);
+ });
+
+ it('should search templates by exact term', () => {
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'webhook'
+ ORDER BY rank
+ `).all();
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({
+ name: 'Webhook to Slack Notification'
+ });
+ });
+
+ it('should search with partial term and prefix', () => {
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'auto*'
+ ORDER BY rank
+ `).all();
+
+ expect(results.length).toBeGreaterThanOrEqual(1);
+ expect(results.some((r: any) => r.name.includes('Automation'))).toBe(true);
+ });
+
+ it('should search across multiple columns', () => {
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'email OR send'
+ ORDER BY rank
+ `).all();
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({
+ name: 'Email Automation Workflow'
+ });
+ });
+
+ it('should handle phrase searches', () => {
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH '"Slack messages"'
+ ORDER BY rank
+ `).all();
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({
+ name: 'Webhook to Slack Notification'
+ });
+ });
+
+ it('should support NOT queries', () => {
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'automation NOT email'
+ ORDER BY rank
+ `).all();
+
+ expect(results.length).toBeGreaterThan(0);
+ expect(results.every((r: any) => !r.name.toLowerCase().includes('email'))).toBe(true);
+ });
+ });
+
+ describe('FTS5 Ranking and Scoring', () => {
+ beforeEach(() => {
+ // Create FTS5 table
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ // Insert templates with varying relevance
+ const templates = [
+ {
+ id: 1,
+ name: 'Advanced HTTP Request Handler',
+ description: 'Complex HTTP request processing with error handling and retries'
+ },
+ {
+ id: 2,
+ name: 'Simple HTTP GET Request',
+ description: 'Basic HTTP GET request example'
+ },
+ {
+ id: 3,
+ name: 'Webhook HTTP Receiver',
+ description: 'Receive HTTP webhooks and process requests'
+ }
+ ];
+
+ const stmt = db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now'), datetime('now'))
+ `);
+
+ templates.forEach(t => {
+ stmt.run(t.id, 1000 + t.id, t.name, t.description);
+ });
+
+ // Populate FTS
+ db.exec(`
+ INSERT INTO templates_fts(rowid, name, description)
+ SELECT id, name, description FROM templates
+ `);
+ });
+
+ it('should rank results by relevance using bm25', () => {
+ const results = db.prepare(`
+ SELECT t.*, bm25(templates_fts) as score
+ FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'http request'
+ ORDER BY bm25(templates_fts)
+ `).all() as any[];
+
+ expect(results.length).toBeGreaterThan(0);
+
+ // Scores should be negative (lower is better in bm25)
+ expect(results[0].score).toBeLessThan(0);
+
+ // Should be ordered by relevance
+ expect(results[0].name).toContain('HTTP');
+ });
+
+ it('should use custom weights for columns', () => {
+ // Give more weight to name (2.0) than description (1.0)
+ const results = db.prepare(`
+ SELECT t.*, bm25(templates_fts, 2.0, 1.0) as score
+ FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'request'
+ ORDER BY bm25(templates_fts, 2.0, 1.0)
+ `).all() as any[];
+
+ expect(results.length).toBeGreaterThan(0);
+
+ // Items with "request" in name should rank higher
+ const nameMatches = results.filter((r: any) =>
+ r.name.toLowerCase().includes('request')
+ );
+ expect(nameMatches.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('FTS5 Advanced Features', () => {
+ beforeEach(() => {
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ // Insert template with longer description
+ db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now'), datetime('now'))
+ `).run(
+ 1,
+ 1001,
+ 'Complex Workflow',
+ 'This is a complex workflow that handles multiple operations including data transformation, filtering, and aggregation. It can process large datasets efficiently and includes error handling.'
+ );
+
+ db.exec(`
+ INSERT INTO templates_fts(rowid, name, description)
+ SELECT id, name, description FROM templates
+ `);
+ });
+
+ it('should support snippet extraction', () => {
+ const results = db.prepare(`
+ SELECT
+ t.*,
+ snippet(templates_fts, 1, '', '', '...', 10) as snippet
+ FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'transformation'
+ `).all() as any[];
+
+ expect(results).toHaveLength(1);
+ expect(results[0].snippet).toContain('transformation');
+ expect(results[0].snippet).toContain('...');
+ });
+
+ it('should support highlight function', () => {
+ const results = db.prepare(`
+ SELECT
+ t.*,
+ highlight(templates_fts, 1, '', '') as highlighted_desc
+ FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'workflow'
+ LIMIT 1
+ `).all() as any[];
+
+ expect(results).toHaveLength(1);
+ expect(results[0].highlighted_desc).toContain('workflow');
+ });
+ });
+
+ describe('FTS5 Triggers and Synchronization', () => {
+ beforeEach(() => {
+ // Create FTS5 table with triggers
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ );
+
+ 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;
+ `);
+ });
+
+ it('should automatically sync FTS on insert', () => {
+ const template = TestDataGenerator.generateTemplate({
+ id: 100,
+ name: 'Auto-synced Template',
+ description: 'This template is automatically indexed'
+ });
+
+ db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
+ `).run(
+ template.id,
+ template.id + 1000,
+ template.name,
+ template.description,
+ JSON.stringify(template.nodeTypes || []),
+ JSON.stringify({}),
+ JSON.stringify(template.categories || []),
+ template.totalViews || 0
+ );
+
+ // Should immediately be searchable
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'automatically'
+ `).all();
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({ id: 100 });
+ });
+
+ it('should automatically sync FTS on update', () => {
+ // Insert template
+ db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now'), datetime('now'))
+ `).run(200, 2000, 'Original Name', 'Original description');
+
+ // Update description
+ db.prepare(`
+ UPDATE templates
+ SET description = 'Updated description with new keywords'
+ WHERE id = ?
+ `).run(200);
+
+ // Should find with new keywords
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'keywords'
+ `).all();
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toMatchObject({ id: 200 });
+
+ // Should not find with old keywords
+ const oldResults = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH 'Original'
+ `).all();
+
+ expect(oldResults).toHaveLength(0);
+ });
+
+ it('should automatically sync FTS on delete', () => {
+ // Insert template
+ db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now'), datetime('now'))
+ `).run(300, 3000, 'Temporary Template', 'This will be deleted');
+
+ // Verify it's searchable
+ let count = db.prepare(`
+ SELECT COUNT(*) as count
+ FROM templates_fts
+ WHERE templates_fts MATCH 'Temporary'
+ `).get() as { count: number };
+ expect(count.count).toBe(1);
+
+ // Delete template
+ db.prepare('DELETE FROM templates WHERE id = ?').run(300);
+
+ // Should no longer be searchable
+ count = db.prepare(`
+ SELECT COUNT(*) as count
+ FROM templates_fts
+ WHERE templates_fts MATCH 'Temporary'
+ `).get() as { count: number };
+ expect(count.count).toBe(0);
+ });
+ });
+
+ describe('FTS5 Performance', () => {
+ it('should handle large dataset searches efficiently', () => {
+ // Create FTS5 table
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ const monitor = new PerformanceMonitor();
+
+ // Insert a large number of templates
+ const templates = TestDataGenerator.generateTemplates(1000);
+ const insertStmt = db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
+ `);
+
+ const insertMany = db.transaction((templates: any[]) => {
+ templates.forEach((template, i) => {
+ insertStmt.run(
+ i + 1,
+ template.id,
+ template.name,
+ template.description || `Template ${i} for ${['webhook handling', 'API calls', 'data processing', 'automation'][i % 4]}`,
+ JSON.stringify(template.nodeTypes || []),
+ JSON.stringify(template.workflowInfo || {}),
+ JSON.stringify(template.categories || []),
+ template.totalViews || 0
+ );
+ });
+
+ // Populate FTS in bulk
+ db.exec(`
+ INSERT INTO templates_fts(rowid, name, description)
+ SELECT id, name, description FROM templates
+ `);
+ });
+
+ const stopInsert = monitor.start('bulk_insert');
+ insertMany(templates);
+ stopInsert();
+
+ // Test search performance
+ const searchTerms = ['workflow', 'webhook', 'automation', 'data processing', 'api'];
+
+ searchTerms.forEach(term => {
+ const stop = monitor.start(`search_${term}`);
+ const results = db.prepare(`
+ SELECT t.* FROM templates t
+ JOIN templates_fts f ON t.id = f.rowid
+ WHERE templates_fts MATCH ?
+ ORDER BY rank
+ LIMIT 10
+ `).all(term);
+ stop();
+
+ expect(results.length).toBeGreaterThan(0);
+ });
+
+ // All searches should complete quickly
+ searchTerms.forEach(term => {
+ const stats = monitor.getStats(`search_${term}`);
+ expect(stats).not.toBeNull();
+ expect(stats!.average).toBeLessThan(10); // Should complete in under 10ms
+ });
+ });
+
+ it('should optimize rebuilding FTS index', () => {
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+
+ // Insert initial data
+ const templates = TestDataGenerator.generateTemplates(100);
+ const insertStmt = db.prepare(`
+ INSERT INTO templates (
+ id, workflow_id, name, description,
+ nodes_used, workflow_json, categories, views,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, '[]', '{}', '[]', 0, datetime('now'), datetime('now'))
+ `);
+
+ db.transaction(() => {
+ templates.forEach((template, i) => {
+ insertStmt.run(
+ i + 1,
+ template.id,
+ template.name,
+ template.description || 'Test template'
+ );
+ });
+
+ db.exec(`
+ INSERT INTO templates_fts(rowid, name, description)
+ SELECT id, name, description FROM templates
+ `);
+ })();
+
+ // Rebuild FTS index
+ const monitor = new PerformanceMonitor();
+ const stop = monitor.start('rebuild_fts');
+
+ db.exec('INSERT INTO templates_fts(templates_fts) VALUES("rebuild")');
+
+ stop();
+
+ const stats = monitor.getStats('rebuild_fts');
+ expect(stats).not.toBeNull();
+ expect(stats!.average).toBeLessThan(100); // Should complete quickly
+ });
+ });
+
+ describe('FTS5 Error Handling', () => {
+ beforeEach(() => {
+ db.exec(`
+ CREATE VIRTUAL TABLE IF NOT EXISTS templates_fts USING fts5(
+ name,
+ description,
+ content=templates,
+ content_rowid=id
+ )
+ `);
+ });
+
+ it('should handle malformed queries gracefully', () => {
+ expect(() => {
+ db.prepare(`
+ SELECT * FROM templates_fts WHERE templates_fts MATCH ?
+ `).all('AND OR NOT'); // Invalid query syntax
+ }).toThrow(/fts5: syntax error/);
+ });
+
+ it('should handle special characters in search terms', () => {
+ const specialChars = ['@', '#', '$', '%', '^', '&', '*', '(', ')'];
+
+ specialChars.forEach(char => {
+ // Should not throw when properly escaped
+ const results = db.prepare(`
+ SELECT * FROM templates_fts WHERE templates_fts MATCH ?
+ `).all(`"${char}"`);
+
+ expect(Array.isArray(results)).toBe(true);
+ });
+ });
+
+ it('should handle empty search terms', () => {
+ const results = db.prepare(`
+ SELECT * FROM templates_fts WHERE templates_fts MATCH ?
+ `).all('');
+
+ expect(results).toHaveLength(0);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/integration/database/node-repository.test.ts b/tests/integration/database/node-repository.test.ts
new file mode 100644
index 0000000..6eb0ca0
--- /dev/null
+++ b/tests/integration/database/node-repository.test.ts
@@ -0,0 +1,615 @@
+import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
+import * as Database from 'better-sqlite3';
+import { NodeRepository } from '../../../src/database/node-repository';
+import { DatabaseAdapter } from '../../../src/database/database-adapter';
+import { TestDatabase, TestDataGenerator, MOCK_NODES } from './test-utils';
+import { ParsedNode } from '../../../src/parsers/node-parser';
+
+describe('NodeRepository Integration Tests', () => {
+ let testDb: TestDatabase;
+ let db: Database;
+ let repository: NodeRepository;
+ let adapter: DatabaseAdapter;
+
+ beforeEach(async () => {
+ testDb = new TestDatabase({ mode: 'memory' });
+ db = await testDb.initialize();
+ adapter = new DatabaseAdapter(db);
+ repository = new NodeRepository(adapter);
+ });
+
+ afterEach(async () => {
+ await testDb.cleanup();
+ });
+
+ describe('saveNode', () => {
+ it('should save single node successfully', () => {
+ const node = createParsedNode(MOCK_NODES.webhook);
+ repository.saveNode(node);
+
+ const saved = repository.getNode(node.nodeType);
+ expect(saved).toBeTruthy();
+ expect(saved.nodeType).toBe(node.nodeType);
+ expect(saved.displayName).toBe(node.displayName);
+ });
+
+ it('should update existing nodes', () => {
+ const node = createParsedNode(MOCK_NODES.webhook);
+
+ // Save initial version
+ repository.saveNode(node);
+
+ // Update and save again
+ const updated = { ...node, displayName: 'Updated Webhook' };
+ repository.saveNode(updated);
+
+ const saved = repository.getNode(node.nodeType);
+ expect(saved?.displayName).toBe('Updated Webhook');
+
+ // Should not create duplicate
+ const count = repository.getNodeCount();
+ expect(count).toBe(1);
+ });
+
+ it('should handle nodes with complex properties', () => {
+ const complexNode: ParsedNode = {
+ nodeType: 'n8n-nodes-base.complex',
+ packageName: 'n8n-nodes-base',
+ displayName: 'Complex Node',
+ description: 'A complex node with many properties',
+ category: 'automation',
+ style: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '1',
+ documentation: 'Complex node documentation',
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ options: [
+ { name: 'User', value: 'user' },
+ { name: 'Post', value: 'post' }
+ ],
+ default: 'user'
+ },
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: ['user']
+ }
+ },
+ options: [
+ { name: 'Create', value: 'create' },
+ { name: 'Get', value: 'get' }
+ ]
+ }
+ ],
+ operations: [
+ { resource: 'user', operation: 'create' },
+ { resource: 'user', operation: 'get' }
+ ],
+ credentials: [
+ {
+ name: 'httpBasicAuth',
+ required: false
+ }
+ ]
+ };
+
+ repository.saveNode(complexNode);
+
+ const saved = repository.getNode(complexNode.nodeType);
+ expect(saved).toBeTruthy();
+ expect(saved.properties).toHaveLength(2);
+ expect(saved.credentials).toHaveLength(1);
+ expect(saved.operations).toHaveLength(2);
+ });
+
+ it('should handle very large nodes', () => {
+ const largeNode: ParsedNode = {
+ nodeType: 'n8n-nodes-base.large',
+ packageName: 'n8n-nodes-base',
+ displayName: 'Large Node',
+ description: 'A very large node',
+ category: 'automation',
+ style: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '1',
+ properties: Array.from({ length: 100 }, (_, i) => ({
+ displayName: `Property ${i}`,
+ name: `prop${i}`,
+ type: 'string',
+ default: ''
+ })),
+ operations: [],
+ credentials: []
+ };
+
+ repository.saveNode(largeNode);
+
+ const saved = repository.getNode(largeNode.nodeType);
+ expect(saved?.properties).toHaveLength(100);
+ });
+ });
+
+ describe('getNode', () => {
+ beforeEach(() => {
+ repository.saveNode(createParsedNode(MOCK_NODES.webhook));
+ repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
+ });
+
+ it('should retrieve node by type', () => {
+ const node = repository.getNode('n8n-nodes-base.webhook');
+ expect(node).toBeTruthy();
+ expect(node.displayName).toBe('Webhook');
+ expect(node.nodeType).toBe('n8n-nodes-base.webhook');
+ expect(node.package).toBe('n8n-nodes-base');
+ });
+
+ it('should return null for non-existent node', () => {
+ const node = repository.getNode('n8n-nodes-base.nonExistent');
+ expect(node).toBeNull();
+ });
+
+ it('should handle special characters in node types', () => {
+ const specialNode: ParsedNode = {
+ nodeType: 'n8n-nodes-base.special-chars_v2.node',
+ packageName: 'n8n-nodes-base',
+ displayName: 'Special Node',
+ description: 'Node with special characters',
+ category: 'automation',
+ style: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '2',
+ properties: [],
+ operations: [],
+ credentials: []
+ };
+
+ repository.saveNode(specialNode);
+ const retrieved = repository.getNode(specialNode.nodeType);
+ expect(retrieved).toBeTruthy();
+ });
+ });
+
+ describe('getAllNodes', () => {
+ it('should return empty array when no nodes', () => {
+ const nodes = repository.getAllNodes();
+ expect(nodes).toHaveLength(0);
+ });
+
+ it('should return all nodes with limit', () => {
+ const nodes = Array.from({ length: 20 }, (_, i) =>
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: `n8n-nodes-base.node${i}`,
+ displayName: `Node ${i}`
+ })
+ );
+
+ nodes.forEach(node => repository.saveNode(node));
+
+ const retrieved = repository.getAllNodes(10);
+ expect(retrieved).toHaveLength(10);
+ });
+
+ it('should return all nodes without limit', () => {
+ const nodes = Array.from({ length: 20 }, (_, i) =>
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: `n8n-nodes-base.node${i}`,
+ displayName: `Node ${i}`
+ })
+ );
+
+ nodes.forEach(node => repository.saveNode(node));
+
+ const retrieved = repository.getAllNodes();
+ expect(retrieved).toHaveLength(20);
+ });
+
+ it('should handle very large result sets efficiently', () => {
+ const nodes = Array.from({ length: 1000 }, (_, i) =>
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: `n8n-nodes-base.node${i}`,
+ displayName: `Node ${i}`
+ })
+ );
+
+ const insertMany = db.transaction((nodes: ParsedNode[]) => {
+ nodes.forEach(node => repository.saveNode(node));
+ });
+
+ const start = Date.now();
+ insertMany(nodes);
+ const duration = Date.now() - start;
+
+ expect(duration).toBeLessThan(1000); // Should complete in under 1 second
+
+ const retrieved = repository.getAllNodes();
+ expect(retrieved).toHaveLength(1000);
+ });
+ });
+
+ describe('getNodesByPackage', () => {
+ beforeEach(() => {
+ const nodes = [
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: 'n8n-nodes-base.node1',
+ packageName: 'n8n-nodes-base'
+ }),
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: 'n8n-nodes-base.node2',
+ packageName: 'n8n-nodes-base'
+ }),
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: '@n8n/n8n-nodes-langchain.node3',
+ packageName: '@n8n/n8n-nodes-langchain'
+ })
+ ];
+ nodes.forEach(node => repository.saveNode(node));
+ });
+
+ it('should filter nodes by package', () => {
+ const baseNodes = repository.getNodesByPackage('n8n-nodes-base');
+ expect(baseNodes).toHaveLength(2);
+
+ const langchainNodes = repository.getNodesByPackage('@n8n/n8n-nodes-langchain');
+ expect(langchainNodes).toHaveLength(1);
+ });
+
+ it('should return empty array for non-existent package', () => {
+ const nodes = repository.getNodesByPackage('non-existent-package');
+ expect(nodes).toHaveLength(0);
+ });
+ });
+
+ describe('getNodesByCategory', () => {
+ beforeEach(() => {
+ const nodes = [
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: 'n8n-nodes-base.webhook',
+ category: 'trigger'
+ }),
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: 'n8n-nodes-base.schedule',
+ displayName: 'Schedule',
+ category: 'trigger'
+ }),
+ createParsedNode({
+ ...MOCK_NODES.httpRequest,
+ nodeType: 'n8n-nodes-base.httpRequest',
+ category: 'automation'
+ })
+ ];
+ nodes.forEach(node => repository.saveNode(node));
+ });
+
+ it('should filter nodes by category', () => {
+ const triggers = repository.getNodesByCategory('trigger');
+ expect(triggers).toHaveLength(2);
+ expect(triggers.every(n => n.category === 'trigger')).toBe(true);
+
+ const automation = repository.getNodesByCategory('automation');
+ expect(automation).toHaveLength(1);
+ expect(automation[0].category).toBe('automation');
+ });
+ });
+
+ describe('searchNodes', () => {
+ beforeEach(() => {
+ const nodes = [
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ description: 'Starts the workflow when webhook is called'
+ }),
+ createParsedNode({
+ ...MOCK_NODES.httpRequest,
+ description: 'Makes HTTP requests to external APIs'
+ }),
+ createParsedNode({
+ nodeType: 'n8n-nodes-base.emailSend',
+ packageName: 'n8n-nodes-base',
+ displayName: 'Send Email',
+ description: 'Sends emails via SMTP protocol',
+ category: 'communication',
+ developmentStyle: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '1',
+ properties: [],
+ operations: [],
+ credentials: []
+ })
+ ];
+ nodes.forEach(node => repository.saveNode(node));
+ });
+
+ it('should search by node type', () => {
+ const results = repository.searchNodes('webhook');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
+ });
+
+ it('should search by display name', () => {
+ const results = repository.searchNodes('Send Email');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
+ });
+
+ it('should search by description', () => {
+ const results = repository.searchNodes('SMTP');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
+ });
+
+ it('should handle OR mode (default)', () => {
+ const results = repository.searchNodes('webhook email', 'OR');
+ expect(results).toHaveLength(2);
+ const nodeTypes = results.map(r => r.nodeType);
+ expect(nodeTypes).toContain('n8n-nodes-base.webhook');
+ expect(nodeTypes).toContain('n8n-nodes-base.emailSend');
+ });
+
+ it('should handle AND mode', () => {
+ const results = repository.searchNodes('HTTP request', 'AND');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
+ });
+
+ it('should handle FUZZY mode', () => {
+ const results = repository.searchNodes('HTT', 'FUZZY');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
+ });
+
+ it('should handle case-insensitive search', () => {
+ const results = repository.searchNodes('WEBHOOK');
+ expect(results).toHaveLength(1);
+ expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
+ });
+
+ it('should return empty array for no matches', () => {
+ const results = repository.searchNodes('nonexistent');
+ expect(results).toHaveLength(0);
+ });
+
+ it('should respect limit parameter', () => {
+ // Add more nodes
+ const nodes = Array.from({ length: 10 }, (_, i) =>
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: `n8n-nodes-base.test${i}`,
+ displayName: `Test Node ${i}`,
+ description: 'Test description'
+ })
+ );
+ nodes.forEach(node => repository.saveNode(node));
+
+ const results = repository.searchNodes('test', 'OR', 5);
+ expect(results).toHaveLength(5);
+ });
+ });
+
+ describe('getAITools', () => {
+ it('should return only AI tool nodes', () => {
+ const nodes = [
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: 'n8n-nodes-base.webhook',
+ isAITool: false
+ }),
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: '@n8n/n8n-nodes-langchain.agent',
+ displayName: 'AI Agent',
+ packageName: '@n8n/n8n-nodes-langchain',
+ isAITool: true
+ }),
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: '@n8n/n8n-nodes-langchain.tool',
+ displayName: 'AI Tool',
+ packageName: '@n8n/n8n-nodes-langchain',
+ isAITool: true
+ })
+ ];
+
+ nodes.forEach(node => repository.saveNode(node));
+
+ const aiTools = repository.getAITools();
+ expect(aiTools).toHaveLength(2);
+ expect(aiTools.every(node => node.package.includes('langchain'))).toBe(true);
+ expect(aiTools[0].displayName).toBe('AI Agent');
+ expect(aiTools[1].displayName).toBe('AI Tool');
+ });
+ });
+
+ describe('getNodeCount', () => {
+ it('should return correct node count', () => {
+ expect(repository.getNodeCount()).toBe(0);
+
+ repository.saveNode(createParsedNode(MOCK_NODES.webhook));
+ expect(repository.getNodeCount()).toBe(1);
+
+ repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
+ expect(repository.getNodeCount()).toBe(2);
+ });
+ });
+
+ describe('searchNodeProperties', () => {
+ beforeEach(() => {
+ const node: ParsedNode = {
+ nodeType: 'n8n-nodes-base.complex',
+ packageName: 'n8n-nodes-base',
+ displayName: 'Complex Node',
+ description: 'A complex node',
+ category: 'automation',
+ style: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '1',
+ properties: [
+ {
+ displayName: 'Authentication',
+ name: 'authentication',
+ type: 'options',
+ options: [
+ { name: 'Basic', value: 'basic' },
+ { name: 'OAuth2', value: 'oauth2' }
+ ]
+ },
+ {
+ displayName: 'Headers',
+ name: 'headers',
+ type: 'collection',
+ default: {},
+ options: [
+ {
+ displayName: 'Header',
+ name: 'header',
+ type: 'string'
+ }
+ ]
+ }
+ ],
+ operations: [],
+ credentials: []
+ };
+ repository.saveNode(node);
+ });
+
+ it('should find properties by name', () => {
+ const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'auth');
+ expect(results.length).toBeGreaterThan(0);
+ expect(results.some(r => r.path.includes('authentication'))).toBe(true);
+ });
+
+ it('should find nested properties', () => {
+ const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'header');
+ expect(results.length).toBeGreaterThan(0);
+ });
+
+ it('should return empty array for non-existent node', () => {
+ const results = repository.searchNodeProperties('non-existent', 'test');
+ expect(results).toHaveLength(0);
+ });
+ });
+
+ describe('Transaction handling', () => {
+ it('should handle errors gracefully', () => {
+ const invalidNode = {
+ nodeType: null, // This will cause an error
+ packageName: 'test',
+ displayName: 'Test'
+ } as any;
+
+ expect(() => {
+ repository.saveNode(invalidNode);
+ }).toThrow();
+
+ // Repository should still be functional
+ const count = repository.getNodeCount();
+ expect(count).toBe(0);
+ });
+
+ it('should handle concurrent saves', () => {
+ const node = createParsedNode(MOCK_NODES.webhook);
+
+ // Simulate concurrent saves of the same node with different display names
+ const promises = Array.from({ length: 10 }, (_, i) => {
+ const updatedNode = {
+ ...node,
+ displayName: `Display ${i}`
+ };
+ return Promise.resolve(repository.saveNode(updatedNode));
+ });
+
+ Promise.all(promises);
+
+ // Should have only one node
+ const count = repository.getNodeCount();
+ expect(count).toBe(1);
+
+ // Should have the last update
+ const saved = repository.getNode(node.nodeType);
+ expect(saved).toBeTruthy();
+ });
+ });
+
+ describe('Performance characteristics', () => {
+ it('should handle bulk operations efficiently', () => {
+ const nodeCount = 1000;
+ const nodes = Array.from({ length: nodeCount }, (_, i) =>
+ createParsedNode({
+ ...MOCK_NODES.webhook,
+ nodeType: `n8n-nodes-base.node${i}`,
+ displayName: `Node ${i}`,
+ description: `Description for node ${i}`
+ })
+ );
+
+ const insertMany = db.transaction((nodes: ParsedNode[]) => {
+ nodes.forEach(node => repository.saveNode(node));
+ });
+
+ const start = Date.now();
+ insertMany(nodes);
+ const saveDuration = Date.now() - start;
+
+ expect(saveDuration).toBeLessThan(1000); // Should complete in under 1 second
+
+ // Test search performance
+ const searchStart = Date.now();
+ const results = repository.searchNodes('node', 'OR', 100);
+ const searchDuration = Date.now() - searchStart;
+
+ expect(searchDuration).toBeLessThan(50); // Search should be fast
+ expect(results.length).toBe(100); // Respects limit
+ });
+ });
+});
+
+// Helper function to create ParsedNode from test data
+function createParsedNode(data: any): ParsedNode {
+ return {
+ nodeType: data.nodeType,
+ packageName: data.packageName,
+ displayName: data.displayName,
+ description: data.description || '',
+ category: data.category || 'automation',
+ style: data.developmentStyle || 'programmatic',
+ isAITool: data.isAITool || false,
+ isTrigger: data.isTrigger || false,
+ isWebhook: data.isWebhook || false,
+ isVersioned: data.isVersioned !== undefined ? data.isVersioned : true,
+ version: data.version || '1',
+ documentation: data.documentation || null,
+ properties: data.properties || [],
+ operations: data.operations || [],
+ credentials: data.credentials || []
+ };
+}
\ No newline at end of file
diff --git a/tests/integration/database/test-utils.ts b/tests/integration/database/test-utils.ts
index db9085c..cbd3281 100644
--- a/tests/integration/database/test-utils.ts
+++ b/tests/integration/database/test-utils.ts
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
-import Database from 'better-sqlite3';
+import * as Database from 'better-sqlite3';
import { execSync } from 'child_process';
export interface TestDatabaseOptions {
@@ -11,7 +11,7 @@ export interface TestDatabaseOptions {
}
export class TestDatabase {
- private db: Database.Database | null = null;
+ private db: Database | null = null;
private dbPath?: string;
private options: TestDatabaseOptions;
@@ -19,7 +19,7 @@ export class TestDatabase {
this.options = options;
}
- async initialize(): Promise {
+ async initialize(): Promise {
if (this.db) return this.db;
if (this.options.mode === 'file') {
@@ -28,9 +28,9 @@ export class TestDatabase {
fs.mkdirSync(testDir, { recursive: true });
}
this.dbPath = path.join(testDir, this.options.name || `test-${Date.now()}.db`);
- this.db = new Database(this.dbPath);
+ this.db = new (Database as any)(this.dbPath);
} else {
- this.db = new Database(':memory:');
+ this.db = new (Database as any)(':memory:');
}
// Enable WAL mode for file databases
@@ -72,7 +72,7 @@ export class TestDatabase {
}
}
- getDatabase(): Database.Database {
+ getDatabase(): Database {
if (!this.db) throw new Error('Database not initialized');
return this.db;
}
@@ -155,17 +155,23 @@ export class PerformanceMonitor {
// Data generation utilities
export class TestDataGenerator {
static generateNode(overrides: any = {}): any {
+ const nodeName = overrides.name || `testNode${Math.random().toString(36).substr(2, 9)}`;
return {
- name: `testNode${Math.random().toString(36).substr(2, 9)}`,
- displayName: 'Test Node',
- description: 'A test node for integration testing',
- version: 1,
- typeVersion: 1,
- type: 'n8n-nodes-base.testNode',
- package: 'n8n-nodes-base',
- category: ['automation'],
- properties: [],
- credentials: [],
+ nodeType: overrides.nodeType || `n8n-nodes-base.${nodeName}`,
+ packageName: overrides.packageName || overrides.package || 'n8n-nodes-base',
+ displayName: overrides.displayName || 'Test Node',
+ description: overrides.description || 'A test node for integration testing',
+ category: overrides.category || 'automation',
+ developmentStyle: overrides.developmentStyle || overrides.style || 'programmatic',
+ isAITool: overrides.isAITool || false,
+ isTrigger: overrides.isTrigger || false,
+ isWebhook: overrides.isWebhook || false,
+ isVersioned: overrides.isVersioned !== undefined ? overrides.isVersioned : true,
+ version: overrides.version || '1',
+ documentation: overrides.documentation || null,
+ properties: overrides.properties || [],
+ operations: overrides.operations || [],
+ credentials: overrides.credentials || [],
...overrides
};
}
@@ -176,7 +182,7 @@ export class TestDataGenerator {
...template,
name: `testNode${i}`,
displayName: `Test Node ${i}`,
- type: `n8n-nodes-base.testNode${i}`
+ nodeType: `n8n-nodes-base.testNode${i}`
})
);
}
@@ -204,7 +210,7 @@ export class TestDataGenerator {
// Transaction test utilities
export async function runInTransaction(
- db: Database.Database,
+ db: Database,
fn: () => T
): Promise {
db.exec('BEGIN');
@@ -260,7 +266,7 @@ export async function simulateConcurrentAccess(
}
// Database integrity check
-export function checkDatabaseIntegrity(db: Database.Database): {
+export function checkDatabaseIntegrity(db: Database): {
isValid: boolean;
errors: string[];
} {
@@ -279,14 +285,14 @@ export function checkDatabaseIntegrity(db: Database.Database): {
errors.push(`Foreign key violations: ${JSON.stringify(fkResult)}`);
}
- // Check for orphaned records
- const orphanedDocs = db.prepare(`
- SELECT COUNT(*) as count FROM node_docs
- WHERE node_name NOT IN (SELECT name FROM nodes)
- `).get() as { count: number };
+ // Check table existence
+ const tables = db.prepare(`
+ SELECT name FROM sqlite_master
+ WHERE type = 'table' AND name = 'nodes'
+ `).all();
- if (orphanedDocs.count > 0) {
- errors.push(`Found ${orphanedDocs.count} orphaned documentation records`);
+ if (tables.length === 0) {
+ errors.push('nodes table does not exist');
}
} catch (error: any) {
@@ -302,13 +308,18 @@ export function checkDatabaseIntegrity(db: Database.Database): {
// Mock data for testing
export const MOCK_NODES = {
webhook: {
- name: 'webhook',
+ nodeType: 'n8n-nodes-base.webhook',
+ packageName: 'n8n-nodes-base',
displayName: 'Webhook',
- type: 'n8n-nodes-base.webhook',
- typeVersion: 1,
description: 'Starts the workflow when a webhook is called',
- category: ['trigger'],
- package: 'n8n-nodes-base',
+ category: 'trigger',
+ developmentStyle: 'programmatic',
+ isAITool: false,
+ isTrigger: true,
+ isWebhook: true,
+ isVersioned: true,
+ version: '1',
+ documentation: 'Webhook documentation',
properties: [
{
displayName: 'HTTP Method',
@@ -320,16 +331,23 @@ export const MOCK_NODES = {
],
default: 'GET'
}
- ]
+ ],
+ operations: [],
+ credentials: []
},
httpRequest: {
- name: 'httpRequest',
+ nodeType: 'n8n-nodes-base.httpRequest',
+ packageName: 'n8n-nodes-base',
displayName: 'HTTP Request',
- type: 'n8n-nodes-base.httpRequest',
- typeVersion: 1,
description: 'Makes an HTTP request and returns the response',
- category: ['automation'],
- package: 'n8n-nodes-base',
+ category: 'automation',
+ developmentStyle: 'programmatic',
+ isAITool: false,
+ isTrigger: false,
+ isWebhook: false,
+ isVersioned: true,
+ version: '1',
+ documentation: 'HTTP Request documentation',
properties: [
{
displayName: 'URL',
@@ -338,6 +356,8 @@ export const MOCK_NODES = {
required: true,
default: ''
}
- ]
+ ],
+ operations: [],
+ credentials: []
}
};
\ No newline at end of file
diff --git a/tests/integration/database/transactions.test.ts b/tests/integration/database/transactions.test.ts
index 7aa1e37..8b90bdb 100644
--- a/tests/integration/database/transactions.test.ts
+++ b/tests/integration/database/transactions.test.ts
@@ -112,8 +112,12 @@ describe('Database Transactions', () => {
// Insert first node
const insertStmt = db.prepare(`
- INSERT INTO nodes (name, type, display_name, package, version, type_version, data)
- VALUES (?, ?, ?, ?, ?, ?, ?)
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertStmt.run(
@@ -462,8 +466,12 @@ describe('Database Transactions', () => {
// Insert initial data
const nodes = TestDataGenerator.generateNodes(2);
const insertStmt = db.prepare(`
- INSERT INTO nodes (name, type, display_name, package, version, type_version, data)
- VALUES (?, ?, ?, ?, ?, ?, ?)
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
nodes.forEach(node => {