UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

247 lines 10.2 kB
import { supabase } from '../db/supabase.js'; import { z } from 'zod'; import { logger } from '../utils/logger.js'; // Transaction result schemas const TransactionResultSchema = z.object({ success: z.boolean(), error: z.string().optional(), error_detail: z.string().optional() }); const CreateProjectResultSchema = TransactionResultSchema.extend({ project_id: z.string().uuid().optional(), projection_id: z.string().uuid().optional(), use_case_count: z.number().optional(), total_investment: z.number().optional(), message: z.string().optional() }); const UpdateProjectResultSchema = TransactionResultSchema.extend({ project_id: z.string().uuid().optional(), use_cases_added: z.number().optional(), use_cases_updated: z.number().optional(), use_cases_deleted: z.number().optional(), projection_marked_for_update: z.boolean().optional() }); const DeleteProjectResultSchema = TransactionResultSchema.extend({ project_id: z.string().uuid().optional(), would_delete: z.object({ use_cases: z.number(), projections: z.number(), simulations: z.number(), metrics: z.number() }).optional(), deleted: z.object({ use_cases: z.number(), projections: z.number(), simulations: z.number(), metrics: z.number() }).optional(), message: z.string().optional() }); const ValidationResultSchema = z.object({ valid: z.boolean(), errors: z.array(z.object({ field: z.string(), message: z.string() })), warnings: z.array(z.object({ field: z.string(), message: z.string() })) }); export class TransactionService { /** * Creates a project with all its use cases atomically * @param projectData Project details * @param useCases Array of use cases * @param implementationCosts Implementation cost breakdown * @param timelineMonths Project timeline in months * @param confidenceLevel Confidence level (0-1) * @returns Transaction result with project and projection IDs */ static async createProjectWithDetails(projectData, useCases, implementationCosts, timelineMonths, confidenceLevel = 0.95) { try { const { data, error } = await supabase.rpc('create_project_with_details', { p_project: projectData, p_use_cases: useCases, p_implementation_costs: implementationCosts, p_timeline_months: timelineMonths, p_confidence_level: confidenceLevel }); if (error) { throw new Error(`Database transaction failed: ${error.message}`); } const result = CreateProjectResultSchema.parse(data); if (!result.success) { throw new Error(result.error || 'Transaction failed'); } return result; } catch (error) { logger.error('Transaction error in createProjectWithDetails', error); throw error; } } /** * Updates a project and its use cases atomically * @param projectId UUID of the project * @param projectUpdates Optional project field updates * @param useCasesToAdd Optional new use cases to add * @param useCasesToUpdate Optional use cases to update (must include id) * @param useCasesToDelete Optional array of use case IDs to delete * @param regenerateProjection Whether to mark projections for recalculation * @returns Transaction result with operation counts */ static async updateProjectWithUseCases(projectId, projectUpdates, useCasesToAdd, useCasesToUpdate, useCasesToDelete, regenerateProjection = false) { try { const { data, error } = await supabase.rpc('update_project_with_use_cases', { p_project_id: projectId, p_project_updates: projectUpdates || null, p_use_cases_to_add: useCasesToAdd || null, p_use_cases_to_update: useCasesToUpdate || null, p_use_cases_to_delete: useCasesToDelete || null, p_regenerate_projection: regenerateProjection }); if (error) { throw new Error(`Database transaction failed: ${error.message}`); } const result = UpdateProjectResultSchema.parse(data); if (!result.success) { throw new Error(result.error || 'Transaction failed'); } return result; } catch (error) { logger.error('Transaction error in updateProjectWithUseCases', error); throw error; } } /** * Deletes a project and all related data atomically * @param projectId UUID of the project to delete * @param confirmDelete Must be true to actually delete (false returns what would be deleted) * @returns Transaction result with deletion summary */ static async deleteProjectCascade(projectId, confirmDelete = false) { try { const { data, error } = await supabase.rpc('delete_project_cascade', { p_project_id: projectId, p_confirm_delete: confirmDelete }); if (error) { throw new Error(`Database transaction failed: ${error.message}`); } const result = DeleteProjectResultSchema.parse(data); if (!result.success && confirmDelete) { throw new Error(result.error || 'Deletion failed'); } return result; } catch (error) { logger.error('Transaction error in deleteProjectCascade', error); throw error; } } /** * Validates project and use case data before creation/update * @param projectData Project details to validate * @param useCases Array of use cases to validate * @returns Validation result with errors and warnings */ static async validateProjectData(projectData, useCases) { try { const { data, error } = await supabase.rpc('validate_project_data', { p_project: projectData, p_use_cases: useCases }); if (error) { throw new Error(`Validation failed: ${error.message}`); } return ValidationResultSchema.parse(data); } catch (error) { logger.error('Validation error in validateProjectData', error); throw error; } } /** * Creates a projection with optional simulation setup * @param projectId UUID of the project * @param projectionData Projection details * @param runSimulation Whether to create a simulation placeholder * @param simulationIterations Number of Monte Carlo iterations * @returns Transaction result with projection ID */ static async createProjectionWithSimulation(projectId, projectionData, runSimulation = false, simulationIterations = 10000) { try { const { data, error } = await supabase.rpc('create_projection_with_simulation', { p_project_id: projectId, p_projection_data: projectionData, p_run_simulation: runSimulation, p_simulation_iterations: simulationIterations }); if (error) { throw new Error(`Database transaction failed: ${error.message}`); } const result = TransactionResultSchema.parse(data); if (!result.success) { throw new Error(result.error || 'Transaction failed'); } return data; // Return full data for projection_id and simulation_id } catch (error) { logger.error('Transaction error in createProjectionWithSimulation', error); throw error; } } /** * Performs a batch operation with automatic rollback on failure * @param operations Array of operations to perform * @returns Results of all operations */ static async batchOperation(operations) { const results = []; const rollbacks = []; try { for (const operation of operations) { const result = await operation(); results.push(result); } return { success: true, results }; } catch (error) { // In a real implementation, you'd execute rollback operations here // For now, we just throw the error logger.error('Batch operation failed', error); throw error; } } } // Export convenience functions for common operations export async function createProjectTransaction(projectData, useCases, implementationCosts, timelineMonths, confidenceLevel) { // First validate the data const validation = await TransactionService.validateProjectData(projectData, useCases); if (!validation.valid) { throw new Error(`Validation failed: ${JSON.stringify(validation.errors)}`); } // Log warnings if any if (validation.warnings.length > 0) { logger.warn('Validation warnings', { warnings: validation.warnings }); } // Create the project return TransactionService.createProjectWithDetails(projectData, useCases, implementationCosts, timelineMonths, confidenceLevel); } export async function updateProjectTransaction(projectId, updates) { return TransactionService.updateProjectWithUseCases(projectId, updates.project, updates.addUseCases, updates.updateUseCases, updates.deleteUseCaseIds, updates.regenerateProjection); } export async function safeDeleteProject(projectId) { // First check what would be deleted const preview = await TransactionService.deleteProjectCascade(projectId, false); // Log what will be deleted logger.info('Project deletion preview', { preview }); // Actually delete return TransactionService.deleteProjectCascade(projectId, true); } //# sourceMappingURL=transaction-service.js.map