agmission/Development/server/scripts/rollbackMigration.js

369 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
/**
* Rollback Migration Script
*
* This script reverts the last customer migration by reading the migration_history.json
* and reversing all changes.
*
* Rollback Operations:
* - migrate_products/crops: Restores byPuid to original parent
* - If --reuse-existing-entities was used, also recreates deleted entities
* - migrate_jobs: Restores byPuid to original parent
* - migrate_invoices: Restores each invoice to its original byPuid
* - update_parent_reference: Restores sub-account parent references
* - convert_customer_to_admin: Restores kind and parent fields
* - deactivate_source_customer: Restores customer to active state
*
* Usage:
* node scripts/rollbackMigration.js
* node scripts/rollbackMigration.js --env ../environment_prod.env
*
* Options:
* --env Custom environment file path (default: ../environment.env)
*/
const path = require('path');
const args = process.argv.slice(2);
const envArgIndex = args.indexOf('--env');
const customEnvPath = envArgIndex !== -1 && args[envArgIndex + 1] ? args[envArgIndex + 1] : null;
const envPath = customEnvPath
? (path.isAbsolute(customEnvPath) ? customEnvPath : path.join(process.cwd(), customEnvPath))
: path.join(__dirname, '../environment.env');
console.log(`Loading environment from: ${envPath}`);
require('dotenv').config({ path: envPath });
const debug = require('debug')('agm:rollback-migration');
const mongoose = require('mongoose');
const { DBConnection } = require('../helpers/db/connect');
const mongoEnhanced = require('../helpers/mongo_enhanced');
const fs = require('fs').promises;
// Import models
const models = {
User: require('../model/user'),
Customer: require('../model/customer'),
Job: require('../model/job'),
Product: require('../model/product'),
Crop: require('../model/crop'),
CostingItem: require('../model/costing_items'),
Invoice: require('../model/invoice')
};
const DEFAULT_HISTORY_FILE = path.join(__dirname, '../migration_history.json');
/**
* Load the last migration from history file
*/
async function loadLastMigration(historyFile) {
try {
const content = await fs.readFile(historyFile, 'utf8');
const migrations = JSON.parse(content);
if (!Array.isArray(migrations) || migrations.length === 0) {
throw new Error('No migrations found in history file');
}
// Get the last migration
const lastMigration = migrations[migrations.length - 1];
if (!lastMigration.completedAt) {
throw new Error('Last migration has not been completed yet');
}
return lastMigration;
} catch (error) {
throw new Error(`Failed to read migration history: ${error.message}`);
}
}
/**
* Rollback the migration
*/
async function rollbackMigration(migrationRecord) {
console.log('\n================================================================================');
console.log('ROLLBACK MIGRATION');
console.log('================================================================================\n');
console.log(`Migration Date: ${migrationRecord.timestamp}`);
console.log(`Completed At: ${migrationRecord.completedAt}`);
console.log(`Source Customer IDs: ${migrationRecord.sourceCustomerIds.join(', ')}`);
console.log(`Target Customer ID: ${migrationRecord.targetCustomerId}`);
console.log('\nReverting changes...\n');
await mongoEnhanced.enhancedRunInTransaction(async (session) => {
debug('Starting rollback transaction...');
const changes = migrationRecord.details.changes;
let revertedCount = 0;
// Process changes in reverse order
for (let i = changes.length - 1; i >= 0; i--) {
const change = changes[i];
switch (change.action) {
case 'deactivate_source_customer':
console.log(` → Reactivating source customer: ${change.customerId}`);
await models.User.updateOne(
{ _id: change.customerId },
{
$set: {
username: change.oldUsername,
markedDelete: false,
active: true
}
},
{ session }
);
revertedCount++;
break;
case 'delete_partner_system_users':
console.log(` → Restoring ${change.count} deleted partner system users`);
if (change.deletedUsers && change.deletedUsers.length > 0) {
for (const userData of change.deletedUsers) {
// Check if user already exists before trying to restore
const existingUser = await models.User.findById(userData._id).session(session);
if (!existingUser) {
// Remove version key and other mongoose internals
const { __v, ...cleanData } = userData;
await models.User.create([cleanData], { session });
} else {
console.log(` → Partner system user ${userData.username || userData.partnerUsername} already exists, skipping restore`);
}
}
}
revertedCount++;
break;
case 'update_parent_reference':
console.log(` → Reverting parent reference for account: ${change.accountId}`);
await models.User.updateOne(
{ _id: change.accountId },
{ $set: { parent: change.fromParent } },
{ session }
);
revertedCount++;
break;
case 'migrate_products':
console.log(` → Reverting ${change.count} products`);
// Restore migrated products to original parent
if (change.productIds && change.productIds.length > 0) {
// Use specific product IDs to revert only the migrated products
await models.Product.updateMany(
{ _id: { $in: change.productIds } },
{ $set: { byPuid: change.oldParent } },
{ session }
);
}
// Restore deleted products (that were replaced by destination entities)
if (change.deletedProducts && change.deletedProducts.length > 0) {
console.log(` → Restoring ${change.deletedProducts.length} deleted products`);
for (const productData of change.deletedProducts) {
// Check if product already exists before trying to restore
const existingProduct = await models.Product.findById(productData._id).session(session);
if (!existingProduct) {
// Remove version key and other mongoose internals
const { __v, ...cleanData } = productData;
await models.Product.create([cleanData], { session });
} else {
console.log(` → Product ${productData.name} already exists, skipping restore`);
}
}
}
revertedCount++;
break;
case 'migrate_crops':
console.log(` → Reverting ${change.count} crops`);
// Restore migrated crops to original parent
if (change.cropIds && change.cropIds.length > 0) {
// Use specific crop IDs to revert only the migrated crops
await models.Crop.updateMany(
{ _id: { $in: change.cropIds } },
{ $set: { byPuid: change.oldParent } },
{ session }
);
}
// Restore deleted crops (that were replaced by destination entities)
if (change.deletedCrops && change.deletedCrops.length > 0) {
console.log(` → Restoring ${change.deletedCrops.length} deleted crops`);
for (const cropData of change.deletedCrops) {
// Check if crop already exists before trying to restore
const existingCrop = await models.Crop.findById(cropData._id).session(session);
if (!existingCrop) {
// Remove version key and other mongoose internals
const { __v, ...cleanData } = cropData;
await models.Crop.create([cleanData], { session });
} else {
console.log(` → Crop ${cropData.name} already exists, skipping restore`);
}
}
}
revertedCount++;
break;
case 'migrate_costing_items':
console.log(` → Reverting ${change.count} costing items`);
if (change.costingItemIds && change.costingItemIds.length > 0) {
// Use specific item IDs to revert only the migrated items
await models.CostingItem.updateMany(
{ _id: { $in: change.costingItemIds } },
{ $set: { byPuid: change.oldParent } },
{ session }
);
}
revertedCount++;
break;
case 'migrate_jobs':
console.log(` → Reverting ${change.jobCount} jobs`);
if (change.jobIds && change.jobIds.length > 0) {
// Use specific job IDs to revert only the migrated jobs
await models.Job.updateMany(
{ _id: { $in: change.jobIds } },
{ $set: { byPuid: change.oldParent } },
{ session }
);
}
revertedCount++;
break;
case 'migrate_invoices':
console.log(` → Reverting ${change.count} invoices`);
if (change.invoiceIds && change.invoiceIds.length > 0) {
// Restore each invoice to its original customer (byPuid field)
for (let i = 0; i < change.invoiceIds.length; i++) {
const invoiceId = change.invoiceIds[i];
const oldCustomerId = change.oldCustomerIds[i];
if (oldCustomerId) {
await models.Invoice.updateOne(
{ _id: invoiceId },
{ $set: { byPuid: oldCustomerId } },
{ session }
);
}
}
}
revertedCount++;
break;
case 'convert_customer_to_admin':
console.log(` → Reverting admin conversion for customer: ${change.customerId || change.sourceCustomerId}`);
const customerId = change.customerId || change.sourceCustomerId;
const originalKind = change.originalKind || '1'; // Default to APP (1) if not specified
const originalParent = change.originalParent || null;
// Handle old format where a new admin user was created
if (change.newAdminId) {
// Old format: delete the admin user that was created
await models.User.deleteOne(
{ _id: change.newAdminId },
{ session }
);
}
// Use direct MongoDB collection update to bypass Mongoose discriminator protection
// This is necessary because 'kind' is a discriminator key and can't be changed via Mongoose
const updateDoc = {
$set: {
kind: originalKind,
parent: originalParent,
active: true,
markedDelete: false
}
};
// Only add unset if there was a temp username
if (change.oldUsername) {
updateDoc.$unset = { username: 1 };
}
await models.Customer.collection.updateOne(
{ _id: mongoose.Types.ObjectId(customerId) },
updateDoc,
{ session }
);
// If there was an old username (temp), restore the original
if (change.oldUsername && change.username) {
await models.Customer.collection.updateOne(
{ _id: mongoose.Types.ObjectId(customerId) },
{ $set: { username: change.username } },
{ session }
);
}
revertedCount++;
break;
default:
console.log(` ⚠ Unknown action: ${change.action}`);
}
}
console.log(`\n✅ Reverted ${revertedCount} changes successfully`);
debug('Rollback transaction completed successfully');
}, mongoEnhanced.DEFAULT_TRANSACTION_OPTIONS);
}
/**
* Main execution
*/
async function main() {
try {
// Initialize database connection
console.log('Connecting to database...');
const dbConn = new DBConnection('Rollback Migration');
await dbConn.connect();
console.log('✓ Database connected\n');
// Load the last migration
console.log(`Loading migration history from: ${DEFAULT_HISTORY_FILE}`);
const migrationRecord = await loadLastMigration(DEFAULT_HISTORY_FILE);
if (migrationRecord.status !== 'completed') {
throw new Error(`Last migration status is '${migrationRecord.status}', not 'completed'. Cannot rollback.`);
}
// Confirm with user
console.log('\n⚠ WARNING: This will revert all changes from the last migration!');
console.log('Press Ctrl+C to cancel, or wait 3 seconds to continue...\n');
await new Promise(resolve => setTimeout(resolve, 3000));
// Execute rollback
await rollbackMigration(migrationRecord);
console.log('\n✅ Rollback completed successfully!\n');
// Close database connection
await mongoose.connection.close();
process.exit(0);
} catch (error) {
console.error('\n❌ Rollback failed:', error.message);
debug('Error:', error);
if (mongoose.connection.readyState === 1) {
await mongoose.connection.close();
}
process.exit(1);
}
}
// Command-line execution
if (require.main === module) {
main();
}
module.exports = { rollbackMigration, loadLastMigration };