Skip to main content
Glama

atlas-mcp-server

exportLogic.ts6.66 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 { Session } from "neo4j-driver"; import { logger, requestContextService } from "../../../utils/index.js"; import { neo4jDriver } from "../driver.js"; import { FullExport } from "./backupRestoreTypes.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 (): Promise<string> => { const operationName = "_exportDatabase"; // Renamed const baseContext = requestContextService.createRequestContext({ operation: operationName, }); await manageBackupRotation(); let session: Session | null = 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: 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: string[] = 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 as 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 as Error, { ...baseContext, cleanupDir: backupDir }, ); } } } throw new Error(`Database export failed: ${errorMessage}`); } finally { if (session) { await session.close(); } } };

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/cyanheads/atlas-mcp-server'

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