mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-27 12:43:12 +00:00
* chore: update n8n to 2.13.3 and bump version to 2.41.0 - Updated n8n from 2.12.3 to 2.13.3 - Updated n8n-core from 2.12.0 to 2.13.1 - Updated n8n-workflow from 2.12.0 to 2.13.1 - Updated @n8n/n8n-nodes-langchain from 2.12.0 to 2.13.1 - Rebuilt node database with 1,396 nodes (812 core + 584 community: 516 verified + 68 npm) - Refreshed community nodes with 581 AI-generated documentation summaries - Improved documentation generator: strip <think> tags, raw fetch for vLLM chat_template_kwargs - Incremental community updates: saveNode uses ON CONFLICT DO UPDATE preserving READMEs/AI summaries - fetch:community now upserts by default (use --rebuild for clean slate) - Updated README badge and node counts - Updated CHANGELOG and MEMORY_N8N_UPDATE.md Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * chore: update MCP SDK from 1.27.1 to 1.28.0 - Pinned @modelcontextprotocol/sdk to 1.28.0 (was ^1.27.1) - Updated CI dependency check to expect 1.28.0 - SDK 1.28.0 includes: loopback port relaxation, inputSchema fix, timeout cleanup fix, OAuth scope improvements - All 15 MCP tool tests pass with no regressions Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * fix: update test assertions for ON CONFLICT saveNode SQL Tests expected old INSERT OR REPLACE SQL, updated to match new INSERT INTO ... ON CONFLICT(node_type) DO UPDATE SET pattern. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * chore: remove documentation generator tests These tests mocked the OpenAI SDK which was replaced with raw fetch. Documentation generation is a local LLM utility, not core functionality. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * fix: relax SQL assertion in outputs test to match ON CONFLICT pattern Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * fix: use INSERT OR REPLACE with docs preservation instead of ON CONFLICT ON CONFLICT DO UPDATE caused FTS5 trigger conflicts ("database disk image is malformed") in CI. Reverted to INSERT OR REPLACE but now reads existing npm_readme/ai_documentation_summary/ai_summary_generated_at before saving and carries them through the replace. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * fix: update saveNode test mocks for docs preservation pattern Tests now account for the SELECT query that reads existing docs before INSERT OR REPLACE, and the 3 extra params (npm_readme, ai_documentation_summary, ai_summary_generated_at). Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * fix: update community integration test mock for INSERT OR REPLACE The mock SQL matching used 'INSERT INTO nodes' which doesn't match 'INSERT OR REPLACE INTO nodes'. Also added handler for the new SELECT npm_readme query in saveNode. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
705 lines
21 KiB
TypeScript
705 lines
21 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { NodeRepository } from '@/database/node-repository';
|
|
import { DatabaseAdapter } from '@/database/database-adapter';
|
|
import { ParsedNode } from '@/parsers/node-parser';
|
|
|
|
describe('NodeRepository - Outputs Handling', () => {
|
|
let repository: NodeRepository;
|
|
let mockDb: DatabaseAdapter;
|
|
let mockStatement: any;
|
|
|
|
beforeEach(() => {
|
|
mockStatement = {
|
|
run: vi.fn(),
|
|
get: vi.fn(),
|
|
all: vi.fn()
|
|
};
|
|
|
|
// saveNode now calls prepare twice: first a SELECT (returns get), then INSERT (returns run).
|
|
// We create a separate mock for the SELECT statement that returns undefined (no existing row).
|
|
const selectStatement = {
|
|
run: vi.fn(),
|
|
get: vi.fn().mockReturnValue(undefined),
|
|
all: vi.fn()
|
|
};
|
|
|
|
mockDb = {
|
|
prepare: vi.fn((sql: string) => {
|
|
if (sql.includes('SELECT npm_readme')) {
|
|
return selectStatement;
|
|
}
|
|
return mockStatement;
|
|
}),
|
|
transaction: vi.fn(),
|
|
exec: vi.fn(),
|
|
close: vi.fn(),
|
|
pragma: vi.fn()
|
|
} as any;
|
|
|
|
repository = new NodeRepository(mockDb);
|
|
});
|
|
|
|
describe('saveNode with outputs', () => {
|
|
it('should save node with outputs and outputNames correctly', () => {
|
|
const outputs = [
|
|
{ displayName: 'Done', description: 'Final results when loop completes' },
|
|
{ displayName: 'Loop', description: 'Current batch data during iteration' }
|
|
];
|
|
const outputNames = ['done', 'loop'];
|
|
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.splitInBatches',
|
|
displayName: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '3',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base',
|
|
outputs,
|
|
outputNames
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
expect(mockDb.prepare).toHaveBeenCalledWith(
|
|
expect.stringContaining('INSERT OR REPLACE INTO nodes')
|
|
);
|
|
|
|
expect(mockStatement.run).toHaveBeenCalledWith(
|
|
'nodes-base.splitInBatches',
|
|
'n8n-nodes-base',
|
|
'Split In Batches',
|
|
'Split data into batches',
|
|
'transform',
|
|
'programmatic',
|
|
0, // isAITool
|
|
0, // isTrigger
|
|
0, // isWebhook
|
|
0, // isVersioned
|
|
0, // isToolVariant
|
|
null, // toolVariantOf
|
|
0, // hasToolVariant
|
|
'3', // version
|
|
null, // documentation
|
|
JSON.stringify([], null, 2), // properties
|
|
JSON.stringify([], null, 2), // operations
|
|
JSON.stringify([], null, 2), // credentials
|
|
JSON.stringify(outputs, null, 2), // outputs
|
|
JSON.stringify(outputNames, null, 2), // output_names
|
|
0, // is_community
|
|
0, // is_verified
|
|
null, // author_name
|
|
null, // author_github_url
|
|
null, // npm_package_name
|
|
null, // npm_version
|
|
0, // npm_downloads
|
|
null, // community_fetched_at
|
|
null, // npm_readme (preserved from existing)
|
|
null, // ai_documentation_summary (preserved from existing)
|
|
null // ai_summary_generated_at (preserved from existing)
|
|
);
|
|
});
|
|
|
|
it('should save node with only outputs (no outputNames)', () => {
|
|
const outputs = [
|
|
{ displayName: 'True', description: 'Items that match condition' },
|
|
{ displayName: 'False', description: 'Items that do not match condition' }
|
|
];
|
|
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.if',
|
|
displayName: 'IF',
|
|
description: 'Route items based on conditions',
|
|
category: 'transform',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '2',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base',
|
|
outputs
|
|
// no outputNames
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
const callArgs = mockStatement.run.mock.calls[0];
|
|
expect(callArgs[18]).toBe(JSON.stringify(outputs, null, 2)); // outputs
|
|
expect(callArgs[19]).toBe(null); // output_names should be null
|
|
});
|
|
|
|
it('should save node with only outputNames (no outputs)', () => {
|
|
const outputNames = ['main', 'error'];
|
|
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.customNode',
|
|
displayName: 'Custom Node',
|
|
description: 'Custom node with output names only',
|
|
category: 'transform',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '1',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base',
|
|
outputNames
|
|
// no outputs
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
const callArgs = mockStatement.run.mock.calls[0];
|
|
expect(callArgs[18]).toBe(null); // outputs should be null
|
|
expect(callArgs[19]).toBe(JSON.stringify(outputNames, null, 2)); // output_names
|
|
});
|
|
|
|
it('should save node without outputs or outputNames', () => {
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.httpRequest',
|
|
displayName: 'HTTP Request',
|
|
description: 'Make HTTP requests',
|
|
category: 'input',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '4',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base'
|
|
// no outputs or outputNames
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
const callArgs = mockStatement.run.mock.calls[0];
|
|
expect(callArgs[18]).toBe(null); // outputs should be null
|
|
expect(callArgs[19]).toBe(null); // output_names should be null
|
|
});
|
|
|
|
it('should handle empty outputs and outputNames arrays', () => {
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.emptyNode',
|
|
displayName: 'Empty Node',
|
|
description: 'Node with empty outputs',
|
|
category: 'misc',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '1',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base',
|
|
outputs: [],
|
|
outputNames: []
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
const callArgs = mockStatement.run.mock.calls[0];
|
|
expect(callArgs[18]).toBe(JSON.stringify([], null, 2)); // outputs
|
|
expect(callArgs[19]).toBe(JSON.stringify([], null, 2)); // output_names
|
|
});
|
|
});
|
|
|
|
describe('getNode with outputs', () => {
|
|
it('should retrieve node with outputs and outputNames correctly', () => {
|
|
const outputs = [
|
|
{ displayName: 'Done', description: 'Final results when loop completes' },
|
|
{ displayName: 'Loop', description: 'Current batch data during iteration' }
|
|
];
|
|
const outputNames = ['done', 'loop'];
|
|
|
|
const mockRow = {
|
|
node_type: 'nodes-base.splitInBatches',
|
|
display_name: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '3',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: JSON.stringify(outputs),
|
|
output_names: JSON.stringify(outputNames),
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null,
|
|
npm_readme: null,
|
|
ai_documentation_summary: null,
|
|
ai_summary_generated_at: null
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.splitInBatches');
|
|
|
|
expect(result).toEqual({
|
|
nodeType: 'nodes-base.splitInBatches',
|
|
displayName: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
developmentStyle: 'programmatic',
|
|
package: 'n8n-nodes-base',
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
isVersioned: false,
|
|
isToolVariant: false,
|
|
toolVariantOf: null,
|
|
hasToolVariant: false,
|
|
version: '3',
|
|
properties: [],
|
|
operations: [],
|
|
credentials: [],
|
|
hasDocumentation: false,
|
|
outputs,
|
|
outputNames,
|
|
isCommunity: false,
|
|
isVerified: false,
|
|
authorName: null,
|
|
authorGithubUrl: null,
|
|
npmPackageName: null,
|
|
npmVersion: null,
|
|
npmDownloads: 0,
|
|
communityFetchedAt: null,
|
|
npmReadme: null,
|
|
aiDocumentationSummary: null,
|
|
aiSummaryGeneratedAt: null
|
|
});
|
|
});
|
|
|
|
it('should retrieve node with only outputs (null outputNames)', () => {
|
|
const outputs = [
|
|
{ displayName: 'True', description: 'Items that match condition' }
|
|
];
|
|
|
|
const mockRow = {
|
|
node_type: 'nodes-base.if',
|
|
display_name: 'IF',
|
|
description: 'Route items',
|
|
category: 'transform',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '2',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: JSON.stringify(outputs),
|
|
output_names: null,
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.if');
|
|
|
|
expect(result.outputs).toEqual(outputs);
|
|
expect(result.outputNames).toBe(null);
|
|
});
|
|
|
|
it('should retrieve node with only outputNames (null outputs)', () => {
|
|
const outputNames = ['main'];
|
|
|
|
const mockRow = {
|
|
node_type: 'nodes-base.customNode',
|
|
display_name: 'Custom Node',
|
|
description: 'Custom node',
|
|
category: 'misc',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '1',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: null,
|
|
output_names: JSON.stringify(outputNames),
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.customNode');
|
|
|
|
expect(result.outputs).toBe(null);
|
|
expect(result.outputNames).toEqual(outputNames);
|
|
});
|
|
|
|
it('should retrieve node without outputs or outputNames', () => {
|
|
const mockRow = {
|
|
node_type: 'nodes-base.httpRequest',
|
|
display_name: 'HTTP Request',
|
|
description: 'Make HTTP requests',
|
|
category: 'input',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '4',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: null,
|
|
output_names: null,
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.httpRequest');
|
|
|
|
expect(result.outputs).toBe(null);
|
|
expect(result.outputNames).toBe(null);
|
|
});
|
|
|
|
it('should handle malformed JSON gracefully', () => {
|
|
const mockRow = {
|
|
node_type: 'nodes-base.malformed',
|
|
display_name: 'Malformed Node',
|
|
description: 'Node with malformed JSON',
|
|
category: 'misc',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '1',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: '{invalid json}',
|
|
output_names: '[invalid, json',
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.malformed');
|
|
|
|
// Should use default values when JSON parsing fails
|
|
expect(result.outputs).toBe(null);
|
|
expect(result.outputNames).toBe(null);
|
|
});
|
|
|
|
it('should return null for non-existent node', () => {
|
|
mockStatement.get.mockReturnValue(null);
|
|
|
|
const result = repository.getNode('nodes-base.nonExistent');
|
|
|
|
expect(result).toBe(null);
|
|
});
|
|
|
|
it('should handle SplitInBatches counterintuitive output order correctly', () => {
|
|
// Test that the output order is preserved: done=0, loop=1
|
|
const outputs = [
|
|
{ displayName: 'Done', description: 'Final results when loop completes', index: 0 },
|
|
{ displayName: 'Loop', description: 'Current batch data during iteration', index: 1 }
|
|
];
|
|
const outputNames = ['done', 'loop'];
|
|
|
|
const mockRow = {
|
|
node_type: 'nodes-base.splitInBatches',
|
|
display_name: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '3',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: JSON.stringify(outputs),
|
|
output_names: JSON.stringify(outputNames),
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null,
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.splitInBatches');
|
|
|
|
// Verify order is preserved
|
|
expect(result.outputs[0].displayName).toBe('Done');
|
|
expect(result.outputs[1].displayName).toBe('Loop');
|
|
expect(result.outputNames[0]).toBe('done');
|
|
expect(result.outputNames[1]).toBe('loop');
|
|
});
|
|
});
|
|
|
|
describe('parseNodeRow with outputs', () => {
|
|
it('should parse node row with outputs correctly using parseNodeRow', () => {
|
|
const outputs = [{ displayName: 'Output' }];
|
|
const outputNames = ['main'];
|
|
|
|
const mockRow = {
|
|
node_type: 'nodes-base.test',
|
|
display_name: 'Test',
|
|
description: 'Test node',
|
|
category: 'misc',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '1',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: JSON.stringify(outputs),
|
|
output_names: JSON.stringify(outputNames),
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null,
|
|
};
|
|
|
|
mockStatement.all.mockReturnValue([mockRow]);
|
|
|
|
const results = repository.getAllNodes(1);
|
|
|
|
expect(results[0].outputs).toEqual(outputs);
|
|
expect(results[0].outputNames).toEqual(outputNames);
|
|
});
|
|
|
|
it('should handle empty string as null for outputs', () => {
|
|
const mockRow = {
|
|
node_type: 'nodes-base.empty',
|
|
display_name: 'Empty',
|
|
description: 'Empty node',
|
|
category: 'misc',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '1',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: '', // empty string
|
|
output_names: '', // empty string
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null,
|
|
};
|
|
|
|
mockStatement.all.mockReturnValue([mockRow]);
|
|
|
|
const results = repository.getAllNodes(1);
|
|
|
|
// Empty strings should be treated as null since they fail JSON parsing
|
|
expect(results[0].outputs).toBe(null);
|
|
expect(results[0].outputNames).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('complex output structures', () => {
|
|
it('should handle complex output objects with metadata', () => {
|
|
const complexOutputs = [
|
|
{
|
|
displayName: 'Done',
|
|
name: 'done',
|
|
type: 'main',
|
|
hint: 'Receives the final data after all batches have been processed',
|
|
description: 'Final results when loop completes',
|
|
index: 0
|
|
},
|
|
{
|
|
displayName: 'Loop',
|
|
name: 'loop',
|
|
type: 'main',
|
|
hint: 'Receives the current batch data during each iteration',
|
|
description: 'Current batch data during iteration',
|
|
index: 1
|
|
}
|
|
];
|
|
|
|
const node: ParsedNode = {
|
|
style: 'programmatic',
|
|
nodeType: 'nodes-base.splitInBatches',
|
|
displayName: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
properties: [],
|
|
credentials: [],
|
|
isAITool: false,
|
|
isTrigger: false,
|
|
isWebhook: false,
|
|
operations: [],
|
|
version: '3',
|
|
isVersioned: false,
|
|
packageName: 'n8n-nodes-base',
|
|
outputs: complexOutputs,
|
|
outputNames: ['done', 'loop']
|
|
};
|
|
|
|
repository.saveNode(node);
|
|
|
|
// Simulate retrieval
|
|
const mockRow = {
|
|
node_type: 'nodes-base.splitInBatches',
|
|
display_name: 'Split In Batches',
|
|
description: 'Split data into batches',
|
|
category: 'transform',
|
|
development_style: 'programmatic',
|
|
package_name: 'n8n-nodes-base',
|
|
is_ai_tool: 0,
|
|
is_trigger: 0,
|
|
is_webhook: 0,
|
|
is_versioned: 0,
|
|
is_tool_variant: 0,
|
|
tool_variant_of: null,
|
|
has_tool_variant: 0,
|
|
version: '3',
|
|
properties_schema: JSON.stringify([]),
|
|
operations: JSON.stringify([]),
|
|
credentials_required: JSON.stringify([]),
|
|
documentation: null,
|
|
outputs: JSON.stringify(complexOutputs),
|
|
output_names: JSON.stringify(['done', 'loop']),
|
|
is_community: 0,
|
|
is_verified: 0,
|
|
author_name: null,
|
|
author_github_url: null,
|
|
npm_package_name: null,
|
|
npm_version: null,
|
|
npm_downloads: 0,
|
|
community_fetched_at: null,
|
|
};
|
|
|
|
mockStatement.get.mockReturnValue(mockRow);
|
|
|
|
const result = repository.getNode('nodes-base.splitInBatches');
|
|
|
|
expect(result.outputs).toEqual(complexOutputs);
|
|
expect(result.outputs[0]).toMatchObject({
|
|
displayName: 'Done',
|
|
name: 'done',
|
|
type: 'main',
|
|
hint: 'Receives the final data after all batches have been processed'
|
|
});
|
|
});
|
|
});
|
|
}); |