godcrm/backend/routes/v3/export-import.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

316 lines
9.1 KiB
JavaScript

/**
* API v3: Export/Import Routes
* ADR-020: Export/Import — Quick Backup & Restore
*
* Endpoints:
* - POST /tables/:tableId/export - Export single table
* - POST /projects/:projectId/export - Export project with tables
* - POST /spaces/:spaceId/export - Export entire space
* - POST /projects/:projectId/import/table - Import table to project
* - POST /spaces/:spaceId/import/project - Import project to space
* - POST /import/space - Import space as new
*/
/**
* @swagger
* components:
* schemas:
* ExportOptions:
* type: object
* properties:
* mode:
* type: string
* enum: [full, schema_only, sanitized]
* format:
* type: string
* enum: [json]
*/
import express from 'express';
import { ExportService } from '../../services/ExportService.js';
import { ImportService } from '../../services/ImportService.js';
import { apiLogger } from '../../utils/logger.js';
const router = express.Router();
// ============================================================
// Response Helpers
// ============================================================
const respondSuccess = (res, data, status = 200) =>
res.status(status).json({
success: true,
data,
timestamp: new Date().toISOString()
});
const respondError = (res, status, code, message) =>
res.status(status).json({
success: false,
error: { code, message },
timestamp: new Date().toISOString()
});
// ============================================================
// EXPORT ENDPOINTS
// ============================================================
/**
* POST /api/v3/tables/:tableId/export
* Export a single table
*
* Request Body:
* {
* "mode": "full" | "schema_only" | "sanitized",
* "format": "json" // MVP: только JSON
* }
*/
router.post('/tables/:tableId/export', async (req, res) => {
try {
const { tableId } = req.params;
const { mode = 'full' } = req.body;
const userId = req.user?.id;
// Check access
if (userId) {
const canExport = await ExportService.canExport(userId, 'table', tableId);
if (!canExport) {
return respondError(res, 403, 'ACCESS_DENIED', 'You do not have permission to export this table');
}
}
const exported = await ExportService.exportTable(tableId, { mode });
apiLogger.info({ tableId, mode, userId }, 'Table exported');
return respondSuccess(res, exported);
} catch (error) {
apiLogger.error({ err: error }, 'POST /tables/:tableId/export error');
return respondError(res, 500, 'EXPORT_FAILED', error.message);
}
});
/**
* POST /api/v3/tables/:tableId/sensitive-columns
* Get sensitive columns info for a table (for UI warning)
*/
router.post('/tables/:tableId/sensitive-columns', async (req, res) => {
try {
const { tableId } = req.params;
const sensitiveInfo = await ExportService.detectSensitiveColumns(tableId);
return respondSuccess(res, sensitiveInfo);
} catch (error) {
apiLogger.error({ err: error }, 'POST /tables/:tableId/sensitive-columns error');
return respondError(res, 500, 'DETECTION_FAILED', error.message);
}
});
/**
* POST /api/v3/projects/:projectId/export
* Export a project with selected tables
*
* Request Body:
* {
* "tables": {
* "customers": "full",
* "api_keys": "sanitized",
* "logs": "exclude"
* },
* "includeDocuments": true
* }
*/
router.post('/projects/:projectId/export', async (req, res) => {
try {
const { projectId } = req.params;
const { tables = {}, includeDocuments = true } = req.body;
const userId = req.user?.id;
// Check access
if (userId) {
const canExport = await ExportService.canExport(userId, 'project', projectId);
if (!canExport) {
return respondError(res, 403, 'ACCESS_DENIED', 'You do not have permission to export this project');
}
}
const exported = await ExportService.exportProject(projectId, {
tables,
includeDocuments
});
apiLogger.info({ projectId, tableCount: exported.tables.length, userId }, 'Project exported');
return respondSuccess(res, exported);
} catch (error) {
apiLogger.error({ err: error }, 'POST /projects/:projectId/export error');
return respondError(res, 500, 'EXPORT_FAILED', error.message);
}
});
/**
* POST /api/v3/spaces/:spaceId/export
* Export an entire space
*
* Request Body:
* {
* "projects": {
* "crm": { "tables": { "customers": "full" } },
* "analytics": { "tables": { "*": "full" } }
* },
* "includeSettings": true
* }
*/
router.post('/spaces/:spaceId/export', async (req, res) => {
try {
const { spaceId } = req.params;
const { projects = {}, includeSettings = true } = req.body;
const userId = req.user?.id;
const exported = await ExportService.exportSpace(spaceId, {
projects,
includeSettings
}, { userId });
apiLogger.info({ spaceId, projectCount: exported.projects.length, userId }, 'Space exported');
return respondSuccess(res, exported);
} catch (error) {
if (error.message.includes('Access denied')) {
return respondError(res, 403, 'ACCESS_DENIED', error.message);
}
apiLogger.error({ err: error }, 'POST /spaces/:spaceId/export error');
return respondError(res, 500, 'EXPORT_FAILED', error.message);
}
});
// ============================================================
// IMPORT ENDPOINTS
// ============================================================
/**
* POST /api/v3/projects/:projectId/import/table
* Import a table to a project
*
* Request Body:
* {
* "data": { ... exported table data ... },
* "mode": "create" | "replace",
* "newName": "customers_copy",
* "targetTableId": null // for replace mode
* }
*/
router.post('/projects/:projectId/import/table', async (req, res) => {
try {
const { projectId } = req.params;
const { data, mode = 'create', newName, targetTableId } = req.body;
const userId = req.user?.id;
if (!data) {
return respondError(res, 400, 'MISSING_DATA', 'Export data is required');
}
// Check access (user must be able to export to import)
if (userId) {
const canExport = await ExportService.canExport(userId, 'project', projectId);
if (!canExport) {
return respondError(res, 403, 'ACCESS_DENIED', 'You do not have permission to import to this project');
}
}
const result = await ImportService.importTable(projectId, data, {
mode,
newName,
targetTableId
});
apiLogger.info({ projectId, tableId: result.tableId, rowsImported: result.rowsImported, userId }, 'Table imported');
return respondSuccess(res, result, 201);
} catch (error) {
apiLogger.error({ err: error }, 'POST /projects/:projectId/import/table error');
return respondError(res, 500, 'IMPORT_FAILED', error.message);
}
});
/**
* POST /api/v3/spaces/:spaceId/import/project
* Import a project to a space
*
* Request Body:
* {
* "data": { ... exported project data ... },
* "mode": "create",
* "newName": "CRM Copy"
* }
*/
router.post('/spaces/:spaceId/import/project', async (req, res) => {
try {
const { spaceId } = req.params;
const { data, mode = 'create', newName } = req.body;
const userId = req.user?.id;
if (!data) {
return respondError(res, 400, 'MISSING_DATA', 'Export data is required');
}
// Check access
if (userId) {
const canExport = await ExportService.canExport(userId, 'space', spaceId);
if (!canExport) {
return respondError(res, 403, 'ACCESS_DENIED', 'You do not have permission to import to this space');
}
}
const result = await ImportService.importProject(spaceId, data, {
mode,
newName
});
apiLogger.info({ spaceId, projectId: result.projectId, tablesImported: result.tablesImported, userId }, 'Project imported');
return respondSuccess(res, result, 201);
} catch (error) {
apiLogger.error({ err: error }, 'POST /spaces/:spaceId/import/project error');
return respondError(res, 500, 'IMPORT_FAILED', error.message);
}
});
/**
* POST /api/v3/import/space
* Import a space as new
*
* Request Body:
* {
* "data": { ... exported space data ... },
* "newName": "Imported Space"
* }
*/
router.post('/import/space', async (req, res) => {
try {
const { data, newName } = req.body;
const userId = req.user?.id;
if (!data) {
return respondError(res, 400, 'MISSING_DATA', 'Export data is required');
}
if (!userId) {
return respondError(res, 401, 'UNAUTHORIZED', 'Authentication required to import space');
}
const result = await ImportService.importSpace(data, {
newName,
ownerId: userId
});
apiLogger.info({ spaceId: result.spaceId, projectsImported: result.projectsImported, userId }, 'Space imported');
return respondSuccess(res, result, 201);
} catch (error) {
apiLogger.error({ err: error }, 'POST /import/space error');
return respondError(res, 500, 'IMPORT_FAILED', error.message);
}
});
export default router;