Skip to main content
Glama
SimpleSQLiteStorage.js13.3 kB
/** * Simple SQLite Storage - 기본 sqlite3 패키지 사용 * Phase 2.5: JSON 파일 저장소를 SQLite로 전환 * Windows 환경에서도 안정적으로 작동하는 간단한 구현 */ import sqlite3 from 'sqlite3'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { promisify } from 'util'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class SimpleSQLiteStorage { constructor() { this.dbPath = path.join(__dirname, '../../data/workflow.db'); this.schemaPath = path.join(__dirname, 'schema.sql'); this.db = null; this.initialized = false; } /** * 데이터베이스 초기화 */ async initialize() { return new Promise(async (resolve, reject) => { try { // 데이터 디렉토리 생성 const dataDir = path.dirname(this.dbPath); await fs.mkdir(dataDir, { recursive: true }); // SQLite 데이터베이스 연결 this.db = new sqlite3.Database(this.dbPath, (err) => { if (err) { reject(new Error(`Failed to connect to SQLite: ${err.message}`)); return; } }); // 프라미스 헬퍼 메서드 추가 this.db.runAsync = promisify(this.db.run.bind(this.db)); this.db.getAsync = promisify(this.db.get.bind(this.db)); this.db.allAsync = promisify(this.db.all.bind(this.db)); // 스키마 적용 await this.applySchema(); this.initialized = true; console.log(`SimpleSQLiteStorage initialized at ${this.dbPath}`); resolve(); } catch (error) { reject(new Error(`Failed to initialize SimpleSQLiteStorage: ${error.message}`)); } }); } /** * 데이터베이스 스키마 적용 */ async applySchema() { try { const schemaSQL = await fs.readFile(this.schemaPath, 'utf8'); // 간단한 테이블만 생성 (복잡한 스키마는 나중에 적용) const basicSchema = ` PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS prds ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, requirements TEXT, priority TEXT DEFAULT 'Medium', status TEXT DEFAULT 'draft', created_at TEXT NOT NULL, updated_at TEXT ); CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'pending', priority TEXT DEFAULT 'Medium', assignee TEXT, estimated_hours INTEGER, due_date TEXT, created_at TEXT NOT NULL, updated_at TEXT, plan_id TEXT ); CREATE TABLE IF NOT EXISTS plans ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'active', start_date TEXT, end_date TEXT, created_at TEXT NOT NULL, updated_at TEXT, prd_id TEXT ); CREATE TABLE IF NOT EXISTS task_dependencies ( id INTEGER PRIMARY KEY AUTOINCREMENT, dependent_task_id TEXT NOT NULL, prerequisite_task_id TEXT NOT NULL, created_at TEXT NOT NULL ); `; // 스키마를 개별 명령으로 분할하여 실행 const statements = basicSchema .split(';') .map(stmt => stmt.trim()) .filter(stmt => stmt.length > 0 && !stmt.startsWith('--')); for (const statement of statements) { try { await this.db.runAsync(statement); } catch (error) { console.warn(`Schema statement warning: ${error.message}`); } } console.log('Basic database schema applied successfully'); } catch (error) { throw new Error(`Failed to apply schema: ${error.message}`); } } /** * 연결 확인 */ ensureInitialized() { if (!this.initialized || !this.db) { throw new Error('SimpleSQLiteStorage not initialized. Call initialize() first.'); } } // ============================================= // PRD Operations // ============================================= /** * PRD 저장 */ async savePRD(prd) { this.ensureInitialized(); const sql = ` INSERT OR REPLACE INTO prds ( id, title, description, requirements, priority, status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; return await this.db.runAsync( sql, prd.id, prd.title, prd.description, JSON.stringify(prd.requirements || []), prd.priority || 'Medium', prd.status || 'draft', prd.createdAt, prd.updatedAt || prd.createdAt ); } /** * PRD 조회 */ async getPRD(id) { this.ensureInitialized(); const sql = 'SELECT * FROM prds WHERE id = ?'; const row = await this.db.getAsync(sql, id); if (!row) return null; return this.formatPRDRow(row); } /** * 모든 PRD 목록 조회 */ async listAllPRDs() { this.ensureInitialized(); const sql = 'SELECT * FROM prds ORDER BY created_at DESC'; const rows = await this.db.allAsync(sql); return rows.map(row => this.formatPRDRow(row)); } /** * PRD 삭제 */ async deletePRD(id) { this.ensureInitialized(); const sql = 'DELETE FROM prds WHERE id = ?'; return await this.db.runAsync(sql, id); } /** * PRD 포맷팅 */ formatPRDRow(row) { return { id: row.id, title: row.title, description: row.description, requirements: JSON.parse(row.requirements || '[]'), priority: row.priority, status: row.status, createdAt: row.created_at, updatedAt: row.updated_at }; } // ============================================= // Task Operations // ============================================= /** * Task 저장 */ async saveTask(task) { this.ensureInitialized(); const sql = ` INSERT OR REPLACE INTO tasks ( id, title, description, status, priority, assignee, estimated_hours, due_date, created_at, updated_at, plan_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; return await this.db.runAsync( sql, task.id, task.title, task.description, task.status || 'pending', task.priority || 'Medium', task.assignee || null, task.estimatedHours || null, task.dueDate || null, task.createdAt, task.updatedAt || task.createdAt, task.plan_id || null ); } /** * Task 조회 */ async getTask(id) { this.ensureInitialized(); const sql = 'SELECT * FROM tasks WHERE id = ?'; const row = await this.db.getAsync(sql, id); if (!row) return null; return await this.formatTaskRow(row); } /** * 모든 Task 목록 조회 */ async listAllTasks() { this.ensureInitialized(); const sql = 'SELECT * FROM tasks ORDER BY created_at DESC'; const rows = await this.db.allAsync(sql); const tasks = []; for (const row of rows) { tasks.push(await this.formatTaskRow(row)); } return tasks; } /** * Task 삭제 */ async deleteTask(id) { this.ensureInitialized(); // 의존성 관계도 함께 삭제 await this.db.runAsync('DELETE FROM task_dependencies WHERE dependent_task_id = ? OR prerequisite_task_id = ?', id, id); const sql = 'DELETE FROM tasks WHERE id = ?'; return await this.db.runAsync(sql, id); } /** * Task 포맷팅 */ async formatTaskRow(row) { return { id: row.id, title: row.title, description: row.description, status: row.status, priority: row.priority, assignee: row.assignee, estimatedHours: row.estimated_hours, dueDate: row.due_date, createdAt: row.created_at, updatedAt: row.updated_at, plan_id: row.plan_id, // 의존성 배열 추가 dependencies: await this.getTaskDependencies(row.id) }; } // ============================================= // Plan Operations // ============================================= /** * Plan 저장 */ async savePlan(plan) { this.ensureInitialized(); const sql = ` INSERT OR REPLACE INTO plans ( id, title, description, status, start_date, end_date, created_at, updated_at, prd_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `; return await this.db.runAsync( sql, plan.id, plan.title, plan.description, plan.status || 'active', plan.startDate || null, plan.endDate || null, plan.createdAt, plan.updatedAt || plan.createdAt, plan.prd_id || null ); } /** * Plan 조회 */ async getPlan(id) { this.ensureInitialized(); const sql = 'SELECT * FROM plans WHERE id = ?'; const row = await this.db.getAsync(sql, id); if (!row) return null; return await this.formatPlanRow(row); } /** * 모든 Plan 목록 조회 */ async listAllPlans() { this.ensureInitialized(); const sql = 'SELECT * FROM plans ORDER BY created_at DESC'; const rows = await this.db.allAsync(sql); const plans = []; for (const row of rows) { plans.push(await this.formatPlanRow(row)); } return plans; } /** * Plan 삭제 */ async deletePlan(id) { this.ensureInitialized(); const sql = 'DELETE FROM plans WHERE id = ?'; return await this.db.runAsync(sql, id); } /** * Plan 포맷팅 */ async formatPlanRow(row) { // 연결된 Task들 찾기 const linkedTasks = await this.db.allAsync('SELECT id FROM tasks WHERE plan_id = ?', row.id); return { id: row.id, title: row.title, description: row.description, status: row.status, startDate: row.start_date, endDate: row.end_date, createdAt: row.created_at, updatedAt: row.updated_at, prd_id: row.prd_id, // Phase 2 호환성을 위한 추가 필드 linked_task_ids: linkedTasks.map(t => t.id), progress: await this.calculatePlanProgress(row.id) }; } // ============================================= // Helper Methods // ============================================= /** * Task의 의존성 목록 조회 */ async getTaskDependencies(taskId) { this.ensureInitialized(); const sql = 'SELECT prerequisite_task_id FROM task_dependencies WHERE dependent_task_id = ?'; const rows = await this.db.allAsync(sql, taskId); return rows.map(row => row.prerequisite_task_id); } /** * Task 의존성 추가 */ async addTaskDependency(dependentTaskId, prerequisiteTaskId) { this.ensureInitialized(); const sql = ` INSERT INTO task_dependencies (dependent_task_id, prerequisite_task_id, created_at) VALUES (?, ?, ?) `; return await this.db.runAsync(sql, dependentTaskId, prerequisiteTaskId, new Date().toISOString()); } /** * Plan 진행률 계산 */ async calculatePlanProgress(planId) { this.ensureInitialized(); const totalTasksSql = 'SELECT COUNT(*) as count FROM tasks WHERE plan_id = ?'; const completedTasksSql = 'SELECT COUNT(*) as count FROM tasks WHERE plan_id = ? AND status = ?'; const totalResult = await this.db.getAsync(totalTasksSql, planId); const completedResult = await this.db.getAsync(completedTasksSql, planId, 'done'); const totalTasks = totalResult.count; const completedTasks = completedResult.count; return { percentage: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0, totalTasks, completedTasks, lastSyncAt: new Date().toISOString() }; } /** * 대시보드 통계 조회 */ async getDashboardStats() { this.ensureInitialized(); const prdCount = await this.db.getAsync('SELECT COUNT(*) as count FROM prds'); const taskCount = await this.db.getAsync('SELECT COUNT(*) as count FROM tasks'); const planCount = await this.db.getAsync('SELECT COUNT(*) as count FROM plans'); return { total_prds: prdCount.count, total_tasks: taskCount.count, total_plans: planCount.count, timestamp: new Date().toISOString() }; } /** * 데이터베이스 연결 정리 */ cleanup() { return new Promise((resolve) => { if (this.db) { this.db.close((err) => { if (err) { console.warn(`Database close warning: ${err.message}`); } this.db = null; this.initialized = false; resolve(); }); } else { resolve(); } }); } /** * 모든 테이블 목록 조회 (디버깅용) */ async getTables() { this.ensureInitialized(); const sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"; const rows = await this.db.allAsync(sql); return rows.map(row => row.name); } }

Latest Blog Posts

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/foswmine/workflow-mcp'

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