Skip to main content
Glama
joelmnz

Article Manager MCP Server

by joelmnz
databaseVersionHistory.ts12.4 kB
import { database } from './database.js'; import { createHash } from 'crypto'; import { databaseArticleService, Article } from './databaseArticles.js'; import { handleDatabaseError, DatabaseServiceError, DatabaseErrorType, logDatabaseError } from './databaseErrors.js'; import { databaseConstraintService } from './databaseConstraints.js'; // Database-specific version interfaces export interface DatabaseVersionHistory { id: number; articleId: number; versionId: number; title: string; content: string; folder: string; message?: string; contentHash: string; createdAt: Date; createdBy?: string; } // Maintain compatibility with existing VersionMetadata interface export interface VersionMetadata { versionId: number; createdAt: string; message?: string; hash: string; size: number; title: string; folder: string; } /** * Database-backed version history service */ export class DatabaseVersionHistoryService { /** * Calculate SHA256 hash of content */ private calculateContentHash(content: string): string { return createHash('sha256').update(content, 'utf-8').digest('hex'); } /** * Convert database row to VersionMetadata interface */ private dbRowToVersionMetadata(row: any): VersionMetadata { return { versionId: row.version_id, createdAt: row.created_at.toISOString(), message: row.message, hash: row.content_hash, size: Buffer.byteLength(row.content, 'utf-8'), title: row.title, folder: row.folder }; } /** * Get the next version ID for an article */ private async getNextVersionId(articleId: number): Promise<number> { const result = await database.query( 'SELECT COALESCE(MAX(version_id), 0) + 1 as next_version FROM article_history WHERE article_id = $1', [articleId] ); return result.rows[0].next_version; } /** * List all versions of an article */ async listVersions(articleId: number): Promise<VersionMetadata[]> { const result = await database.query( `SELECT version_id, title, content, folder, message, content_hash, created_at, created_by FROM article_history WHERE article_id = $1 ORDER BY version_id DESC`, [articleId] ); return result.rows.map(row => this.dbRowToVersionMetadata(row)); } /** * List versions by article slug */ async listVersionsBySlug(slug: string): Promise<VersionMetadata[]> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } return this.listVersions(articleId); } /** * Get a specific version of an article */ async getVersion(articleId: number, versionId: number): Promise<Article | null> { const result = await database.query( `SELECT ah.*, a.slug FROM article_history ah JOIN articles a ON ah.article_id = a.id WHERE ah.article_id = $1 AND ah.version_id = $2`, [articleId, versionId] ); if (result.rows.length === 0) { return null; } const row = result.rows[0]; return { slug: row.slug, title: row.title, content: row.content, folder: row.folder, created: row.created_at.toISOString(), isPublic: false // Version snapshots are not public }; } /** * Get version by article slug and version ID */ async getVersionBySlug(slug: string, versionId: number): Promise<Article | null> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } return this.getVersion(articleId, versionId); } /** * Create a new version entry */ async createVersion( articleId: number, title: string, content: string, folder: string, message?: string, createdBy?: string ): Promise<void> { try { // Validate version data using constraint service await databaseConstraintService.validateVersionData({ articleId, title, content, folder, message }); // Get next version ID const versionId = await getNextVersionId(articleId); // Calculate content hash const contentHash = this.calculateContentHash(content); const now = new Date(); await database.query( `INSERT INTO article_history (article_id, version_id, title, content, folder, message, content_hash, created_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [articleId, versionId, title, content, folder, message, contentHash, now, createdBy] ); } catch (error) { if (error instanceof DatabaseServiceError) { throw error; } const dbError = handleDatabaseError(error); logDatabaseError(dbError, 'Create Version'); throw dbError; } } /** * Create version by article slug */ async createVersionBySlug( slug: string, title: string, content: string, folder: string, message?: string, createdBy?: string ): Promise<void> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } await this.createVersion(articleId, title, content, folder, message, createdBy); } /** * Create version from current article state */ async createVersionFromCurrent(articleId: number, message?: string, createdBy?: string): Promise<void> { // Get current article state const article = await databaseArticleService.readArticleById(articleId); if (!article) { throw new Error(`Article with ID ${articleId} not found`); } await this.createVersion( articleId, article.title, article.content, article.folder, message, createdBy ); } /** * Create version from current article state by slug */ async createVersionFromCurrentBySlug(slug: string, message?: string, createdBy?: string): Promise<void> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } await this.createVersionFromCurrent(articleId, message, createdBy); } /** * Restore an article to a specific version */ async restoreVersion( articleId: number, versionId: number, message?: string, createdBy?: string ): Promise<Article> { // Get the version to restore const versionArticle = await this.getVersion(articleId, versionId); if (!versionArticle) { throw new Error(`Version ${versionId} not found for article ID ${articleId}`); } // Create a snapshot of current state before restoring await this.createVersionFromCurrent(articleId, message || `Restore to version ${versionId}`, createdBy); // Get current article to preserve slug and creation date const currentArticle = await databaseArticleService.readArticleById(articleId); if (!currentArticle) { throw new Error(`Article with ID ${articleId} not found`); } // Update the article with version content const restoredArticle = await databaseArticleService.updateArticle( currentArticle.slug, versionArticle.title, versionArticle.content, versionArticle.folder, message ); return restoredArticle; } /** * Restore version by article slug */ async restoreVersionBySlug( slug: string, versionId: number, message?: string, createdBy?: string ): Promise<Article> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } return this.restoreVersion(articleId, versionId, message, createdBy); } /** * Delete specific versions of an article */ async deleteVersions(articleId: number, versionIds: number[]): Promise<void> { if (versionIds.length === 0) { return; } // Create placeholders for the IN clause const placeholders = versionIds.map((_, index) => `$${index + 2}`).join(', '); const result = await database.query( `DELETE FROM article_history WHERE article_id = $1 AND version_id IN (${placeholders})`, [articleId, ...versionIds] ); console.log(`Deleted ${result.rowCount} versions for article ID ${articleId}`); } /** * Delete versions by article slug */ async deleteVersionsBySlug(slug: string, versionIds: number[]): Promise<void> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } await this.deleteVersions(articleId, versionIds); } /** * Delete all versions of an article */ async deleteAllVersions(articleId: number): Promise<void> { const result = await database.query( 'DELETE FROM article_history WHERE article_id = $1', [articleId] ); console.log(`Deleted ${result.rowCount} versions for article ID ${articleId}`); } /** * Delete all versions by article slug */ async deleteAllVersionsBySlug(slug: string): Promise<void> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } await this.deleteAllVersions(articleId); } /** * Get version statistics for an article */ async getVersionStats(articleId: number): Promise<{ totalVersions: number; oldestVersion: Date | null; newestVersion: Date | null; totalSize: number; }> { const result = await database.query( `SELECT COUNT(*) as total_versions, MIN(created_at) as oldest_version, MAX(created_at) as newest_version, SUM(LENGTH(content)) as total_size FROM article_history WHERE article_id = $1`, [articleId] ); const row = result.rows[0]; return { totalVersions: parseInt(row.total_versions, 10), oldestVersion: row.oldest_version, newestVersion: row.newest_version, totalSize: parseInt(row.total_size || '0', 10) }; } /** * Get version statistics by article slug */ async getVersionStatsBySlug(slug: string): Promise<{ totalVersions: number; oldestVersion: Date | null; newestVersion: Date | null; totalSize: number; }> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } return this.getVersionStats(articleId); } /** * Clean up old versions (keep only the most recent N versions) */ async cleanupOldVersions(articleId: number, keepCount: number): Promise<number> { if (keepCount <= 0) { throw new Error('Keep count must be greater than 0'); } // Get versions to delete (all except the most recent keepCount) const result = await database.query( `SELECT version_id FROM article_history WHERE article_id = $1 ORDER BY version_id DESC OFFSET $2`, [articleId, keepCount] ); if (result.rows.length === 0) { return 0; // No versions to delete } const versionIdsToDelete = result.rows.map(row => row.version_id); await this.deleteVersions(articleId, versionIdsToDelete); return versionIdsToDelete.length; } /** * Clean up old versions by article slug */ async cleanupOldVersionsBySlug(slug: string, keepCount: number): Promise<number> { const articleId = await databaseArticleService.getArticleId(slug); if (!articleId) { throw new Error(`Article with slug '${slug}' not found`); } return this.cleanupOldVersions(articleId, keepCount); } } // Fix the missing function reference async function getNextVersionId(articleId: number): Promise<number> { const result = await database.query( 'SELECT COALESCE(MAX(version_id), 0) + 1 as next_version FROM article_history WHERE article_id = $1', [articleId] ); return result.rows[0].next_version; } // Export singleton instance export const databaseVersionHistoryService = new DatabaseVersionHistoryService();

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/joelmnz/mcp-markdown-manager'

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