Skip to main content
Glama
DocumentManager.js17 kB
/** * Document Manager - SQLite 기반 문서 관리 클래스 * 프로젝트 문서를 체계적으로 저장, 검색, 관리 */ import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export class DocumentManager { constructor() { this.dbPath = path.resolve(__dirname, '../../data/workflow.db'); this.db = null; } async getDatabase() { if (!this.db) { this.db = await open({ filename: this.dbPath, driver: sqlite3.Database }); await this.db.exec('PRAGMA foreign_keys = ON'); } return this.db; } /** * 새로운 문서 생성 * @param {Object} documentData - 문서 데이터 * @returns {Object} 생성된 문서 정보 */ async createDocument(documentData) { const { title, content, doc_type, category, tags, summary, linked_entity_type, linked_entity_id, link_type } = documentData; const db = await this.getDatabase(); // Insert document const result = await db.run(` INSERT INTO documents (title, content, doc_type, category, tags, summary, created_by) VALUES (?, ?, ?, ?, ?, ?, 'mcp-user') `, [ title, content, doc_type, category || null, tags ? JSON.stringify(tags) : null, summary || null ]); let documentId = result.lastInsertRowid; // SQLite lastInsertRowid가 undefined인 경우 직접 조회 if (!documentId) { const lastDoc = await db.get('SELECT last_insert_rowid() as id'); documentId = lastDoc.id; } if (!documentId) { await db.close(); throw new Error('Failed to get document ID after creation'); } // Link to entity if specified if (linked_entity_type && linked_entity_id) { await db.run(` INSERT INTO document_links (document_id, linked_entity_type, linked_entity_id, link_type) VALUES (?, ?, ?, ?) `, [documentId, linked_entity_type, linked_entity_id, link_type || 'notes']); } return { success: true, id: documentId, title, doc_type, category: category || null, tags: tags || [], linked: linked_entity_type ? { type: linked_entity_type, id: linked_entity_id, link_type: link_type || 'notes' } : null }; } /** * 문서 검색 (Full-text search) * @param {Object} searchParams - 검색 조건 * @returns {Array} 검색된 문서 목록 */ async searchDocuments(searchParams) { const { query, doc_type, category, limit = 10 } = searchParams; const db = await this.getDatabase(); let sql = ` SELECT d.id, d.title, d.doc_type, d.category, d.summary, d.created_at, snippet(documents_fts, 1, '<mark>', '</mark>', '...', 32) as snippet FROM documents d JOIN documents_fts ON d.id = documents_fts.rowid WHERE documents_fts MATCH ? `; const params = [query]; if (doc_type) { sql += ' AND d.doc_type = ?'; params.push(doc_type); } if (category) { sql += ' AND d.category = ?'; params.push(category); } sql += ' ORDER BY documents_fts.bm25 LIMIT ?'; params.push(limit); const results = await db.all(sql, params); return results; } /** * 문서 조회 (ID로) * @param {number} id - 문서 ID * @returns {Object} 문서 정보 */ async getDocument(id) { const db = await this.getDatabase(); const doc = await db.get(` SELECT d.*, GROUP_CONCAT( dl.linked_entity_type || ':' || dl.linked_entity_id || ':' || dl.link_type, '|' ) as links FROM documents d LEFT JOIN document_links dl ON d.id = dl.document_id WHERE d.id = ? GROUP BY d.id `, [id]); if (!doc) { return null; } const tags = doc.tags ? JSON.parse(doc.tags) : []; const links = doc.links ? doc.links.split('|').map(link => { const [type, id, linkType] = link.split(':'); return { type, id, linkType }; }) : []; return { ...doc, tags, links }; } /** * 문서 업데이트 * @param {number} id - 문서 ID * @param {Object} updates - 업데이트할 데이터 * @returns {Object} 업데이트 결과 */ async updateDocument(id, updates) { const db = await this.getDatabase(); // Check if document exists const existing = await db.get('SELECT id FROM documents WHERE id = ?', [id]); if (!existing) { throw new Error(`Document ID ${id} not found`); } // Build update query dynamically const updateFields = []; const params = []; if (updates.title !== undefined) { updateFields.push('title = ?'); params.push(updates.title); } if (updates.content !== undefined) { updateFields.push('content = ?'); params.push(updates.content); } if (updates.summary !== undefined) { updateFields.push('summary = ?'); params.push(updates.summary); } if (updates.status !== undefined) { updateFields.push('status = ?'); params.push(updates.status); } if (updates.tags !== undefined) { updateFields.push('tags = ?'); params.push(JSON.stringify(updates.tags)); } updateFields.push('updated_at = CURRENT_TIMESTAMP'); updateFields.push('version = version + 1'); params.push(id); const sql = `UPDATE documents SET ${updateFields.join(', ')} WHERE id = ?`; await db.run(sql, params); return { success: true, id, updated_fields: Object.keys(updates) }; } /** * 문서 삭제 * @param {number} id - 문서 ID * @returns {Object} 삭제 결과 */ async deleteDocument(id) { const db = await this.getDatabase(); const doc = await db.get('SELECT title FROM documents WHERE id = ?', [id]); if (!doc) { throw new Error(`Document ID ${id} not found`); } await db.run('DELETE FROM documents WHERE id = ?', [id]); return { success: true, id, title: doc.title }; } /** * 문서 목록 조회 * @param {Object} filters - 필터 조건 * @returns {Array} 문서 목록 */ async listDocuments(filters = {}) { const { doc_type, category, status, limit = 20 } = filters; const db = await this.getDatabase(); let sql = 'SELECT * FROM document_overview WHERE 1=1'; const params = []; if (doc_type) { sql += ' AND doc_type = ?'; params.push(doc_type); } if (category) { sql += ' AND category = ?'; params.push(category); } if (status) { sql += ' AND status = ?'; params.push(status); } sql += ' ORDER BY updated_at DESC LIMIT ?'; params.push(limit); const docs = await db.all(sql, params); return docs; } /** * 문서를 엔터티에 연결 * @param {number} documentId - 문서 ID * @param {string} entityType - 엔터티 유형 * @param {string} entityId - 엔터티 ID * @param {string} linkType - 연결 유형 * @returns {Object} 연결 결과 */ async linkDocument(documentId, entityType, entityId, linkType = 'notes') { const db = await this.getDatabase(); // Check if document exists const doc = await db.get('SELECT title FROM documents WHERE id = ?', [documentId]); if (!doc) { throw new Error(`Document ID ${documentId} not found`); } // Insert link const result = await db.run(` INSERT OR IGNORE INTO document_links (document_id, linked_entity_type, linked_entity_id, link_type) VALUES (?, ?, ?, ?) `, [documentId, entityType, entityId, linkType]); return { success: true, document_id: documentId, document_title: doc.title, entity_type: entityType, entity_id: entityId, link_type: linkType, is_new: result.changes > 0 }; } /** * 문서의 연결 관계 조회 * @param {number} documentId - 문서 ID * @returns {Object} 연결 관계 정보 */ async getDocumentLinks(documentId) { const db = await this.getDatabase(); const doc = await db.get('SELECT title FROM documents WHERE id = ?', [documentId]); if (!doc) { throw new Error(`Document ID ${documentId} not found`); } const links = await db.all(` SELECT linked_entity_type, linked_entity_id, link_type, created_at FROM document_links WHERE document_id = ? ORDER BY created_at DESC `, [documentId]); return { document_id: documentId, document_title: doc.title, total_links: links.length, links }; } /** * 문서 간 관계 생성 (document_relations 테이블 사용) * @param {number} parentDocId - 부모 문서 ID * @param {number} childDocId - 자식 문서 ID * @param {string} relationType - 관계 유형 (referenced_by, contains, derived_from, replaces) * @param {string} notes - 관계에 대한 설명 * @returns {Object} 관계 생성 결과 */ async createDocumentRelation(parentDocId, childDocId, relationType = 'referenced_by', notes = null) { const db = await this.getDatabase(); // 두 문서가 모두 존재하는지 확인 const [parentDoc, childDoc] = await Promise.all([ db.get('SELECT id, title FROM documents WHERE id = ?', [parentDocId]), db.get('SELECT id, title FROM documents WHERE id = ?', [childDocId]) ]); if (!parentDoc) { throw new Error(`Parent document ID ${parentDocId} not found`); } if (!childDoc) { throw new Error(`Child document ID ${childDocId} not found`); } // 중복 관계 확인 const existingRelation = await db.get(` SELECT id FROM document_relations WHERE parent_doc_id = ? AND child_doc_id = ? AND relation_type = ? `, [parentDocId, childDocId, relationType]); if (existingRelation) { return { success: false, error: 'Document relation already exists', parent_doc: parentDoc, child_doc: childDoc, relation_type: relationType }; } // 관계 생성 const result = await db.run(` INSERT INTO document_relations (parent_doc_id, child_doc_id, relation_type, notes) VALUES (?, ?, ?, ?) `, [parentDocId, childDocId, relationType, notes]); return { success: true, relation_id: result.lastInsertRowid, parent_doc: parentDoc, child_doc: childDoc, relation_type: relationType, notes: notes, message: `문서 관계 생성: "${parentDoc.title}" ${relationType} "${childDoc.title}"` }; } /** * 문서의 관계 조회 (부모-자식 관계) * @param {number} documentId - 문서 ID * @returns {Object} 문서 관계 정보 */ async getDocumentRelations(documentId) { const db = await this.getDatabase(); const doc = await db.get('SELECT id, title FROM documents WHERE id = ?', [documentId]); if (!doc) { throw new Error(`Document ID ${documentId} not found`); } // 부모 관계 (이 문서가 자식인 경우) const parentRelations = await db.all(` SELECT dr.relation_type, dr.notes, dr.created_at, d.id as parent_id, d.title as parent_title FROM document_relations dr JOIN documents d ON dr.parent_doc_id = d.id WHERE dr.child_doc_id = ? ORDER BY dr.created_at DESC `, [documentId]); // 자식 관계 (이 문서가 부모인 경우) const childRelations = await db.all(` SELECT dr.relation_type, dr.notes, dr.created_at, d.id as child_id, d.title as child_title FROM document_relations dr JOIN documents d ON dr.child_doc_id = d.id WHERE dr.parent_doc_id = ? ORDER BY dr.created_at DESC `, [documentId]); return { document_id: documentId, document_title: doc.title, parent_relations: parentRelations, child_relations: childRelations, total_relations: parentRelations.length + childRelations.length }; } /** * 문서 간 관계 삭제 * @param {number} parentDocId - 부모 문서 ID * @param {number} childDocId - 자식 문서 ID * @param {string} relationType - 관계 유형 * @returns {Object} 삭제 결과 */ async removeDocumentRelation(parentDocId, childDocId, relationType) { const db = await this.getDatabase(); // 관계 존재 확인 const relation = await db.get(` SELECT dr.id, p.title as parent_title, c.title as child_title FROM document_relations dr JOIN documents p ON dr.parent_doc_id = p.id JOIN documents c ON dr.child_doc_id = c.id WHERE dr.parent_doc_id = ? AND dr.child_doc_id = ? AND dr.relation_type = ? `, [parentDocId, childDocId, relationType]); if (!relation) { return { success: false, error: 'Document relation not found', parent_doc_id: parentDocId, child_doc_id: childDocId, relation_type: relationType }; } // 관계 삭제 await db.run(` DELETE FROM document_relations WHERE parent_doc_id = ? AND child_doc_id = ? AND relation_type = ? `, [parentDocId, childDocId, relationType]); return { success: true, parent_title: relation.parent_title, child_title: relation.child_title, relation_type: relationType, message: `문서 관계 삭제: "${relation.parent_title}" ${relationType} "${relation.child_title}"` }; } /** * 문서 링크 제거 (document_links 테이블) * @param {number} documentId - 문서 ID * @param {string} entityType - 엔터티 유형 * @param {string} entityId - 엔터티 ID * @returns {Object} 삭제 결과 */ async removeDocumentLink(documentId, entityType, entityId) { const db = await this.getDatabase(); // 링크 존재 확인 const link = await db.get(` SELECT dl.id, d.title as document_title FROM document_links dl JOIN documents d ON dl.document_id = d.id WHERE dl.document_id = ? AND dl.linked_entity_type = ? AND dl.linked_entity_id = ? `, [documentId, entityType, entityId]); if (!link) { return { success: false, error: 'Document link not found', document_id: documentId, entity_type: entityType, entity_id: entityId }; } // 링크 삭제 await db.run(` DELETE FROM document_links WHERE document_id = ? AND linked_entity_type = ? AND linked_entity_id = ? `, [documentId, entityType, entityId]); return { success: true, document_title: link.document_title, entity_type: entityType, entity_id: entityId, message: `문서 링크 삭제: "${link.document_title}" -> ${entityType}:${entityId}` }; } /** * 문서 분류 정보 조회 (doc_type, category, tags) * @returns {Object} 분류 정보 */ async getDocumentCategories() { console.log('🔍 DocumentManager.getDocumentCategories() called - UPDATED VERSION'); const db = await this.getDatabase(); const [docTypes, categories, allTags] = await Promise.all([ // 사용된 doc_type 목록 db.all(` SELECT doc_type, COUNT(*) as count FROM documents WHERE doc_type IS NOT NULL GROUP BY doc_type ORDER BY count DESC, doc_type `), // 사용된 category 목록 db.all(` SELECT category, COUNT(*) as count FROM documents WHERE category IS NOT NULL GROUP BY category ORDER BY count DESC, category `), // 사용된 tags 목록 db.all(` SELECT tags FROM documents WHERE tags IS NOT NULL AND tags != '[]' `) ]); // tags 추출 및 집계 const tagsMap = new Map(); allTags.forEach(row => { try { const tags = JSON.parse(row.tags || '[]'); tags.forEach(tag => { tagsMap.set(tag, (tagsMap.get(tag) || 0) + 1); }); } catch (e) { // JSON 파싱 에러 무시 } }); const tags = Array.from(tagsMap.entries()) .map(([tag, count]) => ({ tag, count })) .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); const result = { success: true, doc_types: docTypes.map(row => ({ value: row.doc_type, count: row.count })), categories: categories.map(row => ({ value: row.category, count: row.count })), tags: tags.map(item => ({ value: item.tag, count: item.count })), total_documents: docTypes.reduce((sum, row) => sum + row.count, 0) }; console.log('📊 DocumentManager result:', { doc_types_count: result.doc_types.length, categories_count: result.categories.length, first_doc_type: result.doc_types[0], first_category: result.categories[0], total: result.total_documents }); return result; } } export default DocumentManager;

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