NeahNew/lib/services/integration-service.ts
2025-05-06 16:27:15 +02:00

271 lines
12 KiB
TypeScript

import { prisma } from '@/lib/prisma';
import { LeantimeService } from './leantime-service';
import { OutlineService } from './outline-service';
import { RocketChatService } from './rocketchat-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;
constructor() {
this.leantimeService = new LeantimeService();
this.outlineService = new OutlineService();
this.rocketChatService = new RocketChatService();
}
/**
* 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;
// 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 }
};
// 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
}
// 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 and penpotProjectId will be added when implemented
}
});
// 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
}
};
} catch (error) {
console.error('Error setting up integrations:', error);
// Rollback any created resources
await this.rollbackIntegrations(leantimeProjectId, outlineCollectionId, rocketChatChannelId);
// 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
*/
private async rollbackIntegrations(
leantimeProjectId?: number,
outlineCollectionId?: string,
rocketChatChannelId?: 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
};
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}`);
} catch (rocketChatError) {
console.error('Error during Rocket.Chat rollback:', rocketChatError);
console.log(`⚠️ Note: Rocket.Chat channel ${rocketChatChannelId} may need to be deleted manually`);
}
}
// 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) {
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}`);
}
}
}