import fs from 'fs' import path from 'path' import { Router } from 'express' import mongoose from 'mongoose' import pkg from '../package.json' import './utils/mongoose' import { ErrorLog } from './models/ErrorLog' const folderPath = path.resolve(__dirname, './routers') const getUrl = (url) => `${process.env.NODE_ENV === 'development' ? '' : '/ms'}${url}` // Определение типов interface EndpointStats { total: number; mock: number; real: number; } interface FileInfo { path: string; type: 'file'; endpoints: EndpointStats; } interface DirectoryInfo { path: string; type: 'directory'; endpoints: EndpointStats; children: (FileInfo | DirectoryInfo)[]; } interface DirScanResult { items: (FileInfo | DirectoryInfo)[]; totalEndpoints: EndpointStats; } // Функция для поиска эндпоинтов в файлах function countEndpoints(filePath) { if (!fs.existsSync(filePath) || !filePath.endsWith('.js') && !filePath.endsWith('.ts')) { return { total: 0, mock: 0, real: 0 }; } try { const content = fs.readFileSync(filePath, 'utf8'); const httpMethods = ['get', 'post', 'put', 'delete', 'patch']; const endpointMatches = []; let totalCount = 0; // Собираем все эндпоинты и их контекст httpMethods.forEach(method => { const regex = new RegExp(`router\\.${method}\\([^{]*{([\\s\\S]*?)(?:}\\s*\\)|},)`, 'gi'); let match; while ((match = regex.exec(content)) !== null) { totalCount++; endpointMatches.push({ method, body: match[1] }); } }); // Проверяем каждый эндпоинт - работает с БД или моковый let mockCount = 0; let realCount = 0; endpointMatches.forEach(endpoint => { const body = endpoint.body; // Признаки работы с базой данных - модели Mongoose и их методы const hasDbInteraction = /\b(find|findOne|findById|create|update|delete|remove|aggregate|count|model)\b.*\(/.test(body) || /\bmongoose\b/.test(body) || /\.[a-zA-Z]+Model\b/.test(body); // Проверка на отправку файлов (считается реальным эндпоинтом) const hasFileServing = /res\.sendFile\(/.test(body); // Проверка на отправку ошибок в JSON (считается реальным эндпоинтом) const hasErrorResponse = /res\.json\(\s*{.*?(error|success\s*:\s*false).*?}\)/.test(body) || /res\.status\(.*?\)\.json\(/.test(body); // Признаки моковых данных - только явный импорт JSON файлов или отправка JSON без ошибок const hasMockJsonImport = /require\s*\(\s*['"`].*\.json['"`]\s*\)/.test(body) || /import\s+.*\s+from\s+['"`].*\.json['"`]/.test(body); // JSON ответ, который не является ошибкой (упрощенная проверка) const hasJsonResponse = /res\.json\(/.test(body) && !hasErrorResponse; // Определяем тип эндпоинта if (hasDbInteraction || hasFileServing || hasErrorResponse) { // Если работает с БД, отправляет файлы или возвращает ошибки - считаем реальным realCount++; } else if (hasMockJsonImport || hasJsonResponse) { // Если импортирует JSON или отправляет JSON без ошибок - считаем моком mockCount++; } else { // По умолчанию считаем реальным realCount++; } }); return { total: totalCount, mock: mockCount, real: realCount }; } catch (err) { return { total: 0, mock: 0, real: 0 }; } } // Функция для рекурсивного обхода директорий function getAllDirs(dir, basePath = ''): DirScanResult { const items: (FileInfo | DirectoryInfo)[] = []; let totalEndpoints = { total: 0, mock: 0, real: 0 }; try { const dirItems = fs.readdirSync(dir); for (const item of dirItems) { const itemPath = path.join(dir, item); const relativePath = path.join(basePath, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { const dirResult = getAllDirs(itemPath, relativePath); totalEndpoints.total += dirResult.totalEndpoints.total; totalEndpoints.mock += dirResult.totalEndpoints.mock; totalEndpoints.real += dirResult.totalEndpoints.real; items.push({ path: relativePath, type: 'directory', endpoints: dirResult.totalEndpoints, children: dirResult.items }); } else { const fileEndpoints = countEndpoints(itemPath); totalEndpoints.total += fileEndpoints.total; totalEndpoints.mock += fileEndpoints.mock; totalEndpoints.real += fileEndpoints.real; items.push({ path: relativePath, type: 'file', endpoints: fileEndpoints }); } } } catch (err) { console.error(`Ошибка при чтении директории ${dir}:`, err); } return { items, totalEndpoints }; } // Функция для генерации HTML-списка директорий function generateDirList(dirs: (FileInfo | DirectoryInfo)[], level = 0) { if (dirs.length === 0) return ''; const indent = level * 20; return ``; } const router = Router() // Эндпоинт для получения содержимого файла router.get('/file-content', async (req, res) => { try { const filePath = req.query.path as string; if (!filePath) { return res.status(400).json({ error: 'Путь к файлу не указан' }); } // Используем корень проекта для получения абсолютного пути к файлу const projectRoot = path.resolve(__dirname, 'routers'); const absolutePath = path.join(projectRoot, filePath); // Проверка, что путь не выходит за пределы проекта if (!absolutePath.startsWith(projectRoot)) { return res.status(403).json({ error: 'Доступ запрещен' }); } if (!fs.existsSync(absolutePath)) { return res.status(404).json({ error: 'Файл не найден' }); } // Проверяем, что это файл, а не директория const stat = fs.statSync(absolutePath); if (!stat.isFile()) { return res.status(400).json({ error: 'Указанный путь не является файлом' }); } // Проверяем размер файла, чтобы не загружать слишком большие файлы const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB if (stat.size > MAX_FILE_SIZE) { return res.status(413).json({ error: 'Файл слишком большой', size: stat.size, maxSize: MAX_FILE_SIZE }); } const content = fs.readFileSync(absolutePath, 'utf8'); const fileExtension = path.extname(absolutePath).slice(1); res.json({ content, extension: fileExtension, fileName: path.basename(absolutePath) }); } catch (err) { console.error('Ошибка при чтении файла:', err); res.status(500).json({ error: 'Ошибка при чтении файла' }); } }); router.get('/', async (req, res) => { // throw new Error('check error message') // Используем корень проекта вместо только директории routers const projectRoot = path.resolve(__dirname, 'routers'); const routersPath = path.resolve(__dirname, 'routers'); const routerFolders = fs.readdirSync(routersPath); const directoryResult = getAllDirs(projectRoot); const totalEndpoints = directoryResult.totalEndpoints.total; const mockEndpoints = directoryResult.totalEndpoints.mock; const realEndpoints = directoryResult.totalEndpoints.real; // Получаем последние 10 ошибок const latestErrors = await ErrorLog.find().sort({ createdAt: -1 }).limit(10); // Сформируем HTML для секции с ошибками let errorsHtml = ''; if (latestErrors.length > 0) { errorsHtml = latestErrors.map(error => `

${error.message}

${new Date(error.createdAt).toLocaleString()}
${error.path ? `
${error.method || 'GET'} ${error.path}
` : ''} ${error.stack ? `
${error.stack}
` : ''}
`).join(''); } else { errorsHtml = '

Нет зарегистрированных ошибок

'; } // Создаем JavaScript для клиентской части const clientScript = ` document.addEventListener('DOMContentLoaded', function() { // Директории document.querySelectorAll('.dir-item[data-expandable="true"]').forEach(item => { item.addEventListener('click', function(e) { const subdirectory = this.nextElementSibling; const isExpanded = this.classList.toggle('expanded'); if (isExpanded) { subdirectory.style.display = 'block'; this.querySelector('.expand-icon').textContent = '▼'; } else { subdirectory.style.display = 'none'; this.querySelector('.expand-icon').textContent = '▶'; } e.stopPropagation(); }); }); // Модальное окно const modal = document.getElementById('fileModal'); const closeBtn = document.querySelector('.close-modal'); const fileContent = document.getElementById('fileContent'); const fileLoader = document.getElementById('fileLoader'); const modalFileName = document.getElementById('modalFileName'); // Закрытие модального окна closeBtn.addEventListener('click', function() { modal.style.display = 'none'; }); window.addEventListener('click', function(event) { if (event.target == modal) { modal.style.display = 'none'; } }); // Обработчик для файлов document.querySelectorAll('.file-item').forEach(item => { item.addEventListener('click', async function() { const filePath = this.getAttribute('data-path'); if (!filePath) return; // Показываем модальное окно и лоадер modal.style.display = 'block'; fileContent.style.display = 'none'; fileLoader.style.display = 'block'; modalFileName.textContent = 'Загрузка...'; try { const response = await fetch('${getUrl('/file-content?path=')}' + encodeURIComponent(filePath)); if (!response.ok) { throw new Error('Ошибка при загрузке файла'); } const data = await response.json(); // Отображаем содержимое файла fileLoader.style.display = 'none'; fileContent.style.display = 'block'; fileContent.textContent = data.content; modalFileName.textContent = data.fileName; // Подсветка синтаксиса const extensionMap = { 'js': 'javascript', 'ts': 'typescript', 'json': 'json', 'css': 'css', 'html': 'xml', 'xml': 'xml', 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'sh': 'bash', 'bash': 'bash' }; const language = extensionMap[data.extension] || ''; if (language) { fileContent.className = 'language-' + language; hljs.highlightElement(fileContent); } } catch (error) { fileLoader.style.display = 'none'; fileContent.style.display = 'block'; fileContent.textContent = 'Ошибка при загрузке файла: ' + error.message; modalFileName.textContent = 'Ошибка'; } }); }); // Обработчик кнопки очистки ошибок const clearErrorsBtn = document.getElementById('clearErrorsBtn'); const successAction = document.getElementById('successAction'); clearErrorsBtn.addEventListener('click', async function() { try { const response = await fetch('${getUrl('/clear-old-errors')}', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { throw new Error('Ошибка при очистке старых ошибок'); } const data = await response.json(); // Показываем сообщение об успехе successAction.textContent = 'Удалено ' + data.deletedCount + ' записей'; successAction.style.display = 'block'; // Перезагружаем страницу через 2 секунды setTimeout(() => { window.location.reload(); }, 2000); } catch (error) { console.error('Ошибка:', error); alert('Произошла ошибка: ' + error.message); } }); // Обработчик кнопок очистки коллекций document.querySelectorAll('.clear-model-btn').forEach(button => { button.addEventListener('click', async function() { const modelName = this.getAttribute('data-model'); if (!modelName) return; if (!confirm(\`Вы уверены, что хотите очистить коллекцию \${modelName}?\`)) { return; } try { const response = await fetch('' + getUrl('/clear-collection'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ modelName }) }); if (!response.ok) { throw new Error('Ошибка при очистке коллекции'); } const data = await response.json(); // Показываем сообщение об успехе successAction.textContent = 'Коллекция ' + data.model + ' очищена. Удалено ' + data.deletedCount + ' записей'; successAction.style.display = 'block'; // Перезагружаем страницу через 2 секунды setTimeout(() => { window.location.reload(); }, 2000); } catch (error) { console.error('Ошибка:', error); alert('Произошла ошибка: ' + error.message); } }); }); }); `; res.send(` Multy Stub v${pkg.version}

Multy Stub v${pkg.version}

${totalEndpoints}
Всего эндпоинтов
${mockEndpoints}
Моковые эндпоинты
${realEndpoints}
Реальные эндпоинты

Routers:

${routerFolders.map((f) => `
${f}
`).join('')}

Структура директорий проекта:

${generateDirList(directoryResult.items)}

Models:

${ (await Promise.all( (await mongoose.modelNames()).map(async (name) => { const model = mongoose.model(name); const count = await model.countDocuments(); // Получаем информацию о полях модели const schema = model.schema; const fields = Object.keys(schema.paths).filter(field => !['__v', '_id'].includes(field)); return `

${name}

${count} документов

Поля:

    ${fields.map(field => { const fieldType = schema.paths[field].instance; return `
  • ${field}: ${fieldType}
  • `; }).join('')}
`; } ) )).join('') }

Последние ошибки:

${errorsHtml}
Операция выполнена успешно
`) }) // Эндпоинт для очистки ошибок старше 10 дней router.post('/clear-old-errors', async (req, res) => { try { const tenDaysAgo = new Date(); tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); const result = await ErrorLog.deleteMany({ createdAt: { $lt: tenDaysAgo } }); res.json({ success: true, deletedCount: result.deletedCount || 0 }); } catch (error) { console.error('Ошибка при очистке старых ошибок:', error); res.status(500).json({ success: false, error: 'Ошибка при очистке старых ошибок' }); } }); // Эндпоинт для очистки отдельной коллекции router.post('/clear-collection', async (req, res) => { try { const { modelName } = req.body; if (!modelName) { return res.status(400).json({ success: false, error: 'Имя модели не указано' }); } // Проверяем, существует ли такая модель if (!mongoose.modelNames().includes(modelName)) { return res.status(404).json({ success: false, error: 'Модель не найдена' }); } const model = mongoose.model(modelName); const result = await model.deleteMany({}); res.json({ success: true, deletedCount: result.deletedCount || 0, model: modelName }); } catch (error) { console.error(`Ошибка при очистке коллекции:`, error); res.status(500).json({ success: false, error: 'Ошибка при очистке коллекции' }); } }); export default router