UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

851 lines 38.4 kB
/** * Entity Migration Orchestrator * @description Core engine that coordinates entity migration between Optimizely projects * using dual OptimizelyMCPTools instances and intelligent dependency management */ import { getLogger } from '../logging/Logger.js'; import { ENTITY_DEPENDENCY_ORDER, DEFAULT_MIGRATION_OPTIONS, PROGRESS_CONFIG, ENTITY_KEY_FIELDS, PROGRESS_MESSAGES, REFERENCE_FIELDS, MIGRATION_SEARCH_FIELDS } from './constants.js'; export class EntityMigrationOrchestrator { logger = getLogger(); sourceTools; destinationTools; config; progress; executionPlan; createdEntities = new Map(); // sourceId → destId progressCallback; checkpointSaveInterval; constructor(sourceTools, destinationTools, config) { this.sourceTools = sourceTools; this.destinationTools = destinationTools; this.config = { ...config, options: { ...DEFAULT_MIGRATION_OPTIONS, ...config.options } }; // Initialize progress tracking this.progress = { phase: 'initializing', overall: { totalEntities: 0, completedEntities: 0, failedEntities: 0, skippedEntities: 0 }, performance: { startTime: Date.now(), elapsedTime: 0 }, errors: [] }; // Restore from checkpoint if provided if (config.resumeFrom) { this.restoreFromCheckpoint(config.resumeFrom); } } /** * Set progress callback for real-time updates */ setProgressCallback(callback) { this.progressCallback = callback; // Start checkpoint save interval if (this.config.options?.continueOnError) { this.checkpointSaveInterval = setInterval(() => { this.saveCheckpoint(); }, PROGRESS_CONFIG.CHECKPOINT_INTERVAL * 1000); } } /** * Execute the migration */ async execute() { const startTime = Date.now(); try { // Phase 1: Initialize and validate await this.initialize(); // Phase 2: Analyze dependencies and build execution plan await this.analyzeAndPlan(); // Phase 3: Validate configuration if (this.config.options?.validateOnly) { return this.createValidationResult(); } // Phase 4: Execute migration await this.migrate(); // Phase 5: Generate final report return this.config.options?.dryRun ? this.createDryRunResult() : this.createSuccessResult(); } catch (error) { this.logger.error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`); // Rollback if configured if (this.config.options?.rollbackStrategy === 'all-or-nothing') { await this.rollback(); } return this.createErrorResult(error); } finally { // Clean up if (this.checkpointSaveInterval) { clearInterval(this.checkpointSaveInterval); } } } /** * Initialize migration engine */ async initialize() { this.updateProgress('initializing', PROGRESS_MESSAGES.INITIALIZING); // Validate source project exists and get platform // Use getProjectData which returns the project info directly const sourceProjectData = await this.sourceTools.getProjectData(this.config.source.projectId); if (!sourceProjectData || sourceProjectData.result !== 'success' || !sourceProjectData.project) { throw new Error(`Source project ${this.config.source.projectId} not found`); } const sourceProject = sourceProjectData.project; // Validate destination project exists const destProjectData = await this.destinationTools.getProjectData(this.config.destination.projectId); if (!destProjectData || destProjectData.result !== 'success' || !destProjectData.project) { throw new Error(`Destination project ${this.config.destination.projectId} not found`); } const destProject = destProjectData.project; // Check platform compatibility const sourcePlatform = sourceProject.platform || sourceProject.is_flags_enabled ? 'feature' : 'web'; const destPlatform = destProject.platform || destProject.is_flags_enabled ? 'feature' : 'web'; if (sourcePlatform !== destPlatform) { throw new Error(`Platform mismatch: source is ${sourcePlatform}, destination is ${destPlatform}. ` + `Cross-platform migration is not supported.`); } this.logger.info(`Migration initialized - Source: ${this.config.source.projectId} (${sourceProject.name}, ${sourcePlatform}), Destination: ${this.config.destination.projectId} (${destProject.name}, ${destPlatform})`); } /** * Analyze dependencies and build execution plan */ async analyzeAndPlan() { this.updateProgress('analyzing', PROGRESS_MESSAGES.ANALYZING_DEPENDENCIES); const phases = []; const dependencies = {}; let totalOperations = 0; // Determine which entity types to migrate const entityTypes = this.config.entities?.types || ENTITY_DEPENDENCY_ORDER; // Build execution phases based on dependency order for (const entityType of ENTITY_DEPENDENCY_ORDER) { if (!entityTypes.includes(entityType)) continue; const operations = []; // Get entities using efficient lookup const specificIds = this.config.entities?.ids?.[entityType]; let entitiesToMigrate = []; if (specificIds && specificIds.length > 0) { // Efficient: Fetch only the specific entities we need this.logger.info(`Migration lookup for ${entityType}: fetching ${specificIds.length} specific entities using ${MIGRATION_SEARCH_FIELDS[entityType]} field`); for (const entityId of specificIds) { const entity = await this.findEntityForMigration(entityType, entityId, this.config.source.projectId, this.sourceTools); if (entity) { entitiesToMigrate.push(entity); } else { this.logger.warn(`${entityType} '${entityId}' not found in source project ${this.config.source.projectId}`); } } } else { // Fallback: Fetch all entities (only when no specific IDs provided) this.logger.info(`Migration lookup for ${entityType}: fetching all entities (no specific IDs provided)`); const sourceEntities = await this.fetchSourceEntities(entityType); entitiesToMigrate = sourceEntities; } this.logger.info(`Migration entities for ${entityType}: found ${entitiesToMigrate.length} entities to process`); // Check each entity against destination for (const entity of entitiesToMigrate) { const searchField = MIGRATION_SEARCH_FIELDS[entityType]; const entityKey = entity[searchField]; // Check if entity exists in destination const existsInDest = await this.checkEntityExists(entityType, entityKey); // Determine operation based on conflict resolution strategy let operation; if (existsInDest) { operation = await this.resolveConflict(entityType, entityKey, entity); } else { operation = 'create'; } const entityOperation = { type: entityType, id: entityKey, name: entity.name || entityKey, dependencies: this.extractDependencies(entityType, entity), operation }; operations.push(entityOperation); totalOperations++; // Build dependency map dependencies[entityKey] = { type: entityType, dependsOn: entityOperation.dependencies, requiredBy: [] }; } if (operations.length > 0) { phases.push({ name: `Migrate ${entityType}s`, order: phases.length, entities: operations, dependencies: this.getEntityDependencies(entityType) }); } } // Update dependency graph with reverse dependencies Object.entries(dependencies).forEach(([entityId, info]) => { info.dependsOn.forEach((depId) => { if (dependencies[depId]) { dependencies[depId].requiredBy.push(entityId); } }); }); this.executionPlan = { phases, totalOperations, dependencies }; this.progress.overall.totalEntities = totalOperations; this.updateProgress('analyzing', `Execution plan created: ${totalOperations} operations across ${phases.length} phases`); } /** * Execute the migration plan */ async migrate() { if (!this.executionPlan) { throw new Error('No execution plan available'); } this.updateProgress('migrating', 'Starting migration...'); // Execute each phase in order for (const phase of this.executionPlan.phases) { this.logger.info(`Executing phase: ${phase.name}`); for (let i = 0; i < phase.entities.length; i++) { const operation = phase.entities[i]; // Update progress this.updateProgress('migrating', PROGRESS_MESSAGES.CREATING_ENTITY .replace('{type}', operation.type) .replace('{name}', operation.name || operation.id), { currentEntity: { type: operation.type, id: operation.id, name: operation.name, index: this.progress.overall.completedEntities + 1, total: this.progress.overall.totalEntities } }); try { if (operation.operation === 'skip') { this.progress.overall.skippedEntities++; this.logger.info(`Skipping existing entity: ${operation.type} ${operation.id}`); continue; } if (this.config.options?.dryRun) { this.logger.info(`[DRY RUN] Would ${operation.operation} ${operation.type}: ${operation.id}`); continue; } // Execute the operation await this.migrateEntity(operation); this.progress.overall.completedEntities++; // Save checkpoint periodically if (this.progress.overall.completedEntities % PROGRESS_CONFIG.CHECKPOINT_INTERVAL === 0) { await this.saveCheckpoint(); } } catch (error) { this.progress.overall.failedEntities++; const migrationError = { timestamp: new Date().toISOString(), entityType: operation.type, entityId: operation.id, operation: operation.operation, error: error.message, recoverable: this.isRecoverableError(error), retryCount: 0 }; this.progress.errors.push(migrationError); if (!this.config.options?.continueOnError) { throw error; } this.logger.error(`Failed to migrate ${operation.type} ${operation.id}`, error); } } } this.updateProgress('finalizing', PROGRESS_MESSAGES.MIGRATION_COMPLETE); } /** * Migrate a single entity */ async migrateEntity(operation) { // Fetch full entity from source const sourceEntity = await this.fetchFullEntity(operation.type, operation.id); // Create transformation context const context = { entityType: operation.type, sourceEntity, mappings: this.config.mappings || {}, createdEntities: this.createdEntities }; // Transform references const transformedEntity = await this.transformReferences(context); // Create or update in destination let result; if (operation.operation === 'create') { result = await this.createInDestination(operation.type, transformedEntity); } else { result = await this.updateInDestination(operation.type, operation.id, transformedEntity); } // Store mapping for future reference if (result && (result.result === 'success' || result.data)) { // Handle both direct response and envelope format const entityData = result.data || result; const keyField = ENTITY_KEY_FIELDS[operation.type]; const destId = entityData[keyField] || entityData.id; this.createdEntities.set(operation.id, destId); // Update mappings in config for persistence if (!this.config.mappings) { this.config.mappings = {}; } if (!this.config.mappings[operation.type + 's']) { this.config.mappings[operation.type + 's'] = {}; } this.config.mappings[operation.type + 's'][operation.id] = destId; } } /** * Find a specific entity for migration using semantic identifiers */ async findEntityForMigration(entityType, entityId, projectId, tools) { const searchField = MIGRATION_SEARCH_FIELDS[entityType]; try { // First try to get the entity directly using the semantic identifier // getEntityDetails supports searching by key, name, or ID for most entities const result = await tools.getEntityDetails(entityType, entityId, projectId); if (result) { // The entity data is at result.entity_data.data based on logs const entity = result.entity_data.data; // Verify this is the entity we're looking for by checking the search field if (entity && entity[searchField] === entityId) { this.logger.debug(`Found ${entityType} '${entityId}' using getEntityDetails`); return entity; } } } catch (error) { // Entity not found via direct lookup, this is expected for some cases this.logger.debug(`Entity ${entityType} '${entityId}' not found via direct lookup: ${error instanceof Error ? error.message : String(error)}`); } // If direct lookup failed, entity doesn't exist this.logger.info(`${entityType} '${entityId}' not found in project ${projectId}`); return null; } /** * Fetch entities from source project */ async fetchSourceEntities(entityType) { const result = await this.sourceTools.listEntities(entityType, this.config.source.projectId, { includeArchived: this.config.entities?.includeArchived }); // DEBUG: Log what we actually received getLogger().info({ entityType, projectId: this.config.source.projectId, resultType: typeof result, isArray: Array.isArray(result), hasResultProperty: result && typeof result === 'object' && 'result' in result, hasEntitiesProperty: result && typeof result === 'object' && 'entities' in result, resultValue: result && typeof result === 'object' ? result.result : undefined, entitiesType: result && typeof result === 'object' && result.entities ? typeof result.entities : undefined, entitiesIsArray: result && typeof result === 'object' && result.entities ? Array.isArray(result.entities) : undefined, // Don't log the full result as it might be huge, just the structure resultKeys: result && typeof result === 'object' ? Object.keys(result) : undefined }, 'EntityMigrationOrchestrator.fetchSourceEntities: Debug response format'); // Handle different response formats from listEntities // Format 1: [...] (raw array for subsequent calls or simple entities) // Format 2: {result: "success", entities: [...]} (wrapped response) // Format 3: {result: "success", entities: [...], _usage_guidance: {...}} (first-time complex entities) // Format 4: {result: "success", data: [...]} (alternative wrapped format) // Format 5: {data: [...]} (data wrapper without result) // Format 6: Special experiment formats from handleSmartExperimentRouting if (Array.isArray(result)) { // For experiments, filter out guidance objects that might be prepended if (entityType === 'experiment') { const filteredResult = result.filter(item => { // Filter out objects that are guidance/metadata, not actual entities return !item._mcp_guidance && !item._smartRouting; }); // If we filtered everything out, it might be a Feature Experimentation project if (filteredResult.length === 0 && result.length > 0) { // Check if the first item has experiment data embedded const firstItem = result[0]; if (firstItem._smartRouting && firstItem.experimentSummary) { getLogger().info({ entityType, projectType: 'feature', experimentCount: firstItem.experimentSummary.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Feature Experimentation project - returning experiment summaries'); return firstItem.experimentSummary; } } getLogger().info({ entityType, originalCount: result.length, filteredCount: filteredResult.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Filtered experiment array'); return filteredResult; } getLogger().info({ entityType, count: result.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning raw array'); return result; } if (result && typeof result === 'object') { // Check for success response with entities as array if (result.result === 'success' && Array.isArray(result.entities)) { // For experiments, filter out guidance objects if (entityType === 'experiment') { const filteredEntities = result.entities.filter((item) => { return !item._mcp_guidance && !item._smartRouting; }); getLogger().info({ entityType, originalCount: result.entities.length, filteredCount: filteredEntities.length, hasGuidance: !!result._usage_guidance }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning filtered entities from success response'); return filteredEntities; } getLogger().info({ entityType, count: result.entities.length, hasGuidance: !!result._usage_guidance }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning entities from success response'); return result.entities; } // Check for success response with entities as object containing data if (result.result === 'success' && result.entities && typeof result.entities === 'object' && Array.isArray(result.entities.data)) { // For experiments, filter out guidance objects if (entityType === 'experiment') { const filteredEntities = result.entities.data.filter((item) => { return !item._mcp_guidance && !item._smartRouting; }); getLogger().info({ entityType, originalCount: result.entities.data.length, filteredCount: filteredEntities.length, hasGuidance: !!result._usage_guidance }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning filtered entities from nested entities.data'); return filteredEntities; } getLogger().info({ entityType, count: result.entities.data.length, hasGuidance: !!result._usage_guidance }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning entities from nested entities.data'); return result.entities.data; } // Check for success response with data if (result.result === 'success' && Array.isArray(result.data)) { getLogger().info({ entityType, count: result.data.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning data from success response'); return result.data; } // Check for data wrapper without result field if (Array.isArray(result.data)) { getLogger().info({ entityType, count: result.data.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning data from wrapper'); return result.data; } // Check for entities without result field if (Array.isArray(result.entities)) { // For experiments, filter out guidance objects if (entityType === 'experiment') { const filteredEntities = result.entities.filter((item) => { return !item._mcp_guidance && !item._smartRouting; }); getLogger().info({ entityType, originalCount: result.entities.length, filteredCount: filteredEntities.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning filtered entities from wrapper'); return filteredEntities; } getLogger().info({ entityType, count: result.entities.length }, 'EntityMigrationOrchestrator.fetchSourceEntities: Returning entities from wrapper'); return result.entities; } if (result.result === 'error') { throw new Error(`Failed to fetch ${entityType} entities from source: ${result.error?.message || result.message || 'Unknown error'}`); } } // Log the actual problematic result for debugging getLogger().error({ entityType, projectId: this.config.source.projectId, result: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }, 'EntityMigrationOrchestrator.fetchSourceEntities: Invalid response format, full result logged'); throw new Error(`Invalid response format from listEntities for ${entityType}: expected array or success response with entities`); } /** * Fetch full entity details */ async fetchFullEntity(entityType, entityId) { const result = await this.sourceTools.getEntityDetails(entityType, entityId, this.config.source.projectId); // getEntityDetails returns the entity directly or an enhanced result with entity_data if (!result) { throw new Error(`Failed to fetch ${entityType} ${entityId}: No data returned`); } // Check if it's an enhanced result with entity_data or direct entity data return result.entity_data || result; } /** * Check if entity exists in destination */ async checkEntityExists(entityType, entityKey) { try { const result = await this.destinationTools.getEntityDetails(entityType, entityKey, this.config.destination.projectId); // Entity exists if we got any result back return !!result; } catch (error) { // Entity doesn't exist return false; } } /** * Transform entity references using mappings */ async transformReferences(context) { const transformed = { ...context.sourceEntity }; const referenceFields = REFERENCE_FIELDS[context.entityType] || []; for (const field of referenceFields) { if (!transformed[field]) continue; if (Array.isArray(transformed[field])) { transformed[field] = transformed[field].map((ref) => this.translateReference(ref, field, context)); } else { transformed[field] = this.translateReference(transformed[field], field, context); } } // Remove source-specific fields delete transformed.id; delete transformed.created; delete transformed.last_modified; delete transformed.project_id; return transformed; } /** * Translate a single reference */ translateReference(ref, field, context) { // Determine target entity type from field name const targetType = this.getTargetTypeFromField(field); if (!targetType) return ref; // Check mappings const mappingKey = targetType + 's'; const mapping = context.mappings[mappingKey]?.[ref] || context.createdEntities.get(ref); if (mapping) { return mapping; } // If no mapping found and it's required, throw error if (!this.config.options?.continueOnError) { throw new Error(`Missing mapping for ${targetType} reference: ${ref} in field ${field}`); } this.logger.warn(`No mapping found for ${targetType} ${ref}, using original value`); return ref; } /** * Create entity in destination */ async createInDestination(entityType, entity) { return await this.destinationTools.manageEntityLifecycle('create', entityType, entity, undefined, this.config.destination.projectId); } /** * Update entity in destination */ async updateInDestination(entityType, entityId, entity) { return await this.destinationTools.manageEntityLifecycle('update', entityType, entity, entityId, this.config.destination.projectId); } /** * Resolve conflict when entity exists in destination */ async resolveConflict(entityType, entityKey, entity) { const conflictResolution = this.config.options?.conflictResolution; // If not in ask mode, use the configured strategy if (conflictResolution !== 'ask' || !this.config.options?.interactiveMode) { switch (conflictResolution) { case 'update': return 'update'; case 'create-copy': return 'create'; // Will be handled with modified key case 'skip': default: return 'skip'; } } // Interactive mode: Ask user for decision using HardStopError const { HardStopError, HardStopErrorType } = await import('../errors/HardStopError.js'); const entityDisplayName = entity.name || entityKey; const message = `Entity "${entityDisplayName}" (${entityType}) already exists in destination project.\n\nHow would you like to handle this conflict?`; const hardStopError = new HardStopError(HardStopErrorType.ENTITY_ALREADY_EXISTS, 'MIGRATION_CONFLICT_RESOLUTION', `${message}\n\nOptions:\n1. Skip - Leave existing entity unchanged\n2. Update - Overwrite existing entity\n3. Create Copy - Create new entity with modified name`, 'ASK_USER', 409, ['retry', 'continue', 'auto_fix'] // Forbidden actions ); // This will cause the tool to return an error asking for user input // The user's response will be handled in the next migration call throw hardStopError; } /** * Extract dependencies from entity */ extractDependencies(entityType, entity) { const dependencies = []; const referenceFields = REFERENCE_FIELDS[entityType] || []; for (const field of referenceFields) { if (!entity[field]) continue; if (Array.isArray(entity[field])) { dependencies.push(...entity[field]); } else { dependencies.push(entity[field]); } } return [...new Set(dependencies)]; // Remove duplicates } /** * Get entity type dependencies */ getEntityDependencies(entityType) { const index = ENTITY_DEPENDENCY_ORDER.indexOf(entityType); return index > 0 ? ENTITY_DEPENDENCY_ORDER.slice(0, index) : []; } /** * Get target entity type from field name */ getTargetTypeFromField(field) { const mappings = { 'audience_ids': 'audience', 'audience_id': 'audience', 'event_ids': 'event', 'event_id': 'event', 'page_ids': 'page', 'page_id': 'page', 'attribute_ids': 'attribute', 'attribute_id': 'attribute', 'flag_key': 'flag', 'experiment_id': 'experiment', 'campaign_id': 'campaign', 'extension_id': 'extension', 'group_id': 'group' }; return mappings[field] || null; } /** * Update progress */ updateProgress(phase, message, additionalData) { this.progress = { ...this.progress, ...additionalData, phase, performance: { ...this.progress.performance, elapsedTime: Date.now() - this.progress.performance.startTime } }; this.logger.info(`[${phase}] ${message}`); if (this.progressCallback) { this.progressCallback(this.progress); } } /** * Save checkpoint for resume capability */ async saveCheckpoint() { const checkpoint = { id: `migration-${Date.now()}`, timestamp: new Date().toISOString(), phase: this.progress.phase, completedEntities: Object.entries(this.createdEntities).map(([sourceId, destId]) => ({ type: 'unknown', // Would need to track this sourceId, destinationId: destId, timestamp: new Date().toISOString() })), mappingState: Object.entries(this.config.mappings || {}).reduce((acc, [key, value]) => { if (value) acc[key] = value; return acc; }, {}) }; this.progress.checkpoint = checkpoint; this.updateProgress(this.progress.phase, PROGRESS_MESSAGES.CHECKPOINT_SAVED); } /** * Restore from checkpoint */ restoreFromCheckpoint(checkpoint) { // Restore created entities mapping checkpoint.completedEntities.forEach(entity => { this.createdEntities.set(entity.sourceId, entity.destinationId); }); // Restore mappings this.config.mappings = checkpoint.mappingState; this.logger.info(`Restored from checkpoint: ${checkpoint.id} with ${checkpoint.completedEntities.length} completed entities`); } /** * Rollback created entities */ async rollback() { this.updateProgress('migrating', PROGRESS_MESSAGES.ROLLING_BACK); // Delete entities in reverse order const entitiesToRollback = Array.from(this.createdEntities.entries()).reverse(); for (const [sourceId, destId] of entitiesToRollback) { try { // Need to determine entity type - would require tracking this this.logger.info(`Rolling back entity ${destId}`); // await this.destinationTools.manageEntityLifecycle({ // action: 'delete', // entityType: entityType, // entityId: destId, // projectId: this.config.destination.projectId // }); } catch (error) { this.logger.error(`Failed to rollback ${destId}: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Check if error is recoverable */ isRecoverableError(error) { const recoverableErrors = [ 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', '429' // Rate limit ]; return recoverableErrors.some(code => error.code === code || error.message?.includes(code)); } /** * Create validation result */ createValidationResult() { return { id: `validation-${Date.now()}`, success: true, startTime: new Date(this.progress.performance.startTime).toISOString(), endTime: new Date().toISOString(), duration: this.progress.performance.elapsedTime, source: { projectId: this.config.source.projectId }, destination: { projectId: this.config.destination.projectId }, summary: { totalEntities: this.progress.overall.totalEntities, successfulEntities: 0, failedEntities: 0, skippedEntities: 0 }, entities: [], errors: [] }; } /** * Create success result */ createSuccessResult() { return { id: `migration-${Date.now()}`, success: true, startTime: new Date(this.progress.performance.startTime).toISOString(), endTime: new Date().toISOString(), duration: this.progress.performance.elapsedTime, source: { projectId: this.config.source.projectId }, destination: { projectId: this.config.destination.projectId }, summary: { totalEntities: this.progress.overall.totalEntities, successfulEntities: this.config.options?.dryRun ? 0 : this.progress.overall.completedEntities, failedEntities: this.config.options?.dryRun ? 0 : this.progress.overall.failedEntities, skippedEntities: this.config.options?.dryRun ? 0 : this.progress.overall.skippedEntities }, entities: [], // Would need to track individual entities errors: this.progress.errors, checkpoint: this.progress.checkpoint }; } /** * Create dry run result with simulation data */ createDryRunResult() { return { id: `dryrun-${Date.now()}`, success: true, startTime: new Date(this.progress.performance.startTime).toISOString(), endTime: new Date().toISOString(), duration: this.progress.performance.elapsedTime, source: { projectId: this.config.source.projectId }, destination: { projectId: this.config.destination.projectId }, summary: { totalEntities: this.executionPlan?.totalOperations || 0, successfulEntities: 0, // Dry run never completes entities failedEntities: 0, skippedEntities: 0 }, entities: [], errors: [], checkpoint: undefined }; } /** * Create error result */ createErrorResult(error) { return { id: `migration-${Date.now()}`, success: false, startTime: new Date(this.progress.performance.startTime).toISOString(), endTime: new Date().toISOString(), duration: this.progress.performance.elapsedTime, source: { projectId: this.config.source.projectId }, destination: { projectId: this.config.destination.projectId }, summary: { totalEntities: this.progress.overall.totalEntities, successfulEntities: this.progress.overall.completedEntities, failedEntities: this.progress.overall.failedEntities, skippedEntities: this.progress.overall.skippedEntities }, entities: [], errors: [ ...this.progress.errors, { timestamp: new Date().toISOString(), entityType: 'unknown', entityId: 'migration', operation: 'execute', error: error.message, recoverable: false } ], checkpoint: this.progress.checkpoint }; } } //# sourceMappingURL=EntityMigrationOrchestrator.js.map