|
1 | 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ |
2 | | -import { eq, and, count } from 'drizzle-orm'; |
| 2 | +import { eq, and, count, or } from 'drizzle-orm'; |
3 | 3 | import { getDb, getSchema } from '../db/index'; |
4 | 4 | import { generateId } from 'lucia'; |
5 | 5 | import { GlobalSettings } from '../global-settings/helpers'; |
6 | 6 | import type { FastifyBaseLogger } from 'fastify'; |
| 7 | +import { McpInstallationService } from './mcpInstallationService'; |
| 8 | +import { SatelliteCommandService } from './satelliteCommandService'; |
7 | 9 |
|
8 | 10 | export interface Team { |
9 | 11 | id: string; |
@@ -281,9 +283,99 @@ export class TeamService { |
281 | 283 | /** |
282 | 284 | * Delete team |
283 | 285 | */ |
284 | | - static async deleteTeam(teamId: string): Promise<boolean> { |
| 286 | + static async deleteTeam(teamId: string, userId: string, logger: FastifyBaseLogger): Promise<boolean> { |
285 | 287 | const { db, schema } = this.getDbAndSchema(); |
286 | | - // Delete team memberships first (cascade should handle this, but being explicit) |
| 288 | + |
| 289 | + // Delete all MCP installations for this team |
| 290 | + // This will remove the database records and create satellite commands to kill processes |
| 291 | + try { |
| 292 | + const installationService = new McpInstallationService(db, logger); |
| 293 | + const satelliteCommandService = new SatelliteCommandService(db, logger); |
| 294 | + |
| 295 | + // Get all MCP installations for this team |
| 296 | + const installations = await installationService.getTeamInstallations(teamId, userId); |
| 297 | + |
| 298 | + let deletedCount = 0; |
| 299 | + let totalCommands = 0; |
| 300 | + |
| 301 | + // Delete each installation and create satellite commands |
| 302 | + for (const installation of installations) { |
| 303 | + try { |
| 304 | + // 1. Delete installation from database |
| 305 | + const deleted = await installationService.deleteInstallation(installation.id, teamId); |
| 306 | + if (deleted) { |
| 307 | + deletedCount++; |
| 308 | + } |
| 309 | + |
| 310 | + // 2. Create satellite commands to kill processes (fire-and-forget) |
| 311 | + try { |
| 312 | + const commands = await satelliteCommandService.notifyMcpInstallation( |
| 313 | + installation.id, |
| 314 | + teamId, |
| 315 | + userId |
| 316 | + ); |
| 317 | + totalCommands += commands.length; |
| 318 | + } catch (commandError) { |
| 319 | + logger.error(commandError, `Failed to create satellite commands for installation ${installation.id}`); |
| 320 | + // Continue even if satellite command creation fails |
| 321 | + } |
| 322 | + } catch (installationError) { |
| 323 | + logger.error(installationError, `Failed to delete installation ${installation.id}`); |
| 324 | + // Continue with other installations even if one fails |
| 325 | + } |
| 326 | + } |
| 327 | + |
| 328 | + logger.info({ |
| 329 | + operation: 'team_deletion', |
| 330 | + teamId, |
| 331 | + totalInstallations: installations.length, |
| 332 | + installationsDeleted: deletedCount, |
| 333 | + satelliteCommandsCreated: totalCommands |
| 334 | + }, 'MCP installations deleted and satellite commands created for team deletion'); |
| 335 | + |
| 336 | + } catch (error) { |
| 337 | + logger.error(error, 'Failed to delete MCP installations - proceeding with team deletion anyway'); |
| 338 | + // Don't fail team deletion if installation deletion fails |
| 339 | + } |
| 340 | + |
| 341 | + // Handle satellite commands - preserve pending commands so satellites can pick them up |
| 342 | + // Set target_team_id to NULL for pending commands (allows team deletion without blocking satellite execution) |
| 343 | + await (db as any) |
| 344 | + .update(schema.satelliteCommands) |
| 345 | + .set({ target_team_id: null }) |
| 346 | + .where( |
| 347 | + and( |
| 348 | + eq(schema.satelliteCommands.target_team_id, teamId), |
| 349 | + eq(schema.satelliteCommands.status, 'pending') |
| 350 | + ) |
| 351 | + ); |
| 352 | + |
| 353 | + // Delete non-pending satellite commands (completed/failed/executing) since they're no longer needed |
| 354 | + await (db as any) |
| 355 | + .delete(schema.satelliteCommands) |
| 356 | + .where( |
| 357 | + and( |
| 358 | + eq(schema.satelliteCommands.target_team_id, teamId), |
| 359 | + or( |
| 360 | + eq(schema.satelliteCommands.status, 'completed'), |
| 361 | + eq(schema.satelliteCommands.status, 'failed'), |
| 362 | + eq(schema.satelliteCommands.status, 'acknowledged'), |
| 363 | + eq(schema.satelliteCommands.status, 'executing') |
| 364 | + ) |
| 365 | + ) |
| 366 | + ); |
| 367 | + |
| 368 | + // Delete satellite processes for this team |
| 369 | + await (db as any) |
| 370 | + .delete(schema.satelliteProcesses) |
| 371 | + .where(eq(schema.satelliteProcesses.team_id, teamId)); |
| 372 | + |
| 373 | + // Delete satellite usage logs for this team |
| 374 | + await (db as any) |
| 375 | + .delete(schema.satelliteUsageLogs) |
| 376 | + .where(eq(schema.satelliteUsageLogs.team_id, teamId)); |
| 377 | + |
| 378 | + // Delete team memberships (cascade should handle this, but being explicit) |
287 | 379 | await (db as any) |
288 | 380 | .delete(schema.teamMemberships) |
289 | 381 | .where(eq(schema.teamMemberships.team_id, teamId)); |
|
0 commit comments