@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
851 lines • 38.4 kB
JavaScript
/**
* 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