feat: implement node parser and property extractor with versioned node support
This commit is contained in:
94
src/database/node-repository.ts
Normal file
94
src/database/node-repository.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { ParsedNode } from '../parsers/node-parser';
|
||||
|
||||
export class NodeRepository {
|
||||
constructor(private db: Database.Database) {}
|
||||
|
||||
/**
|
||||
* Save node with proper JSON serialization
|
||||
*/
|
||||
saveNode(node: ParsedNode): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT OR REPLACE 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
node.nodeType,
|
||||
node.packageName,
|
||||
node.displayName,
|
||||
node.description,
|
||||
node.category,
|
||||
node.style,
|
||||
node.isAITool ? 1 : 0,
|
||||
node.isTrigger ? 1 : 0,
|
||||
node.isWebhook ? 1 : 0,
|
||||
node.isVersioned ? 1 : 0,
|
||||
node.version,
|
||||
node.documentation || null,
|
||||
JSON.stringify(node.properties, null, 2),
|
||||
JSON.stringify(node.operations, null, 2),
|
||||
JSON.stringify(node.credentials, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node with proper JSON deserialization
|
||||
*/
|
||||
getNode(nodeType: string): any {
|
||||
const row = this.db.prepare(`
|
||||
SELECT * FROM nodes WHERE node_type = ?
|
||||
`).get(nodeType) as any;
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
nodeType: row.node_type,
|
||||
displayName: row.display_name,
|
||||
description: row.description,
|
||||
category: row.category,
|
||||
developmentStyle: row.development_style,
|
||||
package: row.package_name,
|
||||
isAITool: !!row.is_ai_tool,
|
||||
isTrigger: !!row.is_trigger,
|
||||
isWebhook: !!row.is_webhook,
|
||||
isVersioned: !!row.is_versioned,
|
||||
version: row.version,
|
||||
properties: this.safeJsonParse(row.properties_schema, []),
|
||||
operations: this.safeJsonParse(row.operations, []),
|
||||
credentials: this.safeJsonParse(row.credentials_required, []),
|
||||
hasDocumentation: !!row.documentation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI tools with proper filtering
|
||||
*/
|
||||
getAITools(): any[] {
|
||||
const rows = this.db.prepare(`
|
||||
SELECT node_type, display_name, description, package_name
|
||||
FROM nodes
|
||||
WHERE is_ai_tool = 1
|
||||
ORDER BY display_name
|
||||
`).all() as any[];
|
||||
|
||||
return rows.map(row => ({
|
||||
nodeType: row.node_type,
|
||||
displayName: row.display_name,
|
||||
description: row.description,
|
||||
package: row.package_name
|
||||
}));
|
||||
}
|
||||
|
||||
private safeJsonParse(json: string, defaultValue: any): any {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* n8n-MCP - Model Context Protocol Server for n8n
|
||||
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
|
||||
* Licensed under the Sustainable Use License v1.0
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import { N8NMCPServer } from './mcp/server';
|
||||
import { MCPServerConfig, N8NConfig } from './types';
|
||||
|
||||
@@ -9,6 +9,7 @@ import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { n8nDocumentationTools } from './tools-update';
|
||||
import { logger } from '../utils/logger';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
|
||||
interface NodeRow {
|
||||
node_type: string;
|
||||
@@ -31,6 +32,7 @@ interface NodeRow {
|
||||
export class N8NDocumentationMCPServer {
|
||||
private server: Server;
|
||||
private db: Database.Database;
|
||||
private repository: NodeRepository;
|
||||
|
||||
constructor() {
|
||||
// Try multiple database paths
|
||||
@@ -55,6 +57,7 @@ export class N8NDocumentationMCPServer {
|
||||
|
||||
try {
|
||||
this.db = new Database(dbPath);
|
||||
this.repository = new NodeRepository(this.db);
|
||||
logger.info(`Initialized database from: ${dbPath}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize database:', error);
|
||||
@@ -184,31 +187,31 @@ export class N8NDocumentationMCPServer {
|
||||
}
|
||||
|
||||
private getNodeInfo(nodeType: string): any {
|
||||
const node = this.db.prepare(`
|
||||
SELECT * FROM nodes WHERE node_type = ?
|
||||
`).get(nodeType) as NodeRow | undefined;
|
||||
let node = this.repository.getNode(nodeType);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
// Try alternative formats
|
||||
const alternatives = [
|
||||
nodeType,
|
||||
nodeType.replace('n8n-nodes-base.', ''),
|
||||
`n8n-nodes-base.${nodeType}`,
|
||||
nodeType.toLowerCase()
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
const found = this.repository.getNode(alt);
|
||||
if (found) {
|
||||
node = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodeType: node.node_type,
|
||||
displayName: node.display_name,
|
||||
description: node.description,
|
||||
category: node.category,
|
||||
developmentStyle: node.development_style,
|
||||
package: node.package_name,
|
||||
isAITool: !!node.is_ai_tool,
|
||||
isTrigger: !!node.is_trigger,
|
||||
isWebhook: !!node.is_webhook,
|
||||
isVersioned: !!node.is_versioned,
|
||||
version: node.version,
|
||||
properties: JSON.parse(node.properties_schema || '[]'),
|
||||
operations: JSON.parse(node.operations || '[]'),
|
||||
credentials: JSON.parse(node.credentials_required || '[]'),
|
||||
hasDocumentation: !!node.documentation,
|
||||
};
|
||||
return node;
|
||||
}
|
||||
|
||||
private searchNodes(query: string, limit: number = 20): any {
|
||||
@@ -256,20 +259,10 @@ export class N8NDocumentationMCPServer {
|
||||
}
|
||||
|
||||
private listAITools(): any {
|
||||
const tools = this.db.prepare(`
|
||||
SELECT node_type, display_name, description, package_name
|
||||
FROM nodes
|
||||
WHERE is_ai_tool = 1
|
||||
ORDER BY display_name
|
||||
`).all() as NodeRow[];
|
||||
const tools = this.repository.getAITools();
|
||||
|
||||
return {
|
||||
tools: tools.map(tool => ({
|
||||
nodeType: tool.node_type,
|
||||
displayName: tool.display_name,
|
||||
description: tool.description,
|
||||
package: tool.package_name,
|
||||
})),
|
||||
tools,
|
||||
totalCount: tools.length,
|
||||
requirements: {
|
||||
environmentVariable: 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true',
|
||||
|
||||
164
src/parsers/node-parser.ts
Normal file
164
src/parsers/node-parser.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { PropertyExtractor } from './property-extractor';
|
||||
|
||||
export interface ParsedNode {
|
||||
style: 'declarative' | 'programmatic';
|
||||
nodeType: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
properties: any[];
|
||||
credentials: any[];
|
||||
isAITool: boolean;
|
||||
isTrigger: boolean;
|
||||
isWebhook: boolean;
|
||||
operations: any[];
|
||||
version?: string;
|
||||
isVersioned: boolean;
|
||||
packageName: string;
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
export class NodeParser {
|
||||
private propertyExtractor = new PropertyExtractor();
|
||||
|
||||
parse(nodeClass: any, packageName: string): ParsedNode {
|
||||
// Get base description (handles versioned nodes)
|
||||
const description = this.getNodeDescription(nodeClass);
|
||||
|
||||
return {
|
||||
style: this.detectStyle(nodeClass),
|
||||
nodeType: this.extractNodeType(description, packageName),
|
||||
displayName: description.displayName || description.name,
|
||||
description: description.description,
|
||||
category: this.extractCategory(description),
|
||||
properties: this.propertyExtractor.extractProperties(nodeClass),
|
||||
credentials: this.propertyExtractor.extractCredentials(nodeClass),
|
||||
isAITool: this.propertyExtractor.detectAIToolCapability(nodeClass),
|
||||
isTrigger: this.detectTrigger(description),
|
||||
isWebhook: this.detectWebhook(description),
|
||||
operations: this.propertyExtractor.extractOperations(nodeClass),
|
||||
version: this.extractVersion(nodeClass),
|
||||
isVersioned: this.detectVersioned(nodeClass),
|
||||
packageName: packageName
|
||||
};
|
||||
}
|
||||
|
||||
private getNodeDescription(nodeClass: any): any {
|
||||
// Try to get description from the class first
|
||||
let description: any;
|
||||
|
||||
// Check if it's a versioned node (has baseDescription and nodeVersions)
|
||||
if (typeof nodeClass === 'function' && nodeClass.prototype &&
|
||||
nodeClass.prototype.constructor &&
|
||||
nodeClass.prototype.constructor.name === 'VersionedNodeType') {
|
||||
// This is a VersionedNodeType class - instantiate it
|
||||
const instance = new nodeClass();
|
||||
description = instance.baseDescription || {};
|
||||
} else if (typeof nodeClass === 'function') {
|
||||
// Try to instantiate to get description
|
||||
try {
|
||||
const instance = new nodeClass();
|
||||
description = instance.description || {};
|
||||
|
||||
// For versioned nodes, we might need to look deeper
|
||||
if (!description.name && instance.baseDescription) {
|
||||
description = instance.baseDescription;
|
||||
}
|
||||
} catch (e) {
|
||||
// Some nodes might require parameters to instantiate
|
||||
// Try to access static properties
|
||||
description = nodeClass.description || {};
|
||||
}
|
||||
} else {
|
||||
// Maybe it's already an instance
|
||||
description = nodeClass.description || {};
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
private detectStyle(nodeClass: any): 'declarative' | 'programmatic' {
|
||||
const desc = this.getNodeDescription(nodeClass);
|
||||
return desc.routing ? 'declarative' : 'programmatic';
|
||||
}
|
||||
|
||||
private extractNodeType(description: any, packageName: string): string {
|
||||
// Ensure we have the full node type including package prefix
|
||||
const name = description.name;
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Node is missing name property');
|
||||
}
|
||||
|
||||
if (name.includes('.')) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// Add package prefix if missing
|
||||
const packagePrefix = packageName.replace('@n8n/', '').replace('n8n-', '');
|
||||
return `${packagePrefix}.${name}`;
|
||||
}
|
||||
|
||||
private extractCategory(description: any): string {
|
||||
return description.group?.[0] ||
|
||||
description.categories?.[0] ||
|
||||
description.category ||
|
||||
'misc';
|
||||
}
|
||||
|
||||
private detectTrigger(description: any): boolean {
|
||||
return description.polling === true ||
|
||||
description.trigger === true ||
|
||||
description.eventTrigger === true ||
|
||||
description.name?.toLowerCase().includes('trigger');
|
||||
}
|
||||
|
||||
private detectWebhook(description: any): boolean {
|
||||
return (description.webhooks?.length > 0) ||
|
||||
description.webhook === true ||
|
||||
description.name?.toLowerCase().includes('webhook');
|
||||
}
|
||||
|
||||
private extractVersion(nodeClass: any): string {
|
||||
if (nodeClass.baseDescription?.defaultVersion) {
|
||||
return nodeClass.baseDescription.defaultVersion.toString();
|
||||
}
|
||||
|
||||
if (nodeClass.nodeVersions) {
|
||||
const versions = Object.keys(nodeClass.nodeVersions);
|
||||
return Math.max(...versions.map(Number)).toString();
|
||||
}
|
||||
|
||||
// Check instance for nodeVersions
|
||||
try {
|
||||
const instance = typeof nodeClass === 'function' ? new nodeClass() : nodeClass;
|
||||
if (instance?.nodeVersions) {
|
||||
const versions = Object.keys(instance.nodeVersions);
|
||||
return Math.max(...versions.map(Number)).toString();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return nodeClass.description?.version || '1';
|
||||
}
|
||||
|
||||
private detectVersioned(nodeClass: any): boolean {
|
||||
// Check class-level nodeVersions
|
||||
if (nodeClass.nodeVersions || nodeClass.baseDescription?.defaultVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check instance-level nodeVersions
|
||||
try {
|
||||
const instance = typeof nodeClass === 'function' ? new nodeClass() : nodeClass;
|
||||
if (instance?.nodeVersions) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
215
src/parsers/property-extractor.ts
Normal file
215
src/parsers/property-extractor.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
export class PropertyExtractor {
|
||||
/**
|
||||
* Extract properties with proper handling of n8n's complex structures
|
||||
*/
|
||||
extractProperties(nodeClass: any): any[] {
|
||||
const properties: any[] = [];
|
||||
|
||||
// First try to get instance-level properties
|
||||
let instance: any;
|
||||
try {
|
||||
instance = typeof nodeClass === 'function' ? new nodeClass() : nodeClass;
|
||||
} catch (e) {
|
||||
// Failed to instantiate
|
||||
}
|
||||
|
||||
// Handle versioned nodes - check instance for nodeVersions
|
||||
if (instance?.nodeVersions) {
|
||||
const versions = Object.keys(instance.nodeVersions);
|
||||
const latestVersion = Math.max(...versions.map(Number));
|
||||
const versionedNode = instance.nodeVersions[latestVersion];
|
||||
|
||||
if (versionedNode?.description?.properties) {
|
||||
return this.normalizeProperties(versionedNode.description.properties);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for description with properties
|
||||
const description = instance?.description || instance?.baseDescription ||
|
||||
this.getNodeDescription(nodeClass);
|
||||
|
||||
if (description?.properties) {
|
||||
return this.normalizeProperties(description.properties);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private getNodeDescription(nodeClass: any): any {
|
||||
// Try to get description from the class first
|
||||
let description: any;
|
||||
|
||||
if (typeof nodeClass === 'function') {
|
||||
// Try to instantiate to get description
|
||||
try {
|
||||
const instance = new nodeClass();
|
||||
description = instance.description || instance.baseDescription || {};
|
||||
} catch (e) {
|
||||
// Some nodes might require parameters to instantiate
|
||||
description = nodeClass.description || {};
|
||||
}
|
||||
} else {
|
||||
description = nodeClass.description || {};
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract operations from both declarative and programmatic nodes
|
||||
*/
|
||||
extractOperations(nodeClass: any): any[] {
|
||||
const operations: any[] = [];
|
||||
|
||||
// First try to get instance-level data
|
||||
let instance: any;
|
||||
try {
|
||||
instance = typeof nodeClass === 'function' ? new nodeClass() : nodeClass;
|
||||
} catch (e) {
|
||||
// Failed to instantiate
|
||||
}
|
||||
|
||||
// Handle versioned nodes
|
||||
if (instance?.nodeVersions) {
|
||||
const versions = Object.keys(instance.nodeVersions);
|
||||
const latestVersion = Math.max(...versions.map(Number));
|
||||
const versionedNode = instance.nodeVersions[latestVersion];
|
||||
|
||||
if (versionedNode?.description) {
|
||||
return this.extractOperationsFromDescription(versionedNode.description);
|
||||
}
|
||||
}
|
||||
|
||||
// Get description
|
||||
const description = instance?.description || instance?.baseDescription ||
|
||||
this.getNodeDescription(nodeClass);
|
||||
|
||||
return this.extractOperationsFromDescription(description);
|
||||
}
|
||||
|
||||
private extractOperationsFromDescription(description: any): any[] {
|
||||
const operations: any[] = [];
|
||||
|
||||
if (!description) return operations;
|
||||
|
||||
// Declarative nodes (with routing)
|
||||
if (description.routing) {
|
||||
const routing = description.routing;
|
||||
|
||||
// Extract from request.resource and request.operation
|
||||
if (routing.request?.resource) {
|
||||
const resources = routing.request.resource.options || [];
|
||||
const operationOptions = routing.request.operation?.options || {};
|
||||
|
||||
resources.forEach((resource: any) => {
|
||||
const resourceOps = operationOptions[resource.value] || [];
|
||||
resourceOps.forEach((op: any) => {
|
||||
operations.push({
|
||||
resource: resource.value,
|
||||
operation: op.value,
|
||||
name: `${resource.name} - ${op.name}`,
|
||||
action: op.action
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Programmatic nodes - look for operation property in properties
|
||||
if (description.properties && Array.isArray(description.properties)) {
|
||||
const operationProp = description.properties.find(
|
||||
(p: any) => p.name === 'operation' || p.name === 'action'
|
||||
);
|
||||
|
||||
if (operationProp?.options) {
|
||||
operationProp.options.forEach((op: any) => {
|
||||
operations.push({
|
||||
operation: op.value,
|
||||
name: op.name,
|
||||
description: op.description
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep search for AI tool capability
|
||||
*/
|
||||
detectAIToolCapability(nodeClass: any): boolean {
|
||||
const description = this.getNodeDescription(nodeClass);
|
||||
|
||||
// Direct property check
|
||||
if (description?.usableAsTool === true) return true;
|
||||
|
||||
// Check in actions for declarative nodes
|
||||
if (description?.actions?.some((a: any) => a.usableAsTool === true)) return true;
|
||||
|
||||
// Check versioned nodes
|
||||
if (nodeClass.nodeVersions) {
|
||||
for (const version of Object.values(nodeClass.nodeVersions)) {
|
||||
if ((version as any).description?.usableAsTool === true) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for specific AI-related properties
|
||||
const aiIndicators = ['openai', 'anthropic', 'huggingface', 'cohere', 'ai'];
|
||||
const nodeName = description?.name?.toLowerCase() || '';
|
||||
|
||||
return aiIndicators.some(indicator => nodeName.includes(indicator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract credential requirements with proper structure
|
||||
*/
|
||||
extractCredentials(nodeClass: any): any[] {
|
||||
const credentials: any[] = [];
|
||||
|
||||
// First try to get instance-level data
|
||||
let instance: any;
|
||||
try {
|
||||
instance = typeof nodeClass === 'function' ? new nodeClass() : nodeClass;
|
||||
} catch (e) {
|
||||
// Failed to instantiate
|
||||
}
|
||||
|
||||
// Handle versioned nodes
|
||||
if (instance?.nodeVersions) {
|
||||
const versions = Object.keys(instance.nodeVersions);
|
||||
const latestVersion = Math.max(...versions.map(Number));
|
||||
const versionedNode = instance.nodeVersions[latestVersion];
|
||||
|
||||
if (versionedNode?.description?.credentials) {
|
||||
return versionedNode.description.credentials;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for description with credentials
|
||||
const description = instance?.description || instance?.baseDescription ||
|
||||
this.getNodeDescription(nodeClass);
|
||||
|
||||
if (description?.credentials) {
|
||||
return description.credentials;
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private normalizeProperties(properties: any[]): any[] {
|
||||
// Ensure all properties have consistent structure
|
||||
return properties.map(prop => ({
|
||||
displayName: prop.displayName,
|
||||
name: prop.name,
|
||||
type: prop.type,
|
||||
default: prop.default,
|
||||
description: prop.description,
|
||||
options: prop.options,
|
||||
required: prop.required,
|
||||
displayOptions: prop.displayOptions,
|
||||
typeOptions: prop.typeOptions,
|
||||
noDataExpression: prop.noDataExpression
|
||||
}));
|
||||
}
|
||||
}
|
||||
65
src/scripts/debug-node.ts
Normal file
65
src/scripts/debug-node.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
import { N8nNodeLoader } from '../loaders/node-loader';
|
||||
import { NodeParser } from '../parsers/node-parser';
|
||||
|
||||
async function debugNode() {
|
||||
const loader = new N8nNodeLoader();
|
||||
const parser = new NodeParser();
|
||||
|
||||
console.log('Loading nodes...');
|
||||
const nodes = await loader.loadAllNodes();
|
||||
|
||||
// Find HTTP Request node
|
||||
const httpNode = nodes.find(n => n.nodeName === 'HttpRequest');
|
||||
|
||||
if (httpNode) {
|
||||
console.log('\n=== HTTP Request Node Debug ===');
|
||||
console.log('NodeName:', httpNode.nodeName);
|
||||
console.log('Package:', httpNode.packageName);
|
||||
console.log('NodeClass type:', typeof httpNode.NodeClass);
|
||||
console.log('NodeClass constructor name:', httpNode.NodeClass?.constructor?.name);
|
||||
|
||||
try {
|
||||
const parsed = parser.parse(httpNode.NodeClass, httpNode.packageName);
|
||||
console.log('\nParsed successfully:');
|
||||
console.log('- Node Type:', parsed.nodeType);
|
||||
console.log('- Display Name:', parsed.displayName);
|
||||
console.log('- Style:', parsed.style);
|
||||
console.log('- Properties count:', parsed.properties.length);
|
||||
console.log('- Operations count:', parsed.operations.length);
|
||||
console.log('- Is AI Tool:', parsed.isAITool);
|
||||
console.log('- Is Versioned:', parsed.isVersioned);
|
||||
|
||||
if (parsed.properties.length > 0) {
|
||||
console.log('\nFirst property:', parsed.properties[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\nError parsing node:', (error as Error).message);
|
||||
console.error('Stack:', (error as Error).stack);
|
||||
}
|
||||
} else {
|
||||
console.log('HTTP Request node not found');
|
||||
}
|
||||
|
||||
// Find Code node
|
||||
const codeNode = nodes.find(n => n.nodeName === 'Code');
|
||||
|
||||
if (codeNode) {
|
||||
console.log('\n\n=== Code Node Debug ===');
|
||||
console.log('NodeName:', codeNode.nodeName);
|
||||
console.log('Package:', codeNode.packageName);
|
||||
console.log('NodeClass type:', typeof codeNode.NodeClass);
|
||||
|
||||
try {
|
||||
const parsed = parser.parse(codeNode.NodeClass, codeNode.packageName);
|
||||
console.log('\nParsed successfully:');
|
||||
console.log('- Node Type:', parsed.nodeType);
|
||||
console.log('- Properties count:', parsed.properties.length);
|
||||
console.log('- Is Versioned:', parsed.isVersioned);
|
||||
} catch (error) {
|
||||
console.error('\nError parsing node:', (error as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugNode().catch(console.error);
|
||||
@@ -1,22 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
|
||||
* Licensed under the Sustainable Use License v1.0
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import { N8nNodeLoader } from '../loaders/node-loader';
|
||||
import { SimpleParser } from '../parsers/simple-parser';
|
||||
import { NodeParser } from '../parsers/node-parser';
|
||||
import { DocsMapper } from '../mappers/docs-mapper';
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
async function rebuild() {
|
||||
console.log('🔄 Rebuilding n8n node database...\n');
|
||||
|
||||
const db = new Database('./data/nodes.db');
|
||||
const loader = new N8nNodeLoader();
|
||||
const parser = new SimpleParser();
|
||||
const parser = new NodeParser();
|
||||
const mapper = new DocsMapper();
|
||||
const repository = new NodeRepository(db);
|
||||
|
||||
// Initialize database
|
||||
const schemaPath = path.join(__dirname, '../../src/database/schema.sql');
|
||||
const schema = readFileSync(schemaPath, 'utf8');
|
||||
const schema = fs.readFileSync(path.join(__dirname, '../../src/database/schema.sql'), 'utf8');
|
||||
db.exec(schema);
|
||||
|
||||
// Clear existing data
|
||||
@@ -28,74 +33,108 @@ async function rebuild() {
|
||||
console.log(`📦 Loaded ${nodes.length} nodes from packages\n`);
|
||||
|
||||
// Statistics
|
||||
let successful = 0;
|
||||
let failed = 0;
|
||||
let aiTools = 0;
|
||||
const stats = {
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
aiTools: 0,
|
||||
triggers: 0,
|
||||
webhooks: 0,
|
||||
withProperties: 0,
|
||||
withOperations: 0,
|
||||
withDocs: 0
|
||||
};
|
||||
|
||||
// Process each node
|
||||
for (const { packageName, nodeName, NodeClass } of nodes) {
|
||||
try {
|
||||
// Debug: log what we're working with
|
||||
// Don't check for description here since it might be an instance property
|
||||
if (!NodeClass) {
|
||||
console.error(`❌ Node ${nodeName} has no NodeClass`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse node
|
||||
const parsed = parser.parse(NodeClass);
|
||||
const parsed = parser.parse(NodeClass, packageName);
|
||||
|
||||
// Validate parsed data
|
||||
if (!parsed.nodeType || !parsed.displayName) {
|
||||
throw new Error('Missing required fields');
|
||||
}
|
||||
|
||||
// Get documentation
|
||||
const docs = await mapper.fetchDocumentation(parsed.nodeType);
|
||||
parsed.documentation = docs || undefined;
|
||||
|
||||
// Insert into database
|
||||
db.prepare(`
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
parsed.nodeType,
|
||||
packageName,
|
||||
parsed.displayName,
|
||||
parsed.description,
|
||||
parsed.category,
|
||||
parsed.style,
|
||||
parsed.isAITool ? 1 : 0,
|
||||
parsed.isTrigger ? 1 : 0,
|
||||
parsed.isWebhook ? 1 : 0,
|
||||
parsed.isVersioned ? 1 : 0,
|
||||
parsed.version,
|
||||
docs,
|
||||
JSON.stringify(parsed.properties),
|
||||
JSON.stringify(parsed.operations),
|
||||
JSON.stringify(parsed.credentials)
|
||||
);
|
||||
// Save to database
|
||||
repository.saveNode(parsed);
|
||||
|
||||
successful++;
|
||||
if (parsed.isAITool) aiTools++;
|
||||
// Update statistics
|
||||
stats.successful++;
|
||||
if (parsed.isAITool) stats.aiTools++;
|
||||
if (parsed.isTrigger) stats.triggers++;
|
||||
if (parsed.isWebhook) stats.webhooks++;
|
||||
if (parsed.properties.length > 0) stats.withProperties++;
|
||||
if (parsed.operations.length > 0) stats.withOperations++;
|
||||
if (docs) stats.withDocs++;
|
||||
|
||||
console.log(`✅ ${parsed.nodeType}`);
|
||||
console.log(`✅ ${parsed.nodeType} [Props: ${parsed.properties.length}, Ops: ${parsed.operations.length}]`);
|
||||
} catch (error) {
|
||||
failed++;
|
||||
stats.failed++;
|
||||
console.error(`❌ Failed to process ${nodeName}: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validation check
|
||||
console.log('\n🔍 Running validation checks...');
|
||||
const validationResults = validateDatabase(repository);
|
||||
|
||||
// Summary
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` Total nodes: ${nodes.length}`);
|
||||
console.log(` Successful: ${successful}`);
|
||||
console.log(` Failed: ${failed}`);
|
||||
console.log(` AI Tools: ${aiTools}`);
|
||||
console.log(` Successful: ${stats.successful}`);
|
||||
console.log(` Failed: ${stats.failed}`);
|
||||
console.log(` AI Tools: ${stats.aiTools}`);
|
||||
console.log(` Triggers: ${stats.triggers}`);
|
||||
console.log(` Webhooks: ${stats.webhooks}`);
|
||||
console.log(` With Properties: ${stats.withProperties}`);
|
||||
console.log(` With Operations: ${stats.withOperations}`);
|
||||
console.log(` With Documentation: ${stats.withDocs}`);
|
||||
|
||||
if (!validationResults.passed) {
|
||||
console.log('\n⚠️ Validation Issues:');
|
||||
validationResults.issues.forEach(issue => console.log(` - ${issue}`));
|
||||
}
|
||||
|
||||
console.log('\n✨ Rebuild complete!');
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
function validateDatabase(repository: NodeRepository): { passed: boolean; issues: string[] } {
|
||||
const issues = [];
|
||||
|
||||
// Check critical nodes
|
||||
const criticalNodes = ['nodes-base.httpRequest', 'nodes-base.code', 'nodes-base.webhook', 'nodes-base.slack'];
|
||||
|
||||
for (const nodeType of criticalNodes) {
|
||||
const node = repository.getNode(nodeType);
|
||||
|
||||
if (!node) {
|
||||
issues.push(`Critical node ${nodeType} not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.properties.length === 0) {
|
||||
issues.push(`Node ${nodeType} has no properties`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check AI tools
|
||||
const aiTools = repository.getAITools();
|
||||
if (aiTools.length === 0) {
|
||||
issues.push('No AI tools found - check detection logic');
|
||||
}
|
||||
|
||||
return {
|
||||
passed: issues.length === 0,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
rebuild().catch(console.error);
|
||||
|
||||
108
src/scripts/test-nodes.ts
Normal file
108
src/scripts/test-nodes.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
|
||||
* Licensed under the Sustainable Use License v1.0
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import { NodeRepository } from '../database/node-repository';
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
nodeType: 'nodes-base.httpRequest',
|
||||
checks: {
|
||||
hasProperties: true,
|
||||
minProperties: 5,
|
||||
hasDocumentation: true,
|
||||
isVersioned: true
|
||||
}
|
||||
},
|
||||
{
|
||||
nodeType: 'nodes-base.slack',
|
||||
checks: {
|
||||
hasOperations: true,
|
||||
minOperations: 10,
|
||||
style: 'declarative'
|
||||
}
|
||||
},
|
||||
{
|
||||
nodeType: 'nodes-base.code',
|
||||
checks: {
|
||||
hasProperties: true,
|
||||
properties: ['mode', 'language', 'jsCode']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function runTests() {
|
||||
const db = new Database('./data/nodes.db');
|
||||
const repository = new NodeRepository(db);
|
||||
|
||||
console.log('🧪 Running node tests...\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const testCase of TEST_CASES) {
|
||||
console.log(`Testing ${testCase.nodeType}...`);
|
||||
|
||||
try {
|
||||
const node = repository.getNode(testCase.nodeType);
|
||||
|
||||
if (!node) {
|
||||
throw new Error('Node not found');
|
||||
}
|
||||
|
||||
// Run checks
|
||||
for (const [check, expected] of Object.entries(testCase.checks)) {
|
||||
switch (check) {
|
||||
case 'hasProperties':
|
||||
if (expected && node.properties.length === 0) {
|
||||
throw new Error('No properties found');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'minProperties':
|
||||
if (node.properties.length < expected) {
|
||||
throw new Error(`Expected at least ${expected} properties, got ${node.properties.length}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hasOperations':
|
||||
if (expected && node.operations.length === 0) {
|
||||
throw new Error('No operations found');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'minOperations':
|
||||
if (node.operations.length < expected) {
|
||||
throw new Error(`Expected at least ${expected} operations, got ${node.operations.length}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'properties':
|
||||
const propNames = node.properties.map((p: any) => p.name);
|
||||
for (const prop of expected as string[]) {
|
||||
if (!propNames.includes(prop)) {
|
||||
throw new Error(`Missing property: ${prop}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${testCase.nodeType} passed all checks\n`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.error(`❌ ${testCase.nodeType} failed: ${(error as Error).message}\n`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`);
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runTests().catch(console.error);
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
|
||||
* Licensed under the Sustainable Use License v1.0
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
interface NodeRow {
|
||||
|
||||
Reference in New Issue
Block a user