349 lines
16 KiB
TypeScript
349 lines
16 KiB
TypeScript
import { prisma } from '@/lib/prisma';
|
|
import { LeantimeService } from './leantime-service';
|
|
import { OutlineService } from './outline-service';
|
|
import { RocketChatService } from './rocketchat-service';
|
|
import { GiteaService } from './gitea-service';
|
|
import axios from 'axios';
|
|
|
|
interface IntegrationResult {
|
|
success: boolean;
|
|
error?: string;
|
|
data?: {
|
|
leantimeProjectId?: number;
|
|
outlineCollectionId?: string;
|
|
rocketChatChannelId?: string;
|
|
giteaRepositoryUrl?: string;
|
|
penpotProjectId?: string;
|
|
};
|
|
}
|
|
|
|
export class IntegrationService {
|
|
private leantimeService: LeantimeService;
|
|
private outlineService: OutlineService;
|
|
private rocketChatService: RocketChatService;
|
|
private giteaService: GiteaService;
|
|
|
|
constructor() {
|
|
this.leantimeService = new LeantimeService();
|
|
this.outlineService = new OutlineService();
|
|
this.rocketChatService = new RocketChatService();
|
|
this.giteaService = new GiteaService();
|
|
}
|
|
|
|
/**
|
|
* Set up all integrations for a mission
|
|
* @param missionId The mission ID
|
|
* @returns Integration result
|
|
*/
|
|
async setupIntegrationsForMission(missionId: string): Promise<IntegrationResult> {
|
|
try {
|
|
// Get complete mission data with users
|
|
const mission = await prisma.mission.findUnique({
|
|
where: { id: missionId },
|
|
include: {
|
|
missionUsers: {
|
|
include: {
|
|
user: true
|
|
}
|
|
},
|
|
attachments: true
|
|
}
|
|
});
|
|
|
|
if (!mission) {
|
|
throw new Error(`Mission not found: ${missionId}`);
|
|
}
|
|
|
|
// These fields will store the IDs of created resources
|
|
let leantimeProjectId: number | undefined;
|
|
let outlineCollectionId: string | undefined;
|
|
let rocketChatChannelId: string | undefined;
|
|
let giteaRepositoryUrl: string | undefined;
|
|
|
|
// Track which integrations succeeded and which failed
|
|
const integrationStatus = {
|
|
leantime: { success: false, id: undefined as number | undefined, error: undefined as string | undefined },
|
|
outline: { success: false, id: undefined as string | undefined, error: undefined as string | undefined },
|
|
rocketchat: { success: false, id: undefined as string | undefined, error: undefined as string | undefined },
|
|
gitea: { success: false, url: undefined as string | undefined, error: undefined as string | undefined }
|
|
};
|
|
|
|
// A flag to determine if we should consider this a success or failure overall
|
|
let criticalFailure = false;
|
|
|
|
try {
|
|
// Step 1: Create Leantime project (Consider this a critical integration)
|
|
try {
|
|
console.log('Starting Leantime project creation...');
|
|
leantimeProjectId = await this.leantimeService.createProject(mission);
|
|
console.log(`Leantime project created with ID: ${leantimeProjectId}`);
|
|
integrationStatus.leantime.success = true;
|
|
integrationStatus.leantime.id = leantimeProjectId;
|
|
} catch (leantimeError) {
|
|
console.error('Error creating Leantime project:', leantimeError);
|
|
integrationStatus.leantime.success = false;
|
|
integrationStatus.leantime.error = leantimeError instanceof Error ? leantimeError.message : String(leantimeError);
|
|
criticalFailure = true;
|
|
throw leantimeError; // Leantime is critical, so we rethrow
|
|
}
|
|
|
|
// Add a delay to avoid rate limits (extended to 3 seconds)
|
|
console.log('Waiting 3 seconds before proceeding to Outline integration...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
// Step 2: Create Outline collection (Consider this non-critical)
|
|
try {
|
|
console.log('Starting Outline collection creation...');
|
|
outlineCollectionId = await this.outlineService.createCollection(mission);
|
|
console.log(`Outline collection created with ID: ${outlineCollectionId}`);
|
|
integrationStatus.outline.success = true;
|
|
integrationStatus.outline.id = outlineCollectionId;
|
|
} catch (outlineError) {
|
|
console.error('Error creating Outline collection:', outlineError);
|
|
integrationStatus.outline.success = false;
|
|
integrationStatus.outline.error = outlineError instanceof Error ? outlineError.message : String(outlineError);
|
|
|
|
// Check if it's an authentication error (401)
|
|
if (axios.isAxiosError(outlineError) && outlineError.response?.status === 401) {
|
|
console.log('⚠️ Outline authentication error. Please check your API credentials.');
|
|
} else if (axios.isAxiosError(outlineError) && outlineError.response?.status === 429) {
|
|
console.log('⚠️ Outline rate limiting error (429). The integration will be skipped for now.');
|
|
}
|
|
|
|
// Don't set criticalFailure - Outline is non-critical
|
|
}
|
|
|
|
// Add a delay to avoid rate limits (extended to 3 seconds)
|
|
console.log('Waiting 3 seconds before proceeding to Rocket.Chat integration...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
// Step 3: Create Rocket.Chat channel (Consider this non-critical)
|
|
try {
|
|
console.log('Starting Rocket.Chat channel creation...');
|
|
rocketChatChannelId = await this.rocketChatService.createChannel(mission);
|
|
console.log(`Rocket.Chat channel created with ID: ${rocketChatChannelId}`);
|
|
integrationStatus.rocketchat.success = true;
|
|
integrationStatus.rocketchat.id = rocketChatChannelId;
|
|
} catch (rocketChatError) {
|
|
console.error('Error creating Rocket.Chat channel:', rocketChatError);
|
|
integrationStatus.rocketchat.success = false;
|
|
integrationStatus.rocketchat.error = rocketChatError instanceof Error ? rocketChatError.message : String(rocketChatError);
|
|
|
|
// Check for rate limiting
|
|
if (axios.isAxiosError(rocketChatError) && rocketChatError.response?.status === 429) {
|
|
console.log('⚠️ Rocket.Chat rate limiting error (429). The integration will be skipped for now.');
|
|
}
|
|
// Don't set criticalFailure - Rocket.Chat is non-critical
|
|
}
|
|
|
|
// Add a delay to avoid rate limits (extended to 3 seconds)
|
|
console.log('Waiting 3 seconds before proceeding to Gitea integration...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
// Step 4: Create Gitea repository (Consider this non-critical)
|
|
// Only create if Gite or Calcul services are selected
|
|
if (mission.services &&
|
|
(mission.services.includes('Gite') || mission.services.includes('Calcul'))) {
|
|
try {
|
|
console.log('Starting Gitea repository creation...');
|
|
const repoData = await this.giteaService.createRepository(mission);
|
|
if (repoData && repoData.html_url) {
|
|
giteaRepositoryUrl = repoData.html_url;
|
|
} else {
|
|
giteaRepositoryUrl = this.giteaService.getRepositoryUrl(
|
|
mission.name
|
|
.toLowerCase()
|
|
.replace(/\s+/g, '-')
|
|
.replace(/[^a-z0-9-]/g, '')
|
|
);
|
|
}
|
|
console.log(`Gitea repository created at: ${giteaRepositoryUrl}`);
|
|
integrationStatus.gitea.success = true;
|
|
integrationStatus.gitea.url = giteaRepositoryUrl;
|
|
} catch (giteaError) {
|
|
console.error('Error creating Gitea repository:', giteaError);
|
|
integrationStatus.gitea.success = false;
|
|
integrationStatus.gitea.error = giteaError instanceof Error ? giteaError.message : String(giteaError);
|
|
|
|
// Don't set criticalFailure - Gitea is non-critical
|
|
}
|
|
} else {
|
|
console.log('Skipping Gitea repository creation - neither Gite nor Calcul services selected');
|
|
integrationStatus.gitea.success = false;
|
|
integrationStatus.gitea.error = 'Skipped - service not selected';
|
|
}
|
|
|
|
// Update the mission with the integration IDs (only for successful integrations)
|
|
await prisma.mission.update({
|
|
where: { id: missionId },
|
|
data: {
|
|
leantimeProjectId: integrationStatus.leantime.success ? leantimeProjectId?.toString() : undefined,
|
|
outlineCollectionId: integrationStatus.outline.success ? outlineCollectionId : undefined,
|
|
rocketChatChannelId: integrationStatus.rocketchat.success ? rocketChatChannelId : undefined,
|
|
giteaRepositoryUrl: integrationStatus.gitea.success ? giteaRepositoryUrl : undefined
|
|
}
|
|
});
|
|
|
|
// Output a summary of integration results
|
|
console.log('Integration results:', JSON.stringify(integrationStatus, null, 2));
|
|
|
|
// If we get here without a critical failure, we consider it a success
|
|
// even if some non-critical integrations failed
|
|
return {
|
|
success: !criticalFailure,
|
|
data: {
|
|
leantimeProjectId,
|
|
outlineCollectionId,
|
|
rocketChatChannelId,
|
|
giteaRepositoryUrl
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error setting up integrations:', error);
|
|
|
|
// Rollback any created resources
|
|
await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId, giteaRepositoryUrl);
|
|
|
|
// Delete the mission itself since a critical integration failed
|
|
try {
|
|
await prisma.mission.delete({
|
|
where: { id: missionId }
|
|
});
|
|
console.log(`Mission ${missionId} deleted due to critical integration failure.`);
|
|
} catch (deleteError) {
|
|
console.error('Error deleting mission after integration failure:', deleteError);
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: `Integration failed: ${error instanceof Error ? error.message : String(error)}`
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in integration setup:', error);
|
|
return {
|
|
success: false,
|
|
error: `Integration setup failed: ${error instanceof Error ? error.message : String(error)}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rollback integrations if any step fails
|
|
* @param leantimeProjectId The Leantime project ID to delete
|
|
* @param outlineCollectionId The Outline collection ID to delete
|
|
* @param rocketChatChannelId The Rocket.Chat channel ID to delete
|
|
* @param giteaRepositoryUrl The Gitea repository URL to extract owner/repo from for deletion
|
|
*/
|
|
public async rollbackIntegrations(
|
|
leantimeProjectId?: number,
|
|
outlineCollectionId?: string,
|
|
rocketChatChannelId?: string,
|
|
giteaRepositoryUrl?: string
|
|
): Promise<void> {
|
|
console.log('⚠️ Rolling back integrations due to an error...');
|
|
|
|
// Track what we've successfully rolled back
|
|
const rollbackStatuses = {
|
|
leantime: false,
|
|
outline: false,
|
|
rocketchat: false,
|
|
gitea: false
|
|
};
|
|
|
|
try {
|
|
// Attempt to delete Leantime project
|
|
if (leantimeProjectId) {
|
|
try {
|
|
console.log(`Attempting to delete Leantime project: ${leantimeProjectId}`);
|
|
const leantimeSuccess = await this.leantimeService.deleteProject(leantimeProjectId);
|
|
rollbackStatuses.leantime = leantimeSuccess;
|
|
console.log(`Leantime project deletion ${leantimeSuccess ? 'successful' : 'failed'}: ${leantimeProjectId}`);
|
|
|
|
// Add a longer delay to avoid rate limiting (3 seconds)
|
|
console.log('Waiting 3 seconds before next rollback operation...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
} catch (leantimeError) {
|
|
console.error('Error during Leantime rollback:', leantimeError);
|
|
console.log(`⚠️ Note: Leantime project ${leantimeProjectId} may need to be deleted manually`);
|
|
}
|
|
}
|
|
|
|
// Attempt to delete Outline collection
|
|
if (outlineCollectionId) {
|
|
try {
|
|
console.log(`Attempting to delete Outline collection: ${outlineCollectionId}`);
|
|
const outlineSuccess = await this.outlineService.deleteCollection(outlineCollectionId);
|
|
rollbackStatuses.outline = outlineSuccess;
|
|
console.log(`Outline collection deletion ${outlineSuccess ? 'successful' : 'failed'}: ${outlineCollectionId}`);
|
|
|
|
// Add a longer delay to avoid rate limiting (3 seconds)
|
|
console.log('Waiting 3 seconds before next rollback operation...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
} catch (outlineError) {
|
|
console.error('Error during Outline rollback:', outlineError);
|
|
console.log(`⚠️ Note: Outline collection ${outlineCollectionId} may need to be deleted manually`);
|
|
}
|
|
}
|
|
|
|
// Attempt to delete Rocket.Chat channel
|
|
if (rocketChatChannelId) {
|
|
try {
|
|
console.log(`Attempting to delete Rocket.Chat channel: ${rocketChatChannelId}`);
|
|
const rocketChatSuccess = await this.rocketChatService.deleteChannel(rocketChatChannelId);
|
|
rollbackStatuses.rocketchat = rocketChatSuccess;
|
|
console.log(`Rocket.Chat channel deletion ${rocketChatSuccess ? 'successful' : 'failed'}: ${rocketChatChannelId}`);
|
|
|
|
// Add a longer delay to avoid rate limiting (3 seconds)
|
|
console.log('Waiting 3 seconds before next rollback operation...');
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
} catch (rocketChatError) {
|
|
console.error('Error during Rocket.Chat rollback:', rocketChatError);
|
|
console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`);
|
|
}
|
|
}
|
|
|
|
// Attempt to delete Gitea repository
|
|
if (giteaRepositoryUrl) {
|
|
try {
|
|
// Extract owner and repo from URL
|
|
const urlParts = giteaRepositoryUrl.split('/');
|
|
// Make sure we have enough parts in the URL before accessing array elements
|
|
if (urlParts.length >= 2) {
|
|
const owner = urlParts[urlParts.length - 2];
|
|
const repo = urlParts[urlParts.length - 1];
|
|
|
|
console.log(`Attempting to delete Gitea repository: ${owner}/${repo}`);
|
|
const giteaSuccess = await this.giteaService.deleteRepository(owner, repo);
|
|
rollbackStatuses.gitea = giteaSuccess;
|
|
console.log(`Gitea repository deletion ${giteaSuccess ? 'successful' : 'failed'}: ${owner}/${repo}`);
|
|
} else {
|
|
console.log(`Invalid Gitea repository URL format: ${giteaRepositoryUrl}`);
|
|
}
|
|
} catch (giteaError) {
|
|
console.error('Error during Gitea rollback:', giteaError);
|
|
console.log(`⚠️ Note: Gitea repository at ${giteaRepositoryUrl} may need to be deleted manually`);
|
|
}
|
|
} else {
|
|
console.log('No Gitea repository was created, skipping rollback');
|
|
// Mark as successful since there's nothing to delete
|
|
rollbackStatuses.gitea = true;
|
|
}
|
|
|
|
// Provide a summary of rollback operations
|
|
console.log('Rollback summary:', JSON.stringify(rollbackStatuses));
|
|
|
|
// If any rollbacks failed, provide a note
|
|
if (!rollbackStatuses.leantime || !rollbackStatuses.outline || !rollbackStatuses.rocketchat || !rollbackStatuses.gitea) {
|
|
console.log('⚠️ Some resources may need to be deleted manually.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during rollback:', error);
|
|
console.log('⚠️ Resources may need to be deleted manually:');
|
|
if (leantimeProjectId) console.log(`- Leantime project: ${leantimeProjectId}`);
|
|
if (outlineCollectionId) console.log(`- Outline collection: ${outlineCollectionId}`);
|
|
if (rocketChatChannelId) console.log(`- Rocket.Chat channel: ${rocketChatChannelId}`);
|
|
if (giteaRepositoryUrl) console.log(`- Gitea repository: ${giteaRepositoryUrl}`);
|
|
}
|
|
}
|
|
} |