UNPKG

atlas-mcp-server

Version:

ATLAS (Adaptive Task & Logic Automation System): An MCP server enabling LLM agents to manage projects, tasks, and knowledge via a Neo4j-backed, three-tier architecture. Facilitates complex workflow automation and project management through LLM Agents.

127 lines (126 loc) 6.58 kB
/** * @fileoverview Implements the database export logic for Neo4j. * @module src/services/neo4j/backupRestoreService/exportLogic */ import { format } from "date-fns"; import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { logger, requestContextService } from "../../../utils/index.js"; import { neo4jDriver } from "../driver.js"; import { manageBackupRotation, secureResolve, validatedBackupRoot, } from "./backupUtils.js"; /** * Exports all Project, Task, and Knowledge nodes and relationships to JSON files. * Also creates a full-export.json file containing all data in a single file. * Manages backup rotation before creating the new backup. * @returns The path to the directory containing the backup files. * @throws Error if the export step fails. Rotation errors are logged but don't throw. */ export const _exportDatabase = async () => { const operationName = "_exportDatabase"; // Renamed const baseContext = requestContextService.createRequestContext({ operation: operationName, }); await manageBackupRotation(); let session = null; const timestamp = format(new Date(), "yyyyMMddHHmmss"); const backupDirName = `atlas-backup-${timestamp}`; const backupDir = secureResolve(validatedBackupRoot, backupDirName); if (!backupDir) { throw new Error(`Failed to create secure backup directory path for ${backupDirName} within ${validatedBackupRoot}`); } const fullExport = { nodes: {}, relationships: [], }; try { session = await neo4jDriver.getSession(); logger.info(`Starting database export to ${backupDir}...`, baseContext); if (!existsSync(backupDir)) { mkdirSync(backupDir, { recursive: true }); logger.debug(`Created backup directory: ${backupDir}`, baseContext); } logger.debug("Fetching all node labels from database...", baseContext); const labelsResult = await session.run("CALL db.labels() YIELD label RETURN label"); const nodeLabels = labelsResult.records.map((record) => record.get("label")); logger.info(`Found labels: ${nodeLabels.join(", ")}`, { ...baseContext, labels: nodeLabels, }); for (const label of nodeLabels) { logger.debug(`Exporting nodes with label: ${label}`, { ...baseContext, currentLabel: label, }); const escapedLabel = `\`${label.replace(/`/g, "``")}\``; const nodeResult = await session.run(`MATCH (n:${escapedLabel}) RETURN n`); const nodes = nodeResult.records.map((record) => record.get("n").properties); const fileName = `${label.toLowerCase()}s.json`; const filePath = secureResolve(backupDir, fileName); if (!filePath) { logger.error(`Skipping export for label ${label}: Could not create secure path for ${fileName} in ${backupDir}`, new Error("Secure path resolution failed"), { ...baseContext, label, fileName, targetDir: backupDir }); continue; } writeFileSync(filePath, JSON.stringify(nodes, null, 2)); logger.info(`Successfully exported ${nodes.length} ${label} nodes to ${filePath}`, { ...baseContext, label, count: nodes.length, filePath }); fullExport.nodes[label] = nodes; } logger.debug("Exporting relationships...", baseContext); const relResult = await session.run(` MATCH (start)-[r]->(end) WHERE start.id IS NOT NULL AND end.id IS NOT NULL RETURN start.id as startNodeAppId, end.id as endNodeAppId, type(r) as relType, properties(r) as relProps `); const relationships = relResult.records.map((record) => ({ startNodeId: record.get("startNodeAppId"), endNodeId: record.get("endNodeAppId"), type: record.get("relType"), properties: record.get("relProps") || {}, })); const relFileName = "relationships.json"; const relFilePath = secureResolve(backupDir, relFileName); if (!relFilePath) { throw new Error(`Failed to create secure path for ${relFileName} in ${backupDir}`); } writeFileSync(relFilePath, JSON.stringify(relationships, null, 2)); logger.info(`Successfully exported ${relationships.length} relationships to ${relFilePath}`, { ...baseContext, count: relationships.length, filePath: relFilePath }); fullExport.relationships = relationships; const fullExportFileName = "full-export.json"; const fullExportPath = secureResolve(backupDir, fullExportFileName); if (!fullExportPath) { throw new Error(`Failed to create secure path for ${fullExportFileName} in ${backupDir}`); } writeFileSync(fullExportPath, JSON.stringify(fullExport, null, 2)); logger.info(`Successfully created full database export to ${fullExportPath}`, { ...baseContext, filePath: fullExportPath }); logger.info(`Database export completed successfully to ${backupDir}`, baseContext); return backupDir; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Database export failed: ${errorMessage}`, error, baseContext); if (backupDir && existsSync(backupDir)) { if (!backupDir.startsWith(validatedBackupRoot + require("path").sep)) { // Use require("path") for sep logger.error(`Security Error: Attempting cleanup of directory outside backup root: ${backupDir}. Aborting cleanup.`, new Error("Cleanup security violation"), { ...baseContext, cleanupDir: backupDir }); } else { try { rmSync(backupDir, { recursive: true, force: true }); logger.warning(`Removed partially created backup directory due to export failure: ${backupDir}`, { ...baseContext, cleanupDir: backupDir }); } catch (rmError) { const rmErrorMsg = rmError instanceof Error ? rmError.message : String(rmError); logger.error(`Failed to remove partial backup directory ${backupDir}: ${rmErrorMsg}`, rmError, { ...baseContext, cleanupDir: backupDir }); } } } throw new Error(`Database export failed: ${errorMessage}`); } finally { if (session) { await session.close(); } } };