godcrm/backend/routes/v3/documents/legacy.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

262 lines
11 KiB
JavaScript

// Documents: Legacy v3 import/export routes (backward compatibility)
import express from 'express';
import {
dbGet, dbRun, safeJsonParse,
generateBaseId, apiLogger,
success, created, error, badRequest, notFound,
requireEditorAccess, buildDocumentStructure, slugify,
} from './_helpers.js';
const router = express.Router();
/** POST /api/v3/documents/import — Batch import document with atoms (sections) */
router.post('/documents/import', async (req, res) => {
try {
const { document, atoms, documents_table_id, sections_table_id } = req.body;
const userId = req.user?.id;
if (documents_table_id) {
const legacyDocsTable = await dbGet('SELECT project_id FROM universal_tables WHERE id = ?', [documents_table_id]);
if (legacyDocsTable?.project_id) {
if (!(await requireEditorAccess(req, res, legacyDocsTable.project_id))) return;
}
}
if (!document?.name) return badRequest(res, 'Document name is required');
if (!documents_table_id || !sections_table_id) return badRequest(res, 'documents_table_id and sections_table_id are required');
if (!atoms || !Array.isArray(atoms) || atoms.length === 0) return badRequest(res, 'At least one atom is required');
const docsTable = await dbGet('SELECT id FROM universal_tables WHERE id = ?', [documents_table_id]);
const sectionsTable = await dbGet('SELECT id FROM universal_tables WHERE id = ?', [sections_table_id]);
if (!docsTable || !sectionsTable) return notFound(res, 'Documents or sections table not found');
// STEP 1: Create atoms (sections)
const createdSections = [];
const tempIdToRealId = new Map();
const h2Sections = [];
for (let i = 0; i < atoms.length; i++) {
const atom = atoms[i];
const baseId = generateBaseId();
const atomData = {
type: atom.type || 'reference',
key: atom.key || slugify(atom.title || `section-${i + 1}`),
title: atom.title || `Section ${i + 1}`,
content: atom.content || '',
order_index: atom.order_index ?? (i + 1) * 10,
local_order: atom.local_order ?? (i + 1),
h2: atom.h2 || null, h3: atom.h3 || null,
parent: null,
http_method: atom.http_method || null,
http_path: atom.http_path || null,
tags: atom.tags || [],
source_file: atom.source_file || null
};
const result = await dbRun(
`INSERT INTO table_rows (table_id, base_id, data, created_by) VALUES (?, ?, ?, ?)`,
[sections_table_id, baseId, JSON.stringify(atomData), userId]
);
const sectionId = result.lastInsertRowid;
createdSections.push({ id: sectionId, ...atomData });
if (atom.temp_id) tempIdToRealId.set(atom.temp_id, sectionId);
if (atom.h2 && !atom.h3) h2Sections.push({ id: sectionId, children: [], order: atomData.local_order });
}
// STEP 2: Update parent references
for (let i = 0; i < atoms.length; i++) {
const atom = atoms[i];
const section = createdSections[i];
if (atom.parent_temp_id && tempIdToRealId.has(atom.parent_temp_id)) {
const parentId = tempIdToRealId.get(atom.parent_temp_id);
const existingRow = await dbGet('SELECT data FROM table_rows WHERE id = ?', [section.id]);
if (existingRow) {
const data = JSON.parse(existingRow.data);
data.parent = parentId;
await dbRun('UPDATE table_rows SET data = ? WHERE id = ?', [JSON.stringify(data), section.id]);
}
const parentH2 = h2Sections.find(h2 => h2.id === parentId);
if (parentH2) parentH2.children.push({ id: section.id, order: atom.local_order ?? i });
} else if (atom.parent_index !== undefined && atom.parent_index >= 0) {
const parentSection = createdSections[atom.parent_index];
if (parentSection) {
const existingRow = await dbGet('SELECT data FROM table_rows WHERE id = ?', [section.id]);
if (existingRow) {
const data = JSON.parse(existingRow.data);
data.parent = parentSection.id;
await dbRun('UPDATE table_rows SET data = ? WHERE id = ?', [JSON.stringify(data), section.id]);
}
const parentH2 = h2Sections.find(h2 => h2.id === parentSection.id);
if (parentH2) parentH2.children.push({ id: section.id, order: atom.local_order ?? i });
}
}
}
h2Sections.forEach(h2 => { h2.children.sort((a, b) => a.order - b.order); });
// STEP 3: Build document structure
const structure = buildDocumentStructure(h2Sections, document.name, document.description);
// STEP 4: Create document
const docBaseId = generateBaseId();
const docData = {
name: document.name, description: document.description || '',
category: document.category || '', icon: document.icon || '📄',
status: document.status || 'draft',
sections: createdSections.map(s => s.id),
structure: JSON.stringify(structure)
};
const docResult = await dbRun(
`INSERT INTO table_rows (table_id, base_id, data, created_by) VALUES (?, ?, ?, ?)`,
[documents_table_id, docBaseId, JSON.stringify(docData), userId]
);
const documentId = docResult.lastInsertRowid;
apiLogger.info(`[Documents] Imported document "${document.name}" with ${createdSections.length} sections`);
created(res, {
document_id: documentId,
section_ids: createdSections.map(s => s.id),
section_count: createdSections.length,
structure
});
} catch (err) {
apiLogger.error({ err }, 'POST /documents/import error:', err);
error(res, 'IMPORT_DOCUMENT_ERROR', err.message, 500);
}
});
/** GET /api/v3/documents/:documentId/export — Export document as Markdown or JSON */
router.get('/documents/:documentId/export', async (req, res) => {
try {
const { documentId } = req.params;
const { documents_table_id, sections_table_id, format = 'markdown', lang } = req.query;
if (!documents_table_id || !sections_table_id) return badRequest(res, 'documents_table_id and sections_table_id are required');
const docRow = await dbGet('SELECT * FROM table_rows WHERE id = ? AND table_id = ?', [documentId, documents_table_id]);
if (!docRow) return notFound(res, 'Document not found');
const docData = safeJsonParse(docRow.data, {});
// Parse sections array
let sectionIds = [];
if (typeof docData.sections === 'string') { try { sectionIds = JSON.parse(docData.sections); } catch { sectionIds = []; } }
else if (Array.isArray(docData.sections)) { sectionIds = docData.sections; }
let structure = null;
if (docData.structure) { try { structure = safeJsonParse(docData.structure, null); } catch { structure = null; } }
// If no sections but structure exists, extract IDs from structure
if (sectionIds.length === 0 && structure?.sections) {
for (const structSection of structure.sections) {
const h2Id = structSection.section_id || structSection.id;
if (h2Id) sectionIds.push(h2Id);
if (structSection.children && Array.isArray(structSection.children)) {
for (const child of structSection.children) {
const childId = typeof child === 'object' ? (child.section_id || child.id) : child;
if (childId) sectionIds.push(childId);
}
}
}
}
// Fetch all sections
const sections = [];
for (const sectionId of sectionIds) {
const numId = typeof sectionId === 'string' ? parseInt(sectionId, 10) : sectionId;
if (isNaN(numId)) continue;
const sectionRow = await dbGet('SELECT * FROM table_rows WHERE id = ? AND table_id = ?', [numId, sections_table_id]);
if (sectionRow) sections.push({ id: sectionRow.id, ...safeJsonParse(sectionRow.data, {}) });
}
sections.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
const getLocalizedField = (section, field) => {
if (lang) {
const langSuffix = lang === 'ru' ? '_rus' : `_${lang}`;
const val = section[`${field}${langSuffix}`];
if (val && String(val).trim()) return val;
}
return section[field] || '';
};
// JSON FORMAT
if (format === 'json') {
return success(res, {
document: { id: docRow.id, name: docData.name, description: docData.description, category: docData.category, icon: docData.icon, status: docData.status, structure },
sections: sections.map(s => ({
id: s.id, type: s.type, key: s.key, title: s.title,
content: getLocalizedField(s, 'content'),
h2: s.h2, h3: s.h3, local_order: s.local_order, order_index: s.order_index,
parent: s.parent, http_method: s.http_method, http_path: s.http_path, tags: s.tags
}))
});
}
// MARKDOWN FORMAT
let markdown = `# ${docData.icon || '📄'} ${docData.name || 'Untitled Document'}\n\n`;
if (docData.description) markdown += `${docData.description}\n\n`;
markdown += '---\n\n';
const sectionMap = new Map(sections.map(s => [s.id, s]));
const renderedIds = new Set();
const renderSection = (section, level = 2) => {
if (!section || renderedIds.has(section.id)) return '';
renderedIds.add(section.id);
let md = '';
const headingPrefix = '#'.repeat(level);
const title = section.title || section.key || 'Untitled';
if (section.type === 'endpoint' && section.http_method) {
md += `${headingPrefix} ${section.http_method} ${section.http_path || title}\n\n`;
} else {
md += `${headingPrefix} ${title}\n\n`;
}
const content = getLocalizedField(section, 'content');
if (content) md += `${content}\n\n`;
if (section.http_path && section.type === 'endpoint' && !section.http_method) md += `**Path:** \`${section.http_path}\`\n\n`;
if (section.code) md += '```\n' + section.code + '\n```\n\n';
if (section.tags && section.tags.length > 0) md += `**Tags:** ${section.tags.join(', ')}\n\n`;
return md;
};
if (structure && structure.version === 2 && structure.sections) {
for (const h2Struct of structure.sections) {
const h2Id = h2Struct.section_id || h2Struct.id;
const h2Section = sectionMap.get(h2Id);
if (h2Section) {
markdown += renderSection(h2Section, 2);
if (h2Struct.children && h2Struct.children.length > 0) {
for (const child of h2Struct.children) {
const childId = typeof child === 'object' ? (child.section_id || child.id) : child;
const childSection = sectionMap.get(childId);
if (childSection) markdown += renderSection(childSection, 3);
}
}
if (h2Struct.footer?.text) markdown += `> ${h2Struct.footer.text}\n\n`;
}
}
if (structure.footer?.text) markdown += `---\n\n**${structure.footer.text}**\n\n`;
if (structure.links && structure.links.length > 0) {
for (const linkGroup of structure.links) {
markdown += `### ${linkGroup.label}\n\n`;
markdown += `_References: ${linkGroup.refs.join(', ')}_\n\n`;
}
}
} else {
for (const section of sections) {
markdown += renderSection(section, section.h3 ? 3 : 2);
}
}
const filename = slugify(docData.name || 'document') + '.md';
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(markdown);
} catch (err) {
apiLogger.error({ err }, 'GET /documents/:id/export error:', err);
error(res, 'EXPORT_DOCUMENT_ERROR', err.message, 500);
}
});
export default router;