#!/usr/bin/env node /** * Migrate legacy 'public' field to modern 'visibility' field * * SECURITY: Safe migration with dry-run support * - Migrates public: true → visibility: 'public' * - Migrates public: false → visibility: 'internal' * - Preserves documents that already have visibility set * - Removes the deprecated 'public' field after migration */ const { getCollection } = require('../src/utils/db.util'); async function migrate(dryRun = false) { try { const collection = await getCollection('documents'); // Find documents with public field but no visibility const docsWithPublicOnly = await collection.find({ public: { $exists: true }, visibility: { $exists: false } }).toArray(); // Find documents with both fields (inconsistent state) const docsWithBoth = await collection.find({ public: { $exists: true }, visibility: { $exists: true } }).toArray(); console.log('\nšŸ“Š Migration Analysis:'); console.log(` Documents with only 'public' field: ${docsWithPublicOnly.length}`); console.log(` Documents with both fields: ${docsWithBoth.length}`); console.log(` Total to migrate: ${docsWithPublicOnly.length + docsWithBoth.length}`); if (docsWithPublicOnly.length === 0 && docsWithBoth.length === 0) { console.log('\nāœ… No documents need migration. All documents already use visibility field.'); return; } if (dryRun) { console.log('\nšŸ” DRY RUN - No changes will be made\n'); // Show what would be migrated if (docsWithPublicOnly.length > 0) { console.log('Documents with only public field:'); docsWithPublicOnly.forEach(doc => { const newVisibility = doc.public ? 'public' : 'internal'; console.log(` - ${doc.title} (${doc.slug})`); console.log(` public: ${doc.public} → visibility: '${newVisibility}'`); }); } if (docsWithBoth.length > 0) { console.log('\nDocuments with both fields (will remove public):'); docsWithBoth.forEach(doc => { console.log(` - ${doc.title} (${doc.slug})`); console.log(` current: public=${doc.public}, visibility='${doc.visibility}'`); console.log(` action: Keep visibility='${doc.visibility}', remove public field`); }); } console.log('\nšŸ’” Run with --execute to perform migration'); return; } // Perform actual migration console.log('\nšŸ”„ Performing migration...\n'); let migratedCount = 0; // Migrate documents with only public field for (const doc of docsWithPublicOnly) { const visibility = doc.public ? 'public' : 'internal'; await collection.updateOne( { _id: doc._id }, { $set: { visibility }, $unset: { public: "" } } ); console.log(`āœ“ ${doc.title}: public=${doc.public} → visibility='${visibility}'`); migratedCount++; } // Clean up documents with both fields (keep visibility, remove public) for (const doc of docsWithBoth) { await collection.updateOne( { _id: doc._id }, { $unset: { public: "" } } ); console.log(`āœ“ ${doc.title}: Removed public field, kept visibility='${doc.visibility}'`); migratedCount++; } console.log(`\nāœ… Migration complete! ${migratedCount} documents updated.`); // Verify results const remainingWithPublic = await collection.countDocuments({ public: { $exists: true } }); const totalWithVisibility = await collection.countDocuments({ visibility: { $exists: true } }); console.log('\nšŸ“Š Post-migration verification:'); console.log(` Documents with 'public' field: ${remainingWithPublic}`); console.log(` Documents with 'visibility' field: ${totalWithVisibility}`); if (remainingWithPublic > 0) { console.warn('\nāš ļø Warning: Some documents still have the public field. Review manually.'); } else { console.log('\nāœ… All documents successfully migrated to visibility field!'); } } catch (error) { console.error('āŒ Migration failed:', error); throw error; } } // CLI interface const args = process.argv.slice(2); const dryRun = !args.includes('--execute'); if (dryRun) { console.log('šŸ” Running in DRY RUN mode (no changes will be made)'); console.log(' Use --execute flag to perform actual migration\n'); } migrate(dryRun) .then(() => { console.log('\n✨ Script complete'); process.exit(0); }) .catch((error) => { console.error('\nšŸ’„ Script failed:', error); process.exit(1); });