319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
/**
|
||
* Migration Script: Migrate PartnerSystemUser 'customer' field to 'parent'
|
||
*
|
||
* This script migrates existing PartnerSystemUser documents that have a 'customer' field
|
||
* but no 'parent' field. It copies the customer value to parent and optionally removes
|
||
* the customer field.
|
||
*
|
||
* Usage:
|
||
* node scripts/migratePartnerSystemUserCustomerToParent.js [options]
|
||
*
|
||
* Options:
|
||
* --preview Show what will be migrated without making changes
|
||
* --remove-old Remove the 'customer' field after migration (default: keep for safety)
|
||
* --env <path> Path to environment file (default: ../environment.env)
|
||
* --help Show this help message
|
||
*
|
||
* Examples:
|
||
* # Preview migration (recommended first)
|
||
* node scripts/migratePartnerSystemUserCustomerToParent.js --preview
|
||
*
|
||
* # Execute migration (keep customer field for safety)
|
||
* node scripts/migratePartnerSystemUserCustomerToParent.js
|
||
*
|
||
* # Execute migration and remove customer field
|
||
* node scripts/migratePartnerSystemUserCustomerToParent.js --remove-old
|
||
*
|
||
* # Use production environment
|
||
* node scripts/migratePartnerSystemUserCustomerToParent.js --env ../environment_prod.env --preview
|
||
*/
|
||
|
||
const path = require('path');
|
||
|
||
// Parse command line arguments
|
||
const args = process.argv.slice(2);
|
||
const options = {
|
||
preview: args.includes('--preview'),
|
||
removeOld: args.includes('--remove-old'),
|
||
envPath: '../environment.env',
|
||
help: args.includes('--help') || args.includes('-h')
|
||
};
|
||
|
||
// Parse --env option
|
||
const envIndex = args.indexOf('--env');
|
||
if (envIndex !== -1 && args[envIndex + 1]) {
|
||
options.envPath = args[envIndex + 1];
|
||
}
|
||
|
||
if (options.help) {
|
||
console.log(`
|
||
Migration Script: Migrate PartnerSystemUser 'customer' field to 'parent'
|
||
|
||
Usage:
|
||
node scripts/migratePartnerSystemUserCustomerToParent.js [options]
|
||
|
||
Options:
|
||
--preview Show what will be migrated without making changes
|
||
--remove-old Remove the 'customer' field after migration (default: keep for safety)
|
||
--env <path> Path to environment file (default: ../environment.env)
|
||
--help Show this help message
|
||
|
||
Examples:
|
||
# Preview migration (recommended first)
|
||
node scripts/migratePartnerSystemUserCustomerToParent.js --preview
|
||
|
||
# Execute migration (keep customer field for safety)
|
||
node scripts/migratePartnerSystemUserCustomerToParent.js
|
||
|
||
# Execute migration and remove customer field
|
||
node scripts/migratePartnerSystemUserCustomerToParent.js --remove-old
|
||
`);
|
||
process.exit(0);
|
||
}
|
||
|
||
// Load environment
|
||
const envPath = path.isAbsolute(options.envPath) ? options.envPath : path.join(process.cwd(), options.envPath);
|
||
require('dotenv').config({ path: envPath });
|
||
|
||
const mongoose = require('mongoose');
|
||
const { DBConnection } = require('../helpers/db/connect');
|
||
const { UserTypes } = require('../helpers/constants');
|
||
|
||
// Create database connection instance
|
||
const dbConn = new DBConnection('Partner System User Migration');
|
||
|
||
// Get raw collection access (bypass mongoose schema)
|
||
async function getRawCollection() {
|
||
return mongoose.connection.collection('users');
|
||
}
|
||
|
||
async function migratePartnerSystemUsers() {
|
||
console.log('================================================================================');
|
||
console.log('PARTNER SYSTEM USER MIGRATION: customer → parent');
|
||
console.log('================================================================================');
|
||
console.log(`Mode: ${options.preview ? 'PREVIEW (no changes)' : 'EXECUTE'}`);
|
||
console.log(`Remove old customer field: ${options.removeOld ? 'YES' : 'NO'}`);
|
||
console.log(`Environment: ${options.envPath}`);
|
||
console.log('================================================================================\n');
|
||
|
||
try {
|
||
// Connect to database
|
||
console.log('Connecting to database...');
|
||
await dbConn.connect({ exitOnError: false });
|
||
console.log('Connected successfully.\n');
|
||
|
||
const usersCollection = await getRawCollection();
|
||
|
||
// Find all PartnerSystemUser documents that need migration
|
||
// Case 1: Has 'customer' but no 'parent'
|
||
// Case 2: Has 'customer' and 'parent' but they differ (data inconsistency)
|
||
const query = {
|
||
kind: UserTypes.PARTNER_SYSTEM_USER,
|
||
customer: { $exists: true }
|
||
};
|
||
|
||
const usersToMigrate = await usersCollection.find(query).toArray();
|
||
|
||
console.log(`Found ${usersToMigrate.length} PartnerSystemUser document(s) with 'customer' field.\n`);
|
||
|
||
if (usersToMigrate.length === 0) {
|
||
console.log('✅ No migration needed - all documents are up to date.');
|
||
return { migrated: 0, skipped: 0, errors: [] };
|
||
}
|
||
|
||
const stats = {
|
||
migrated: 0,
|
||
skipped: 0,
|
||
alreadyCorrect: 0,
|
||
errors: []
|
||
};
|
||
|
||
console.log('Processing documents:\n');
|
||
|
||
for (const user of usersToMigrate) {
|
||
const customerId = user.customer;
|
||
const parentId = user.parent;
|
||
const username = user.username || user.name || user._id;
|
||
|
||
// Check if parent already matches customer
|
||
if (parentId && parentId.toString() === customerId.toString()) {
|
||
console.log(` ⏭️ ${username}: parent already equals customer - ${options.removeOld ? 'will remove customer field' : 'skipping'}`);
|
||
stats.alreadyCorrect++;
|
||
|
||
if (!options.preview && options.removeOld) {
|
||
// Remove the redundant customer field
|
||
await usersCollection.updateOne(
|
||
{ _id: user._id },
|
||
{ $unset: { customer: '' } }
|
||
);
|
||
stats.migrated++;
|
||
} else {
|
||
stats.skipped++;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Check for data inconsistency (parent exists but differs from customer)
|
||
if (parentId && parentId.toString() !== customerId.toString()) {
|
||
console.log(` ⚠️ ${username}: INCONSISTENCY - parent (${parentId}) differs from customer (${customerId})`);
|
||
console.log(` → Will use customer value for parent`);
|
||
}
|
||
|
||
console.log(` 📝 ${username}:`);
|
||
console.log(` customer: ${customerId}`);
|
||
console.log(` parent: ${parentId || '(not set)'} → ${customerId}`);
|
||
|
||
if (!options.preview) {
|
||
try {
|
||
const updateOps = {
|
||
$set: { parent: customerId }
|
||
};
|
||
|
||
if (options.removeOld) {
|
||
updateOps.$unset = { customer: '' };
|
||
}
|
||
|
||
const result = await usersCollection.updateOne(
|
||
{ _id: user._id },
|
||
updateOps
|
||
);
|
||
|
||
if (result.modifiedCount === 1) {
|
||
console.log(` ✅ Migrated successfully`);
|
||
stats.migrated++;
|
||
} else {
|
||
console.log(` ⚠️ No changes made (modifiedCount: ${result.modifiedCount})`);
|
||
stats.skipped++;
|
||
}
|
||
} catch (error) {
|
||
console.log(` ❌ Error: ${error.message}`);
|
||
stats.errors.push({ userId: user._id, username, error: error.message });
|
||
}
|
||
} else {
|
||
stats.migrated++; // Count as would-be-migrated in preview
|
||
}
|
||
}
|
||
|
||
console.log('\n================================================================================');
|
||
console.log('MIGRATION SUMMARY');
|
||
console.log('================================================================================');
|
||
console.log(`Total documents found: ${usersToMigrate.length}`);
|
||
console.log(`Already correct: ${stats.alreadyCorrect}`);
|
||
if (options.preview) {
|
||
console.log(`Would migrate: ${stats.migrated}`);
|
||
console.log(`Would skip: ${stats.skipped}`);
|
||
} else {
|
||
console.log(`Successfully migrated: ${stats.migrated}`);
|
||
console.log(`Skipped: ${stats.skipped}`);
|
||
}
|
||
console.log(`Errors: ${stats.errors.length}`);
|
||
|
||
if (stats.errors.length > 0) {
|
||
console.log('\nErrors:');
|
||
stats.errors.forEach(err => {
|
||
console.log(` - ${err.username} (${err.userId}): ${err.error}`);
|
||
});
|
||
}
|
||
|
||
if (options.preview) {
|
||
console.log('\n⚠️ PREVIEW MODE - No changes were made.');
|
||
console.log('Run without --preview to execute the migration.');
|
||
} else {
|
||
console.log('\n✅ Migration completed.');
|
||
if (!options.removeOld) {
|
||
console.log('Note: The "customer" field was kept for safety.');
|
||
console.log('Run with --remove-old to remove it in a future migration.');
|
||
}
|
||
}
|
||
|
||
return stats;
|
||
|
||
} catch (error) {
|
||
console.error('\n❌ Migration failed:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Verification function to check migration results
|
||
async function verifyMigration() {
|
||
console.log('\n================================================================================');
|
||
console.log('VERIFICATION');
|
||
console.log('================================================================================\n');
|
||
|
||
try {
|
||
const usersCollection = await getRawCollection();
|
||
|
||
// Check for documents with customer but no parent
|
||
const missingParent = await usersCollection.countDocuments({
|
||
kind: UserTypes.PARTNER_SYSTEM_USER,
|
||
customer: { $exists: true },
|
||
parent: { $exists: false }
|
||
});
|
||
|
||
// Check for documents with parent set
|
||
const hasParent = await usersCollection.countDocuments({
|
||
kind: UserTypes.PARTNER_SYSTEM_USER,
|
||
parent: { $exists: true }
|
||
});
|
||
|
||
// Check for documents with both fields matching
|
||
const pipeline = [
|
||
{
|
||
$match: {
|
||
kind: UserTypes.PARTNER_SYSTEM_USER,
|
||
parent: { $exists: true },
|
||
customer: { $exists: true }
|
||
}
|
||
},
|
||
{
|
||
$project: {
|
||
match: { $eq: ['$parent', '$customer'] }
|
||
}
|
||
},
|
||
{
|
||
$match: { match: true }
|
||
},
|
||
{
|
||
$count: 'count'
|
||
}
|
||
];
|
||
|
||
const matchingResult = await usersCollection.aggregate(pipeline).toArray();
|
||
const matchingCount = matchingResult[0]?.count || 0;
|
||
|
||
console.log('Current state of PartnerSystemUser documents:');
|
||
console.log(` With parent field set: ${hasParent}`);
|
||
console.log(` With customer but missing parent: ${missingParent}`);
|
||
console.log(` With both fields matching: ${matchingCount}`);
|
||
|
||
if (missingParent === 0 && hasParent > 0) {
|
||
console.log('\n✅ All PartnerSystemUser documents have parent field set.');
|
||
} else if (missingParent > 0) {
|
||
console.log(`\n⚠️ ${missingParent} document(s) still need migration.`);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Verification failed:', error.message);
|
||
}
|
||
}
|
||
|
||
// Main execution
|
||
async function main() {
|
||
try {
|
||
await migratePartnerSystemUsers();
|
||
await verifyMigration();
|
||
} catch (error) {
|
||
console.error('Migration script failed:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
await mongoose.connection.close();
|
||
console.log('\nDatabase connection closed.');
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
main();
|