Skip to main content
Glama

n8n-MCP

by 88-888
node-repository-operations.test.tsโ€ข20.1 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { NodeRepository } from '@/database/node-repository'; import { DatabaseAdapter, PreparedStatement, RunResult } from '@/database/database-adapter'; // Mock DatabaseAdapter for testing the new operation methods class MockDatabaseAdapter implements DatabaseAdapter { private statements = new Map<string, MockPreparedStatement>(); private mockNodes = new Map<string, any>(); prepare = vi.fn((sql: string) => { if (!this.statements.has(sql)) { this.statements.set(sql, new MockPreparedStatement(sql, this.mockNodes)); } return this.statements.get(sql)!; }); exec = vi.fn(); close = vi.fn(); pragma = vi.fn(); transaction = vi.fn((fn: () => any) => fn()); checkFTS5Support = vi.fn(() => true); inTransaction = false; // Test helper to set mock data _setMockNode(nodeType: string, value: any) { this.mockNodes.set(nodeType, value); } } class MockPreparedStatement implements PreparedStatement { run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 })); get = vi.fn(); all = vi.fn(() => []); iterate = vi.fn(); pluck = vi.fn(() => this); expand = vi.fn(() => this); raw = vi.fn(() => this); columns = vi.fn(() => []); bind = vi.fn(() => this); constructor(private sql: string, private mockNodes: Map<string, any>) { // Configure get() to return node data if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) { this.get = vi.fn((nodeType: string) => this.mockNodes.get(nodeType)); } // Configure all() for getAllNodes if (sql.includes('SELECT * FROM nodes ORDER BY display_name')) { this.all = vi.fn(() => Array.from(this.mockNodes.values())); } } } describe('NodeRepository - Operations and Resources', () => { let repository: NodeRepository; let mockAdapter: MockDatabaseAdapter; beforeEach(() => { mockAdapter = new MockDatabaseAdapter(); repository = new NodeRepository(mockAdapter); }); describe('getNodeOperations', () => { it('should extract operations from array format', () => { const mockNode = { node_type: 'nodes-base.httpRequest', display_name: 'HTTP Request', operations: JSON.stringify([ { name: 'get', displayName: 'GET' }, { name: 'post', displayName: 'POST' } ]), properties_schema: '[]', credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.httpRequest', mockNode); const operations = repository.getNodeOperations('nodes-base.httpRequest'); expect(operations).toEqual([ { name: 'get', displayName: 'GET' }, { name: 'post', displayName: 'POST' } ]); }); it('should extract operations from object format grouped by resource', () => { const mockNode = { node_type: 'nodes-base.slack', display_name: 'Slack', operations: JSON.stringify({ message: [ { name: 'send', displayName: 'Send Message' }, { name: 'update', displayName: 'Update Message' } ], channel: [ { name: 'create', displayName: 'Create Channel' }, { name: 'archive', displayName: 'Archive Channel' } ] }), properties_schema: '[]', credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.slack', mockNode); const allOperations = repository.getNodeOperations('nodes-base.slack'); const messageOperations = repository.getNodeOperations('nodes-base.slack', 'message'); expect(allOperations).toHaveLength(4); expect(messageOperations).toEqual([ { name: 'send', displayName: 'Send Message' }, { name: 'update', displayName: 'Update Message' } ]); }); it('should extract operations from properties with operation field', () => { const mockNode = { node_type: 'nodes-base.googleSheets', display_name: 'Google Sheets', operations: '[]', properties_schema: JSON.stringify([ { name: 'resource', type: 'options', options: [{ name: 'sheet', displayName: 'Sheet' }] }, { name: 'operation', type: 'options', displayOptions: { show: { resource: ['sheet'] } }, options: [ { name: 'append', displayName: 'Append Row' }, { name: 'read', displayName: 'Read Rows' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.googleSheets', mockNode); const operations = repository.getNodeOperations('nodes-base.googleSheets'); expect(operations).toEqual([ { name: 'append', displayName: 'Append Row' }, { name: 'read', displayName: 'Read Rows' } ]); }); it('should filter operations by resource when specified', () => { const mockNode = { node_type: 'nodes-base.googleSheets', display_name: 'Google Sheets', operations: '[]', properties_schema: JSON.stringify([ { name: 'operation', type: 'options', displayOptions: { show: { resource: ['sheet'] } }, options: [ { name: 'append', displayName: 'Append Row' } ] }, { name: 'operation', type: 'options', displayOptions: { show: { resource: ['cell'] } }, options: [ { name: 'update', displayName: 'Update Cell' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.googleSheets', mockNode); const sheetOperations = repository.getNodeOperations('nodes-base.googleSheets', 'sheet'); const cellOperations = repository.getNodeOperations('nodes-base.googleSheets', 'cell'); expect(sheetOperations).toEqual([{ name: 'append', displayName: 'Append Row' }]); expect(cellOperations).toEqual([{ name: 'update', displayName: 'Update Cell' }]); }); it('should return empty array for non-existent node', () => { const operations = repository.getNodeOperations('nodes-base.nonexistent'); expect(operations).toEqual([]); }); it('should handle nodes without operations', () => { const mockNode = { node_type: 'nodes-base.simple', display_name: 'Simple Node', operations: '[]', properties_schema: '[]', credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.simple', mockNode); const operations = repository.getNodeOperations('nodes-base.simple'); expect(operations).toEqual([]); }); it('should handle malformed operations JSON gracefully', () => { const mockNode = { node_type: 'nodes-base.broken', display_name: 'Broken Node', operations: '{invalid json}', properties_schema: '[]', credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.broken', mockNode); const operations = repository.getNodeOperations('nodes-base.broken'); expect(operations).toEqual([]); }); }); describe('getNodeResources', () => { it('should extract resources from properties', () => { const mockNode = { node_type: 'nodes-base.slack', display_name: 'Slack', operations: '[]', properties_schema: JSON.stringify([ { name: 'resource', type: 'options', options: [ { name: 'message', displayName: 'Message' }, { name: 'channel', displayName: 'Channel' }, { name: 'user', displayName: 'User' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.slack', mockNode); const resources = repository.getNodeResources('nodes-base.slack'); expect(resources).toEqual([ { name: 'message', displayName: 'Message' }, { name: 'channel', displayName: 'Channel' }, { name: 'user', displayName: 'User' } ]); }); it('should return empty array for node without resources', () => { const mockNode = { node_type: 'nodes-base.simple', display_name: 'Simple Node', operations: '[]', properties_schema: JSON.stringify([ { name: 'url', type: 'string' } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.simple', mockNode); const resources = repository.getNodeResources('nodes-base.simple'); expect(resources).toEqual([]); }); it('should return empty array for non-existent node', () => { const resources = repository.getNodeResources('nodes-base.nonexistent'); expect(resources).toEqual([]); }); it('should handle multiple resource properties', () => { const mockNode = { node_type: 'nodes-base.multi', display_name: 'Multi Resource Node', operations: '[]', properties_schema: JSON.stringify([ { name: 'resource', type: 'options', options: [{ name: 'type1', displayName: 'Type 1' }] }, { name: 'resource', type: 'options', options: [{ name: 'type2', displayName: 'Type 2' }] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.multi', mockNode); const resources = repository.getNodeResources('nodes-base.multi'); expect(resources).toEqual([ { name: 'type1', displayName: 'Type 1' }, { name: 'type2', displayName: 'Type 2' } ]); }); }); describe('getOperationsForResource', () => { it('should return operations for specific resource', () => { const mockNode = { node_type: 'nodes-base.slack', display_name: 'Slack', operations: '[]', properties_schema: JSON.stringify([ { name: 'operation', type: 'options', displayOptions: { show: { resource: ['message'] } }, options: [ { name: 'send', displayName: 'Send Message' }, { name: 'update', displayName: 'Update Message' } ] }, { name: 'operation', type: 'options', displayOptions: { show: { resource: ['channel'] } }, options: [ { name: 'create', displayName: 'Create Channel' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.slack', mockNode); const messageOps = repository.getOperationsForResource('nodes-base.slack', 'message'); const channelOps = repository.getOperationsForResource('nodes-base.slack', 'channel'); const nonExistentOps = repository.getOperationsForResource('nodes-base.slack', 'nonexistent'); expect(messageOps).toEqual([ { name: 'send', displayName: 'Send Message' }, { name: 'update', displayName: 'Update Message' } ]); expect(channelOps).toEqual([ { name: 'create', displayName: 'Create Channel' } ]); expect(nonExistentOps).toEqual([]); }); it('should handle array format for resource display options', () => { const mockNode = { node_type: 'nodes-base.multi', display_name: 'Multi Node', operations: '[]', properties_schema: JSON.stringify([ { name: 'operation', type: 'options', displayOptions: { show: { resource: ['message', 'channel'] // Array format } }, options: [ { name: 'list', displayName: 'List Items' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.multi', mockNode); const messageOps = repository.getOperationsForResource('nodes-base.multi', 'message'); const channelOps = repository.getOperationsForResource('nodes-base.multi', 'channel'); const otherOps = repository.getOperationsForResource('nodes-base.multi', 'other'); expect(messageOps).toEqual([{ name: 'list', displayName: 'List Items' }]); expect(channelOps).toEqual([{ name: 'list', displayName: 'List Items' }]); expect(otherOps).toEqual([]); }); it('should return empty array for non-existent node', () => { const operations = repository.getOperationsForResource('nodes-base.nonexistent', 'message'); expect(operations).toEqual([]); }); it('should handle string format for single resource', () => { const mockNode = { node_type: 'nodes-base.single', display_name: 'Single Node', operations: '[]', properties_schema: JSON.stringify([ { name: 'operation', type: 'options', displayOptions: { show: { resource: 'document' // String format } }, options: [ { name: 'create', displayName: 'Create Document' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.single', mockNode); const operations = repository.getOperationsForResource('nodes-base.single', 'document'); expect(operations).toEqual([{ name: 'create', displayName: 'Create Document' }]); }); }); describe('getAllOperations', () => { it('should collect operations from all nodes', () => { const mockNodes = [ { node_type: 'nodes-base.httpRequest', display_name: 'HTTP Request', operations: JSON.stringify([{ name: 'execute' }]), properties_schema: '[]', credentials_required: '[]' }, { node_type: 'nodes-base.slack', display_name: 'Slack', operations: JSON.stringify([{ name: 'send' }]), properties_schema: '[]', credentials_required: '[]' }, { node_type: 'nodes-base.empty', display_name: 'Empty Node', operations: '[]', properties_schema: '[]', credentials_required: '[]' } ]; mockNodes.forEach(node => { mockAdapter._setMockNode(node.node_type, node); }); const allOperations = repository.getAllOperations(); expect(allOperations.size).toBe(2); // Only nodes with operations expect(allOperations.get('nodes-base.httpRequest')).toEqual([{ name: 'execute' }]); expect(allOperations.get('nodes-base.slack')).toEqual([{ name: 'send' }]); expect(allOperations.has('nodes-base.empty')).toBe(false); }); it('should handle empty node list', () => { const allOperations = repository.getAllOperations(); expect(allOperations.size).toBe(0); }); }); describe('getAllResources', () => { it('should collect resources from all nodes', () => { const mockNodes = [ { node_type: 'nodes-base.slack', display_name: 'Slack', operations: '[]', properties_schema: JSON.stringify([ { name: 'resource', options: [{ name: 'message' }, { name: 'channel' }] } ]), credentials_required: '[]' }, { node_type: 'nodes-base.sheets', display_name: 'Google Sheets', operations: '[]', properties_schema: JSON.stringify([ { name: 'resource', options: [{ name: 'sheet' }] } ]), credentials_required: '[]' }, { node_type: 'nodes-base.simple', display_name: 'Simple Node', operations: '[]', properties_schema: '[]', // No resources credentials_required: '[]' } ]; mockNodes.forEach(node => { mockAdapter._setMockNode(node.node_type, node); }); const allResources = repository.getAllResources(); expect(allResources.size).toBe(2); // Only nodes with resources expect(allResources.get('nodes-base.slack')).toEqual([ { name: 'message' }, { name: 'channel' } ]); expect(allResources.get('nodes-base.sheets')).toEqual([{ name: 'sheet' }]); expect(allResources.has('nodes-base.simple')).toBe(false); }); it('should handle empty node list', () => { const allResources = repository.getAllResources(); expect(allResources.size).toBe(0); }); }); describe('edge cases and error handling', () => { it('should handle null or undefined properties gracefully', () => { const mockNode = { node_type: 'nodes-base.null', display_name: 'Null Node', operations: null, properties_schema: null, credentials_required: null }; mockAdapter._setMockNode('nodes-base.null', mockNode); const operations = repository.getNodeOperations('nodes-base.null'); const resources = repository.getNodeResources('nodes-base.null'); expect(operations).toEqual([]); expect(resources).toEqual([]); }); it('should handle complex nested operation properties', () => { const mockNode = { node_type: 'nodes-base.complex', display_name: 'Complex Node', operations: '[]', properties_schema: JSON.stringify([ { name: 'operation', type: 'options', displayOptions: { show: { resource: ['message'], mode: ['advanced'] } }, options: [ { name: 'complexOperation', displayName: 'Complex Operation' } ] } ]), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.complex', mockNode); const operations = repository.getNodeOperations('nodes-base.complex'); expect(operations).toEqual([{ name: 'complexOperation', displayName: 'Complex Operation' }]); }); it('should handle operations with mixed data types', () => { const mockNode = { node_type: 'nodes-base.mixed', display_name: 'Mixed Node', operations: JSON.stringify({ string_operation: 'invalid', // Should be array valid_operations: [{ name: 'valid' }], nested_object: { inner: [{ name: 'nested' }] } }), properties_schema: '[]', credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.mixed', mockNode); const operations = repository.getNodeOperations('nodes-base.mixed'); expect(operations).toEqual([{ name: 'valid' }]); // Only valid array operations }); it('should handle very deeply nested properties', () => { const deepProperties = [ { name: 'resource', options: [{ name: 'deep', displayName: 'Deep Resource' }], nested: { level1: { level2: { operations: [{ name: 'deep_operation' }] } } } } ]; const mockNode = { node_type: 'nodes-base.deep', display_name: 'Deep Node', operations: '[]', properties_schema: JSON.stringify(deepProperties), credentials_required: '[]' }; mockAdapter._setMockNode('nodes-base.deep', mockNode); const resources = repository.getNodeResources('nodes-base.deep'); expect(resources).toEqual([{ name: 'deep', displayName: 'Deep Resource' }]); }); }); });

MCP directory API

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

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

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