562 lines
16 KiB
JavaScript
562 lines
16 KiB
JavaScript
// Updated routes/github-integration.js
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const GitHubIntegrationService = require('../services/github-integration.service');
|
|
const GitHubOAuthService = require('../services/github-oauth');
|
|
const FileStorageService = require('../services/file-storage.service');
|
|
const database = require('../config/database');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const githubService = new GitHubIntegrationService();
|
|
const oauthService = new GitHubOAuthService();
|
|
const fileStorageService = new FileStorageService();
|
|
|
|
// Attach GitHub repository to template
|
|
router.post('/attach-repository', async (req, res) => {
|
|
try {
|
|
const { template_id, repository_url, branch_name } = req.body;
|
|
|
|
// Validate input
|
|
if (!template_id || !repository_url) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Template ID and repository URL are required'
|
|
});
|
|
}
|
|
|
|
// Check if template exists
|
|
const templateQuery = 'SELECT * FROM templates WHERE id = $1 AND is_active = true';
|
|
const templateResult = await database.query(templateQuery, [template_id]);
|
|
|
|
if (templateResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Template not found'
|
|
});
|
|
}
|
|
|
|
// Parse GitHub URL
|
|
const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url);
|
|
|
|
// Check repository access
|
|
const accessCheck = await githubService.checkRepositoryAccess(owner, repo);
|
|
|
|
if (!accessCheck.hasAccess) {
|
|
if (accessCheck.requiresAuth) {
|
|
// Check if we have OAuth token
|
|
const tokenRecord = await oauthService.getToken();
|
|
if (!tokenRecord) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'GitHub authentication required for this repository',
|
|
requires_auth: true,
|
|
auth_url: `/api/github/auth/github`
|
|
});
|
|
}
|
|
}
|
|
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: accessCheck.error || 'Repository not accessible'
|
|
});
|
|
}
|
|
|
|
// Get repository information from GitHub
|
|
const repositoryData = await githubService.fetchRepositoryMetadata(owner, repo);
|
|
|
|
// Analyze the codebase
|
|
const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, branch || branch_name);
|
|
|
|
// Store everything in PostgreSQL
|
|
const insertQuery = `
|
|
INSERT INTO github_repositories (
|
|
template_id, repository_url, repository_name, owner_name,
|
|
branch_name, is_public, metadata, codebase_analysis, sync_status,
|
|
requires_auth
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING *
|
|
`;
|
|
|
|
const insertValues = [
|
|
template_id,
|
|
repository_url,
|
|
repo,
|
|
owner,
|
|
branch || branch_name || 'main',
|
|
repositoryData.visibility === 'public',
|
|
JSON.stringify(repositoryData),
|
|
JSON.stringify(codebaseAnalysis),
|
|
'synced',
|
|
accessCheck.requiresAuth
|
|
];
|
|
|
|
const insertResult = await database.query(insertQuery, insertValues);
|
|
const repositoryRecord = insertResult.rows[0];
|
|
|
|
// Download repository with file storage
|
|
console.log('Downloading repository with storage...');
|
|
const downloadResult = await githubService.downloadRepositoryWithStorage(
|
|
owner, repo, branch || branch_name || 'main', repositoryRecord.id
|
|
);
|
|
|
|
if (!downloadResult.success) {
|
|
// If download failed, still return the repository record but mark the storage issue
|
|
console.warn('Repository download failed:', downloadResult.error);
|
|
}
|
|
|
|
// Create feature-codebase mappings
|
|
const featureQuery = 'SELECT id FROM template_features WHERE template_id = $1';
|
|
const featureResult = await database.query(featureQuery, [template_id]);
|
|
|
|
if (featureResult.rows.length > 0) {
|
|
const mappingValues = [];
|
|
const mappingParams = [];
|
|
let paramIndex = 1;
|
|
|
|
for (const feature of featureResult.rows) {
|
|
mappingValues.push(`(uuid_generate_v4(), $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})`);
|
|
mappingParams.push(
|
|
feature.id,
|
|
repositoryRecord.id,
|
|
'[]', // Empty paths for now
|
|
'{}' // Empty snippets for now
|
|
);
|
|
}
|
|
|
|
const mappingQuery = `
|
|
INSERT INTO feature_codebase_mappings (id, feature_id, repository_id, code_paths, code_snippets)
|
|
VALUES ${mappingValues.join(', ')}
|
|
`;
|
|
|
|
await database.query(mappingQuery, mappingParams);
|
|
}
|
|
|
|
// Get storage information
|
|
const storageInfo = await githubService.getRepositoryStorage(repositoryRecord.id);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Repository attached successfully',
|
|
data: {
|
|
repository_id: repositoryRecord.id,
|
|
template_id: repositoryRecord.template_id,
|
|
repository_name: repositoryRecord.repository_name,
|
|
owner_name: repositoryRecord.owner_name,
|
|
branch_name: repositoryRecord.branch_name,
|
|
is_public: repositoryRecord.is_public,
|
|
requires_auth: repositoryRecord.requires_auth,
|
|
metadata: repositoryData,
|
|
codebase_analysis: codebaseAnalysis,
|
|
storage_info: storageInfo,
|
|
download_result: downloadResult
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error attaching repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to attach repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get repository information for a template
|
|
router.get('/template/:id/repository', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const query = `
|
|
SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count,
|
|
rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at
|
|
FROM github_repositories gr
|
|
LEFT JOIN repository_storage rs ON gr.id = rs.repository_id
|
|
WHERE gr.template_id = $1
|
|
ORDER BY gr.created_at DESC
|
|
LIMIT 1
|
|
`;
|
|
|
|
const result = await database.query(query, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'No repository found for this template'
|
|
});
|
|
}
|
|
|
|
const repository = result.rows[0];
|
|
|
|
const parseMaybe = (v) => {
|
|
if (v == null) return {};
|
|
if (typeof v === 'string') {
|
|
try { return JSON.parse(v); } catch { return {}; }
|
|
}
|
|
return v; // already an object from jsonb
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
...repository,
|
|
metadata: parseMaybe(repository.metadata),
|
|
codebase_analysis: parseMaybe(repository.codebase_analysis)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to fetch repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get repository file structure
|
|
router.get('/repository/:id/structure', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { path: directoryPath } = req.query;
|
|
|
|
// Get repository info
|
|
const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1';
|
|
const repoResult = await database.query(repoQuery, [id]);
|
|
|
|
if (repoResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Repository not found'
|
|
});
|
|
}
|
|
|
|
const structure = await fileStorageService.getRepositoryStructure(id, directoryPath);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
repository_id: id,
|
|
directory_path: directoryPath || '',
|
|
structure: structure
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching repository structure:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to fetch repository structure'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get files in a directory
|
|
router.get('/repository/:id/files', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { directory_path = '' } = req.query;
|
|
|
|
// Get repository info
|
|
const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1';
|
|
const repoResult = await database.query(repoQuery, [id]);
|
|
|
|
if (repoResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Repository not found'
|
|
});
|
|
}
|
|
|
|
const files = await fileStorageService.getDirectoryFiles(id, directory_path);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
repository_id: id,
|
|
directory_path: directory_path,
|
|
files: files
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching directory files:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to fetch directory files'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get file content
|
|
router.get('/repository/:id/file-content', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { file_path } = req.query;
|
|
|
|
if (!file_path) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'File path is required'
|
|
});
|
|
}
|
|
|
|
const query = `
|
|
SELECT rf.*, rfc.content_text, rfc.content_preview, rfc.language_detected,
|
|
rfc.line_count, rfc.char_count
|
|
FROM repository_files rf
|
|
LEFT JOIN repository_file_contents rfc ON rf.id = rfc.file_id
|
|
WHERE rf.repository_id = $1 AND rf.relative_path = $2
|
|
`;
|
|
|
|
const result = await database.query(query, [id, file_path]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'File not found'
|
|
});
|
|
}
|
|
|
|
const file = result.rows[0];
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
file_info: {
|
|
id: file.id,
|
|
filename: file.filename,
|
|
file_extension: file.file_extension,
|
|
relative_path: file.relative_path,
|
|
file_size_bytes: file.file_size_bytes,
|
|
mime_type: file.mime_type,
|
|
is_binary: file.is_binary,
|
|
language_detected: file.language_detected,
|
|
line_count: file.line_count,
|
|
char_count: file.char_count
|
|
},
|
|
content: file.is_binary ? null : file.content_text,
|
|
preview: file.content_preview
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching file content:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to fetch file content'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Search repository files
|
|
router.get('/repository/:id/search', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { q: query } = req.query;
|
|
|
|
if (!query) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Search query is required'
|
|
});
|
|
}
|
|
|
|
const results = await fileStorageService.searchFileContent(id, query);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
repository_id: id,
|
|
search_query: query,
|
|
results: results,
|
|
total_results: results.length
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error searching repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to search repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
// List all repositories for a template
|
|
router.get('/template/:id/repositories', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const query = `
|
|
SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count,
|
|
rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at
|
|
FROM github_repositories gr
|
|
LEFT JOIN repository_storage rs ON gr.id = rs.repository_id
|
|
WHERE gr.template_id = $1
|
|
ORDER BY gr.created_at DESC
|
|
`;
|
|
|
|
const result = await database.query(query, [id]);
|
|
|
|
const repositories = result.rows.map(repo => ({
|
|
...repo,
|
|
metadata: JSON.parse(repo.metadata || '{}'),
|
|
codebase_analysis: JSON.parse(repo.codebase_analysis || '{}')
|
|
}));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: repositories
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching repositories:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to fetch repositories'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Download repository files (legacy endpoint for backward compatibility)
|
|
router.post('/download', async (req, res) => {
|
|
try {
|
|
const { repository_url, branch_name } = req.body;
|
|
|
|
if (!repository_url) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Repository URL is required'
|
|
});
|
|
}
|
|
|
|
const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url);
|
|
const targetBranch = branch || branch_name || 'main';
|
|
|
|
const result = await githubService.downloadRepository(owner, repo, targetBranch);
|
|
|
|
if (result.success) {
|
|
res.json({
|
|
success: true,
|
|
message: 'Repository downloaded successfully',
|
|
data: result
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to download repository',
|
|
error: result.error
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error downloading repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to download repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Re-sync repository (re-download and update database)
|
|
router.post('/repository/:id/sync', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// Get repository info
|
|
const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1';
|
|
const repoResult = await database.query(repoQuery, [id]);
|
|
|
|
if (repoResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Repository not found'
|
|
});
|
|
}
|
|
|
|
const repository = repoResult.rows[0];
|
|
const { owner, repo, branch } = githubService.parseGitHubUrl(repository.repository_url);
|
|
|
|
// Clean up existing storage
|
|
await githubService.cleanupRepositoryStorage(id);
|
|
|
|
// Re-download with storage
|
|
const downloadResult = await githubService.downloadRepositoryWithStorage(
|
|
owner, repo, branch || repository.branch_name, id
|
|
);
|
|
|
|
// Update sync status
|
|
await database.query(
|
|
'UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
|
|
[downloadResult.success ? 'synced' : 'error', id]
|
|
);
|
|
|
|
res.json({
|
|
success: downloadResult.success,
|
|
message: downloadResult.success ? 'Repository synced successfully' : 'Failed to sync repository',
|
|
data: downloadResult
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error syncing repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to sync repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Remove repository from template
|
|
router.delete('/repository/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// Get repository info before deletion
|
|
const getQuery = 'SELECT * FROM github_repositories WHERE id = $1';
|
|
const getResult = await database.query(getQuery, [id]);
|
|
|
|
if (getResult.rows.length === 0) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Repository not found'
|
|
});
|
|
}
|
|
|
|
const repository = getResult.rows[0];
|
|
|
|
// Clean up file storage
|
|
await githubService.cleanupRepositoryStorage(id);
|
|
|
|
// Delete feature mappings first
|
|
await database.query(
|
|
'DELETE FROM feature_codebase_mappings WHERE repository_id = $1',
|
|
[id]
|
|
);
|
|
|
|
// Delete repository record
|
|
await database.query(
|
|
'DELETE FROM github_repositories WHERE id = $1',
|
|
[id]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Repository removed successfully',
|
|
data: {
|
|
removed_repository: repository.repository_name,
|
|
template_id: repository.template_id
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error removing repository:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message || 'Failed to remove repository'
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|