feat: implement universal Node.js compatibility with automatic database adapter fallback

This commit is contained in:
czlonkowski
2025-06-12 23:51:47 +02:00
parent 66f5d74e42
commit b476d36275
20 changed files with 16668 additions and 4772 deletions

View File

@@ -0,0 +1,383 @@
import { promises as fs } from 'fs';
import * as fsSync from 'fs';
import path from 'path';
import { logger } from '../utils/logger';
/**
* Unified database interface that abstracts better-sqlite3 and sql.js
*/
export interface DatabaseAdapter {
prepare(sql: string): PreparedStatement;
exec(sql: string): void;
close(): void;
pragma(key: string, value?: any): any;
readonly inTransaction: boolean;
transaction<T>(fn: () => T): T;
}
export interface PreparedStatement {
run(...params: any[]): RunResult;
get(...params: any[]): any;
all(...params: any[]): any[];
iterate(...params: any[]): IterableIterator<any>;
pluck(toggle?: boolean): this;
expand(toggle?: boolean): this;
raw(toggle?: boolean): this;
columns(): ColumnDefinition[];
bind(...params: any[]): this;
}
export interface RunResult {
changes: number;
lastInsertRowid: number | bigint;
}
export interface ColumnDefinition {
name: string;
column: string | null;
table: string | null;
database: string | null;
type: string | null;
}
/**
* Factory function to create a database adapter
* Tries better-sqlite3 first, falls back to sql.js if needed
*/
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
// Log Node.js version information
logger.info(`Node.js version: ${process.version}`);
logger.info(`Platform: ${process.platform} ${process.arch}`);
// First, try to use better-sqlite3
try {
logger.info('Attempting to use better-sqlite3...');
const adapter = await createBetterSQLiteAdapter(dbPath);
logger.info('Successfully initialized better-sqlite3 adapter');
return adapter;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check if it's a version mismatch error
if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
logger.warn(`Current Node.js version: ${process.version}`);
}
logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
// Fall back to sql.js
try {
const adapter = await createSQLJSAdapter(dbPath);
logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
return adapter;
} catch (sqlJsError) {
logger.error('Failed to initialize sql.js adapter', sqlJsError);
throw new Error('Failed to initialize any database adapter');
}
}
}
/**
* Create better-sqlite3 adapter
*/
async function createBetterSQLiteAdapter(dbPath: string): Promise<DatabaseAdapter> {
try {
const Database = require('better-sqlite3');
const db = new Database(dbPath);
return new BetterSQLiteAdapter(db);
} catch (error) {
throw new Error(`Failed to create better-sqlite3 adapter: ${error}`);
}
}
/**
* Create sql.js adapter with persistence
*/
async function createSQLJSAdapter(dbPath: string): Promise<DatabaseAdapter> {
const initSqlJs = require('sql.js');
// Initialize sql.js
const SQL = await initSqlJs({
// This will look for the wasm file in node_modules
locateFile: (file: string) => {
if (file.endsWith('.wasm')) {
return path.join(__dirname, '../../node_modules/sql.js/dist/', file);
}
return file;
}
});
// Try to load existing database
let db: any;
try {
const data = await fs.readFile(dbPath);
db = new SQL.Database(new Uint8Array(data));
logger.info(`Loaded existing database from ${dbPath}`);
} catch (error) {
// Create new database if file doesn't exist
db = new SQL.Database();
logger.info(`Created new database at ${dbPath}`);
}
return new SQLJSAdapter(db, dbPath);
}
/**
* Adapter for better-sqlite3
*/
class BetterSQLiteAdapter implements DatabaseAdapter {
constructor(private db: any) {}
prepare(sql: string): PreparedStatement {
const stmt = this.db.prepare(sql);
return new BetterSQLiteStatement(stmt);
}
exec(sql: string): void {
this.db.exec(sql);
}
close(): void {
this.db.close();
}
pragma(key: string, value?: any): any {
return this.db.pragma(key, value);
}
get inTransaction(): boolean {
return this.db.inTransaction;
}
transaction<T>(fn: () => T): T {
return this.db.transaction(fn)();
}
}
/**
* Adapter for sql.js with persistence
*/
class SQLJSAdapter implements DatabaseAdapter {
private saveTimer: NodeJS.Timeout | null = null;
constructor(private db: any, private dbPath: string) {
// Set up auto-save on changes
this.scheduleSave();
}
prepare(sql: string): PreparedStatement {
const stmt = this.db.prepare(sql);
this.scheduleSave();
return new SQLJSStatement(stmt, () => this.scheduleSave());
}
exec(sql: string): void {
this.db.exec(sql);
this.scheduleSave();
}
close(): void {
this.saveToFile();
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
this.db.close();
}
pragma(key: string, value?: any): any {
// sql.js doesn't support pragma in the same way
// We'll handle specific pragmas as needed
if (key === 'journal_mode' && value === 'WAL') {
// WAL mode not supported in sql.js, ignore
return 'memory';
}
return null;
}
get inTransaction(): boolean {
// sql.js doesn't expose transaction state
return false;
}
transaction<T>(fn: () => T): T {
// Simple transaction implementation for sql.js
try {
this.exec('BEGIN');
const result = fn();
this.exec('COMMIT');
return result;
} catch (error) {
this.exec('ROLLBACK');
throw error;
}
}
private scheduleSave(): void {
if (this.saveTimer) {
clearTimeout(this.saveTimer);
}
// Save after 100ms of inactivity
this.saveTimer = setTimeout(() => {
this.saveToFile();
}, 100);
}
private saveToFile(): void {
try {
const data = this.db.export();
const buffer = Buffer.from(data);
fsSync.writeFileSync(this.dbPath, buffer);
logger.debug(`Database saved to ${this.dbPath}`);
} catch (error) {
logger.error('Failed to save database', error);
}
}
}
/**
* Statement wrapper for better-sqlite3
*/
class BetterSQLiteStatement implements PreparedStatement {
constructor(private stmt: any) {}
run(...params: any[]): RunResult {
return this.stmt.run(...params);
}
get(...params: any[]): any {
return this.stmt.get(...params);
}
all(...params: any[]): any[] {
return this.stmt.all(...params);
}
iterate(...params: any[]): IterableIterator<any> {
return this.stmt.iterate(...params);
}
pluck(toggle?: boolean): this {
this.stmt.pluck(toggle);
return this;
}
expand(toggle?: boolean): this {
this.stmt.expand(toggle);
return this;
}
raw(toggle?: boolean): this {
this.stmt.raw(toggle);
return this;
}
columns(): ColumnDefinition[] {
return this.stmt.columns();
}
bind(...params: any[]): this {
this.stmt.bind(...params);
return this;
}
}
/**
* Statement wrapper for sql.js
*/
class SQLJSStatement implements PreparedStatement {
private boundParams: any = null;
constructor(private stmt: any, private onModify: () => void) {}
run(...params: any[]): RunResult {
if (params.length > 0) {
this.bindParams(params);
this.stmt.bind(this.boundParams);
}
this.stmt.run();
this.onModify();
// sql.js doesn't provide changes/lastInsertRowid easily
return {
changes: 0,
lastInsertRowid: 0
};
}
get(...params: any[]): any {
if (params.length > 0) {
this.bindParams(params);
}
this.stmt.bind(this.boundParams);
if (this.stmt.step()) {
const result = this.stmt.getAsObject();
this.stmt.reset();
return result;
}
this.stmt.reset();
return undefined;
}
all(...params: any[]): any[] {
if (params.length > 0) {
this.bindParams(params);
}
this.stmt.bind(this.boundParams);
const results: any[] = [];
while (this.stmt.step()) {
results.push(this.stmt.getAsObject());
}
this.stmt.reset();
return results;
}
iterate(...params: any[]): IterableIterator<any> {
// sql.js doesn't support generators well, return array iterator
return this.all(...params)[Symbol.iterator]();
}
pluck(toggle?: boolean): this {
// Not directly supported in sql.js
return this;
}
expand(toggle?: boolean): this {
// Not directly supported in sql.js
return this;
}
raw(toggle?: boolean): this {
// Not directly supported in sql.js
return this;
}
columns(): ColumnDefinition[] {
// sql.js has different column info
return [];
}
bind(...params: any[]): this {
this.bindParams(params);
return this;
}
private bindParams(params: any[]): void {
if (params.length === 1 && typeof params[0] === 'object' && !Array.isArray(params[0])) {
// Named parameters passed as object
this.boundParams = params[0];
} else {
// Positional parameters - sql.js uses array for positional
this.boundParams = params;
}
}
}

View File

@@ -1,8 +1,8 @@
import Database from 'better-sqlite3';
import { DatabaseAdapter } from './database-adapter';
import { ParsedNode } from '../parsers/node-parser';
export class NodeRepository {
constructor(private db: Database.Database) {}
constructor(private db: DatabaseAdapter) {}
/**
* Save node with proper JSON serialization

View File

@@ -8,8 +8,8 @@ export interface LoadedNode {
export class N8nNodeLoader {
private readonly CORE_PACKAGES = [
'n8n-nodes-base',
'@n8n/n8n-nodes-langchain'
{ name: 'n8n-nodes-base', path: 'n8n/node_modules/n8n-nodes-base' },
{ name: '@n8n/n8n-nodes-langchain', path: '@n8n/n8n-nodes-langchain' }
];
async loadAllNodes(): Promise<LoadedNode[]> {
@@ -17,21 +17,21 @@ export class N8nNodeLoader {
for (const pkg of this.CORE_PACKAGES) {
try {
console.log(`\n📦 Loading package: ${pkg}`);
// Direct require - no complex path resolution
const packageJson = require(`${pkg}/package.json`);
console.log(`\n📦 Loading package: ${pkg.name} from ${pkg.path}`);
// Use the path property to locate the package
const packageJson = require(`${pkg.path}/package.json`);
console.log(` Found ${Object.keys(packageJson.n8n?.nodes || {}).length} nodes in package.json`);
const nodes = await this.loadPackageNodes(pkg, packageJson);
const nodes = await this.loadPackageNodes(pkg.name, pkg.path, packageJson);
results.push(...nodes);
} catch (error) {
console.error(`Failed to load ${pkg}:`, error);
console.error(`Failed to load ${pkg.name}:`, error);
}
}
return results;
}
private async loadPackageNodes(packageName: string, packageJson: any): Promise<LoadedNode[]> {
private async loadPackageNodes(packageName: string, packagePath: string, packageJson: any): Promise<LoadedNode[]> {
const n8nConfig = packageJson.n8n || {};
const nodes: LoadedNode[] = [];
@@ -42,7 +42,7 @@ export class N8nNodeLoader {
// Handle array format (n8n-nodes-base uses this)
for (const nodePath of nodesList) {
try {
const fullPath = require.resolve(`${packageName}/${nodePath}`);
const fullPath = require.resolve(`${packagePath}/${nodePath}`);
const nodeModule = require(fullPath);
// Extract node name from path (e.g., "dist/nodes/Slack/Slack.node.js" -> "Slack")
@@ -65,7 +65,7 @@ export class N8nNodeLoader {
// Handle object format (for other packages)
for (const [nodeName, nodePath] of Object.entries(nodesList)) {
try {
const fullPath = require.resolve(`${packageName}/${nodePath as string}`);
const fullPath = require.resolve(`${packagePath}/${nodePath as string}`);
const nodeModule = require(fullPath);
// Handle default export and various export patterns

View File

@@ -4,12 +4,12 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import Database from 'better-sqlite3';
import { existsSync } from 'fs';
import path from 'path';
import { n8nDocumentationTools } from './tools-update';
import { logger } from '../utils/logger';
import { NodeRepository } from '../database/node-repository';
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
interface NodeRow {
node_type: string;
@@ -31,8 +31,9 @@ interface NodeRow {
export class N8NDocumentationMCPServer {
private server: Server;
private db: Database.Database;
private repository: NodeRepository;
private db: DatabaseAdapter | null = null;
private repository: NodeRepository | null = null;
private initialized: Promise<void>;
constructor() {
// Try multiple database paths
@@ -55,14 +56,8 @@ export class N8NDocumentationMCPServer {
throw new Error('Database nodes.db not found. Please run npm run rebuild first.');
}
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);
throw new Error(`Failed to open database: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Initialize database asynchronously
this.initialized = this.initializeDatabase(dbPath);
logger.info('Initializing n8n Documentation MCP server');
@@ -80,6 +75,24 @@ export class N8NDocumentationMCPServer {
this.setupHandlers();
}
private async initializeDatabase(dbPath: string): Promise<void> {
try {
this.db = await createDatabaseAdapter(dbPath);
this.repository = new NodeRepository(this.db);
logger.info(`Initialized database from: ${dbPath}`);
} catch (error) {
logger.error('Failed to initialize database:', error);
throw new Error(`Failed to open database: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private async ensureInitialized(): Promise<void> {
await this.initialized;
if (!this.db || !this.repository) {
throw new Error('Database not initialized');
}
}
private setupHandlers(): void {
// Handle tool listing
@@ -168,7 +181,7 @@ export class N8NDocumentationMCPServer {
params.push(filters.limit);
}
const nodes = this.db.prepare(query).all(...params) as NodeRow[];
const nodes = this.db!.prepare(query).all(...params) as NodeRow[];
return {
nodes: nodes.map(node => ({
@@ -187,6 +200,7 @@ export class N8NDocumentationMCPServer {
}
private getNodeInfo(nodeType: string): any {
if (!this.repository) throw new Error('Repository not initialized');
let node = this.repository.getNode(nodeType);
if (!node) {
@@ -199,7 +213,7 @@ export class N8NDocumentationMCPServer {
];
for (const alt of alternatives) {
const found = this.repository.getNode(alt);
const found = this.repository!.getNode(alt);
if (found) {
node = found;
break;
@@ -215,9 +229,10 @@ export class N8NDocumentationMCPServer {
}
private searchNodes(query: string, limit: number = 20): any {
if (!this.db) throw new Error('Database not initialized');
// Simple search across multiple fields
const searchQuery = `%${query}%`;
const nodes = this.db.prepare(`
const nodes = this.db!.prepare(`
SELECT * FROM nodes
WHERE node_type LIKE ?
OR display_name LIKE ?
@@ -259,6 +274,7 @@ export class N8NDocumentationMCPServer {
}
private listAITools(): any {
if (!this.repository) throw new Error('Repository not initialized');
const tools = this.repository.getAITools();
return {
@@ -272,7 +288,8 @@ export class N8NDocumentationMCPServer {
}
private getNodeDocumentation(nodeType: string): any {
const node = this.db.prepare(`
if (!this.db) throw new Error('Database not initialized');
const node = this.db!.prepare(`
SELECT node_type, display_name, documentation
FROM nodes
WHERE node_type = ?
@@ -291,7 +308,8 @@ export class N8NDocumentationMCPServer {
}
private getDatabaseStatistics(): any {
const stats = this.db.prepare(`
if (!this.db) throw new Error('Database not initialized');
const stats = this.db!.prepare(`
SELECT
COUNT(*) as total,
SUM(is_ai_tool) as ai_tools,
@@ -303,7 +321,7 @@ export class N8NDocumentationMCPServer {
FROM nodes
`).get() as any;
const packages = this.db.prepare(`
const packages = this.db!.prepare(`
SELECT package_name, COUNT(*) as count
FROM nodes
GROUP BY package_name
@@ -328,6 +346,9 @@ export class N8NDocumentationMCPServer {
}
async run(): Promise<void> {
// Ensure database is initialized before starting server
await this.ensureInitialized();
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('n8n Documentation MCP Server running on stdio transport');

View File

@@ -416,7 +416,7 @@ export class N8NMCPServer {
private async getNodeStatistics(args: any): Promise<any> {
try {
logger.info(`Getting node statistics`);
const stats = this.nodeDocService.getStatistics();
const stats = await this.nodeDocService.getStatistics();
return {
...stats,

View File

@@ -4,7 +4,6 @@ import {
INodeType,
INodeTypeDescription,
NodeOperationError,
NodeConnectionType,
} from 'n8n-workflow';
import { MCPClient } from '../utils/mcp-client';
import { N8NMCPBridge } from '../utils/bridge';
@@ -20,8 +19,8 @@ export class MCPNode implements INodeType {
defaults: {
name: 'MCP',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mcpApi',

View File

@@ -41,7 +41,7 @@ async function rebuildDocumentationDatabase() {
}
// Get and display statistics
const stats = service.getStatistics();
const stats = await service.getStatistics();
console.log('\n📈 Database Statistics:');
console.log(` Total nodes: ${stats.totalNodes}`);
console.log(` Nodes with documentation: ${stats.nodesWithDocs}`);
@@ -56,7 +56,7 @@ async function rebuildDocumentationDatabase() {
});
// Close database connection
service.close();
await service.close();
console.log('\n✨ Enhanced documentation database is ready!');
console.log('💡 The database now includes:');

View File

@@ -3,7 +3,7 @@
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
* Licensed under the Sustainable Use License v1.0
*/
import Database from 'better-sqlite3';
import { createDatabaseAdapter } from '../database/database-adapter';
import { N8nNodeLoader } from '../loaders/node-loader';
import { NodeParser } from '../parsers/node-parser';
import { DocsMapper } from '../mappers/docs-mapper';
@@ -14,7 +14,7 @@ import * as path from 'path';
async function rebuild() {
console.log('🔄 Rebuilding n8n node database...\n');
const db = new Database('./data/nodes.db');
const db = await createDatabaseAdapter('./data/nodes.db');
const loader = new N8nNodeLoader();
const parser = new NodeParser();
const mapper = new DocsMapper();

View File

@@ -3,7 +3,7 @@
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
* Licensed under the Sustainable Use License v1.0
*/
import Database from 'better-sqlite3';
import { createDatabaseAdapter } from '../database/database-adapter';
import { NodeRepository } from '../database/node-repository';
const TEST_CASES = [
@@ -34,7 +34,7 @@ const TEST_CASES = [
];
async function runTests() {
const db = new Database('./data/nodes.db');
const db = await createDatabaseAdapter('./data/nodes.db');
const repository = new NodeRepository(db);
console.log('🧪 Running node tests...\n');

View File

@@ -3,7 +3,7 @@
* Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
* Licensed under the Sustainable Use License v1.0
*/
import Database from 'better-sqlite3';
import { createDatabaseAdapter } from '../database/database-adapter';
interface NodeRow {
node_type: string;
@@ -25,7 +25,7 @@ interface NodeRow {
}
async function validate() {
const db = new Database('./data/nodes.db');
const db = await createDatabaseAdapter('./data/nodes.db');
console.log('🔍 Validating critical nodes...\n');

View File

@@ -1,4 +1,3 @@
import Database from 'better-sqlite3';
import { createHash } from 'crypto';
import path from 'path';
import { promises as fs } from 'fs';
@@ -14,6 +13,7 @@ import {
RelatedResource
} from '../utils/enhanced-documentation-fetcher';
import { ExampleGenerator } from '../utils/example-generator';
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
interface NodeInfo {
nodeType: string;
@@ -57,30 +57,51 @@ interface SearchOptions {
}
export class NodeDocumentationService {
private db: Database.Database;
private db: DatabaseAdapter | null = null;
private extractor: NodeSourceExtractor;
private docsFetcher: EnhancedDocumentationFetcher;
private dbPath: string;
private initialized: Promise<void>;
constructor(dbPath?: string) {
const databasePath = dbPath || process.env.NODE_DB_PATH || path.join(process.cwd(), 'data', 'nodes-v2.db');
this.dbPath = dbPath || process.env.NODE_DB_PATH || path.join(process.cwd(), 'data', 'nodes-v2.db');
// Ensure directory exists
const dbDir = path.dirname(databasePath);
const dbDir = path.dirname(this.dbPath);
if (!require('fs').existsSync(dbDir)) {
require('fs').mkdirSync(dbDir, { recursive: true });
}
this.db = new Database(databasePath);
this.extractor = new NodeSourceExtractor();
this.docsFetcher = new EnhancedDocumentationFetcher();
// Initialize database with new schema
this.initializeDatabase();
logger.info('Node Documentation Service initialized');
// Initialize database asynchronously
this.initialized = this.initializeAsync();
}
private async initializeAsync(): Promise<void> {
try {
this.db = await createDatabaseAdapter(this.dbPath);
// Initialize database with new schema
this.initializeDatabase();
logger.info('Node Documentation Service initialized');
} catch (error) {
logger.error('Failed to initialize database adapter', error);
throw error;
}
}
private async ensureInitialized(): Promise<void> {
await this.initialized;
if (!this.db) {
throw new Error('Database not initialized');
}
}
private initializeDatabase(): void {
if (!this.db) throw new Error('Database not initialized');
// Execute the schema directly
const schema = `
-- Main nodes table with documentation and examples
@@ -193,16 +214,17 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
);
`;
this.db.exec(schema);
this.db!.exec(schema);
}
/**
* Store complete node information including docs and examples
*/
async storeNode(nodeInfo: NodeInfo): Promise<void> {
await this.ensureInitialized();
const hash = this.generateHash(nodeInfo.sourceCode);
const stmt = this.db.prepare(`
const stmt = this.db!.prepare(`
INSERT OR REPLACE INTO nodes (
node_type, name, display_name, description, category, subcategory, icon,
source_code, credential_code, code_hash, code_length,
@@ -260,7 +282,8 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
* Get complete node information
*/
async getNodeInfo(nodeType: string): Promise<NodeInfo | null> {
const stmt = this.db.prepare(`
await this.ensureInitialized();
const stmt = this.db!.prepare(`
SELECT * FROM nodes WHERE node_type = ? OR name = ? COLLATE NOCASE
`);
@@ -274,6 +297,7 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
* Search nodes with various filters
*/
async searchNodes(options: SearchOptions): Promise<NodeInfo[]> {
await this.ensureInitialized();
let query = 'SELECT * FROM nodes WHERE 1=1';
const params: any = {};
@@ -313,7 +337,7 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
query += ' ORDER BY name LIMIT @limit';
params.limit = options.limit || 20;
const stmt = this.db.prepare(query);
const stmt = this.db!.prepare(query);
const rows = stmt.all(params);
return rows.map(row => this.rowToNodeInfo(row));
@@ -323,7 +347,8 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
* List all nodes
*/
async listNodes(): Promise<NodeInfo[]> {
const stmt = this.db.prepare('SELECT * FROM nodes ORDER BY name');
await this.ensureInitialized();
const stmt = this.db!.prepare('SELECT * FROM nodes ORDER BY name');
const rows = stmt.all();
return rows.map(row => this.rowToNodeInfo(row));
}
@@ -337,11 +362,12 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
failed: number;
errors: string[];
}> {
await this.ensureInitialized();
logger.info('Starting complete database rebuild...');
// Clear existing data
this.db.exec('DELETE FROM nodes');
this.db.exec('DELETE FROM extraction_stats');
this.db!.exec('DELETE FROM nodes');
this.db!.exec('DELETE FROM extraction_stats');
// Ensure documentation repository is available
await this.docsFetcher.ensureDocsRepository();
@@ -581,6 +607,7 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
* Store extraction statistics
*/
private storeStatistics(stats: any): void {
if (!this.db) throw new Error('Database not initialized');
const stmt = this.db.prepare(`
INSERT INTO extraction_stats (
total_nodes, nodes_with_docs, nodes_with_examples,
@@ -589,7 +616,7 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
`);
// Calculate sizes
const sizeStats = this.db.prepare(`
const sizeStats = this.db!.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN documentation_markdown IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
@@ -611,8 +638,9 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
/**
* Get database statistics
*/
getStatistics(): any {
const stats = this.db.prepare(`
async getStatistics(): Promise<any> {
await this.ensureInitialized();
const stats = this.db!.prepare(`
SELECT
COUNT(*) as totalNodes,
COUNT(DISTINCT package_name) as totalPackages,
@@ -625,7 +653,7 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
FROM nodes
`).get() as any;
const packages = this.db.prepare(`
const packages = this.db!.prepare(`
SELECT package_name as package, COUNT(*) as count
FROM nodes
GROUP BY package_name
@@ -648,7 +676,8 @@ CREATE TABLE IF NOT EXISTS extraction_stats (
/**
* Close database connection
*/
close(): void {
this.db.close();
async close(): Promise<void> {
await this.ensureInitialized();
this.db!.close();
}
}