fix: webhook and 4 other nodes incorrectly marked as non-triggers
Fixed issue where Docker images using sql.js adapter returned boolean fields as strings, causing is_trigger=0 to evaluate as true instead of false. Changes: - Added convertIntegerColumns() to sql.js adapter to convert SQLite integers - Updated server.ts and node-repository.ts to use Number() conversion as backup - Added test script to verify fix works with sql.js adapter This fixes webhook, cron, interval, and emailReadImap nodes showing isTrigger: false in Docker deployments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
86
scripts/test-sqljs-triggers.ts
Normal file
86
scripts/test-sqljs-triggers.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test script to verify trigger detection works with sql.js adapter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDatabaseAdapter } from '../src/database/database-adapter';
|
||||||
|
import { NodeRepository } from '../src/database/node-repository';
|
||||||
|
import { logger } from '../src/utils/logger';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
async function testSqlJsTriggers() {
|
||||||
|
logger.info('🧪 Testing trigger detection with sql.js adapter...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force sql.js by temporarily renaming better-sqlite3
|
||||||
|
const originalRequire = require.cache[require.resolve('better-sqlite3')];
|
||||||
|
if (originalRequire) {
|
||||||
|
delete require.cache[require.resolve('better-sqlite3')];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock better-sqlite3 to force sql.js usage
|
||||||
|
const Module = require('module');
|
||||||
|
const originalResolveFilename = Module._resolveFilename;
|
||||||
|
Module._resolveFilename = function(request: string, parent: any, isMain: boolean) {
|
||||||
|
if (request === 'better-sqlite3') {
|
||||||
|
throw new Error('Forcing sql.js adapter for testing');
|
||||||
|
}
|
||||||
|
return originalResolveFilename.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now create adapter - should use sql.js
|
||||||
|
const dbPath = path.join(process.cwd(), 'data', 'nodes.db');
|
||||||
|
logger.info(`📁 Database path: ${dbPath}`);
|
||||||
|
|
||||||
|
const adapter = await createDatabaseAdapter(dbPath);
|
||||||
|
logger.info('✅ Adapter created (should be sql.js)\n');
|
||||||
|
|
||||||
|
// Test direct query
|
||||||
|
logger.info('📊 Testing direct database query:');
|
||||||
|
const triggerNodes = ['nodes-base.webhook', 'nodes-base.cron', 'nodes-base.interval', 'nodes-base.emailReadImap'];
|
||||||
|
|
||||||
|
for (const nodeType of triggerNodes) {
|
||||||
|
const row = adapter.prepare('SELECT * FROM nodes WHERE node_type = ?').get(nodeType);
|
||||||
|
if (row) {
|
||||||
|
logger.info(`${nodeType}:`);
|
||||||
|
logger.info(` is_trigger raw value: ${row.is_trigger} (type: ${typeof row.is_trigger})`);
|
||||||
|
logger.info(` !!is_trigger: ${!!row.is_trigger}`);
|
||||||
|
logger.info(` Number(is_trigger) === 1: ${Number(row.is_trigger) === 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test through repository
|
||||||
|
logger.info('\n📦 Testing through NodeRepository:');
|
||||||
|
const repository = new NodeRepository(adapter);
|
||||||
|
|
||||||
|
for (const nodeType of triggerNodes) {
|
||||||
|
const node = repository.getNode(nodeType);
|
||||||
|
if (node) {
|
||||||
|
logger.info(`${nodeType}: isTrigger = ${node.isTrigger}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test list query
|
||||||
|
logger.info('\n📋 Testing list query:');
|
||||||
|
const allTriggers = adapter.prepare(
|
||||||
|
'SELECT node_type, is_trigger FROM nodes WHERE node_type IN (?, ?, ?, ?)'
|
||||||
|
).all(...triggerNodes);
|
||||||
|
|
||||||
|
for (const node of allTriggers) {
|
||||||
|
logger.info(`${node.node_type}: is_trigger = ${node.is_trigger} (type: ${typeof node.is_trigger})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.close();
|
||||||
|
logger.info('\n✅ Test complete!');
|
||||||
|
|
||||||
|
// Restore original require
|
||||||
|
Module._resolveFilename = originalResolveFilename;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Test failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run test
|
||||||
|
testSqlJsTriggers().catch(console.error);
|
||||||
@@ -338,7 +338,7 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
if (this.stmt.step()) {
|
if (this.stmt.step()) {
|
||||||
const result = this.stmt.getAsObject();
|
const result = this.stmt.getAsObject();
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
return result;
|
return this.convertIntegerColumns(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
@@ -354,7 +354,7 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
|
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
while (this.stmt.step()) {
|
while (this.stmt.step()) {
|
||||||
results.push(this.stmt.getAsObject());
|
results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stmt.reset();
|
this.stmt.reset();
|
||||||
@@ -400,4 +400,24 @@ class SQLJSStatement implements PreparedStatement {
|
|||||||
this.boundParams = params;
|
this.boundParams = params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert SQLite integer columns to JavaScript numbers
|
||||||
|
* sql.js returns all values as strings, but we need proper types for boolean conversion
|
||||||
|
*/
|
||||||
|
private convertIntegerColumns(row: any): any {
|
||||||
|
if (!row) return row;
|
||||||
|
|
||||||
|
// Known integer columns in the nodes table
|
||||||
|
const integerColumns = ['is_ai_tool', 'is_trigger', 'is_webhook', 'is_versioned'];
|
||||||
|
|
||||||
|
const converted = { ...row };
|
||||||
|
for (const col of integerColumns) {
|
||||||
|
if (col in converted && typeof converted[col] === 'string') {
|
||||||
|
converted[col] = parseInt(converted[col], 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,10 @@ export class NodeRepository {
|
|||||||
category: row.category,
|
category: row.category,
|
||||||
developmentStyle: row.development_style,
|
developmentStyle: row.development_style,
|
||||||
package: row.package_name,
|
package: row.package_name,
|
||||||
isAITool: !!row.is_ai_tool,
|
isAITool: Number(row.is_ai_tool) === 1,
|
||||||
isTrigger: !!row.is_trigger,
|
isTrigger: Number(row.is_trigger) === 1,
|
||||||
isWebhook: !!row.is_webhook,
|
isWebhook: Number(row.is_webhook) === 1,
|
||||||
isVersioned: !!row.is_versioned,
|
isVersioned: Number(row.is_versioned) === 1,
|
||||||
version: row.version,
|
version: row.version,
|
||||||
properties: this.safeJsonParse(row.properties_schema, []),
|
properties: this.safeJsonParse(row.properties_schema, []),
|
||||||
operations: this.safeJsonParse(row.operations, []),
|
operations: this.safeJsonParse(row.operations, []),
|
||||||
|
|||||||
@@ -331,9 +331,9 @@ export class N8NDocumentationMCPServer {
|
|||||||
category: node.category,
|
category: node.category,
|
||||||
package: node.package_name,
|
package: node.package_name,
|
||||||
developmentStyle: node.development_style,
|
developmentStyle: node.development_style,
|
||||||
isAITool: !!node.is_ai_tool,
|
isAITool: Number(node.is_ai_tool) === 1,
|
||||||
isTrigger: !!node.is_trigger,
|
isTrigger: Number(node.is_trigger) === 1,
|
||||||
isVersioned: !!node.is_versioned,
|
isVersioned: Number(node.is_versioned) === 1,
|
||||||
})),
|
})),
|
||||||
totalCount: nodes.length,
|
totalCount: nodes.length,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user