/** * Migration Script: Tags → Notebooks with Contextual Labels * * This script migrates from the flat tags system to the new notebooks system. * It creates a default "Labels Migrés" notebook for each user and moves all * existing labels to that notebook. Notes remain in "Notes générales" (Inbox). * * IMPORTANT: This is a NON-BREAKING migration. The existing system continues * to work during and after migration. * * Usage: * npx tsx scripts/migrate-to-notebooks.ts * * Rollback: * npx tsx scripts/rollback-notebooks.ts */ import { prisma } from '../keep-notes/lib/prisma' interface MigrationStats { usersProcessed: number notebooksCreated: number labelsMigrated: number notesAffected: number errors: string[] } /** * Main migration function */ async function migrateToNotebooks(): Promise { console.log('🚀 Starting migration to notebooks...\n') const stats: MigrationStats = { usersProcessed: 0, notebooksCreated: 0, labelsMigrated: 0, notesAffected: 0, errors: [], } try { // Step 1: Get all users console.log('📊 Fetching users...') const users = await prisma.user.findMany({ select: { id: true, name: true, email: true, }, }) if (users.length === 0) { console.log('⚠️ No users found. Nothing to migrate.') return stats } console.log(`✅ Found ${users.length} user(s)\n`) // Step 2: Process each user for (const user of users) { console.log(`\n👤 Processing user: ${user.name || user.email} (${user.id})`) try { // Step 2.1: Check if migration notebook already exists const existingNotebook = await prisma.notebook.findFirst({ where: { name: 'Labels Migrés', userId: user.id, }, }) if (existingNotebook) { console.log(' ⏭️ Migration notebook already exists, skipping...') continue } // Step 2.2: Create "Labels Migrés" notebook console.log(' 📁 Creating "Labels Migrés" notebook...') const migrationNotebook = await prisma.notebook.create({ data: { id: `migrate-${user.id}`, // Deterministic ID name: 'Labels Migrés', icon: '📦', color: '#9CA3AF', // gray-400 order: 999, // At the bottom userId: user.id, }, }) stats.notebooksCreated++ console.log(` ✅ Created notebook: ${migrationNotebook.id}`) // Step 2.3: Migrate all labels for this user console.log(' 🏷️ Migrating labels...') const labels = await prisma.label.findMany({ where: { userId: user.id, notebookId: { equals: '', // Empty string = not migrated yet }, }, }) // Also try with null (in case database has null values) const labelsWithNull = await prisma.label.findMany({ where: { userId: user.id, notebookId: null, }, }) // Combine both results (avoid duplicates) const allLabels = [...labels] for (const label of labelsWithNull) { if (!allLabels.find(l => l.id === label.id)) { allLabels.push(label) } } if (allLabels.length === 0) { console.log(' ℹ️ No labels to migrate') continue } // Update all labels to belong to the migration notebook const labelUpdates = allLabels.map((label) => prisma.label.update({ where: { id: label.id }, data: { notebookId: migrationNotebook.id }, }) ) await prisma.$transaction(labelUpdates) stats.labelsMigrated += allLabels.length console.log(` ✅ Migrated ${allLabels.length} label(s)`) // Step 2.4: Count notes that will be affected (information only) const noteCount = await prisma.note.count({ where: { userId: user.id }, }) stats.notesAffected += noteCount console.log(` ℹ️ User has ${noteCount} note(s) (will remain in "Notes générales")`) stats.usersProcessed++ } catch (userError) { const errorMsg = `Failed to migrate user ${user.id}: ${userError}` console.error(` ❌ ${errorMsg}`) stats.errors.push(errorMsg) } } // Summary console.log('\n' + '='.repeat(60)) console.log('✅ Migration complete!\n') console.log('📊 Summary:') console.log(` Users processed: ${stats.usersProcessed}`) console.log(` Notebooks created: ${stats.notebooksCreated}`) console.log(` Labels migrated: ${stats.labelsMigrated}`) console.log(` Notes affected: ${stats.notesAffected} (all remain in Inbox)`) if (stats.errors.length > 0) { console.log(`\n⚠️ Errors: ${stats.errors.length}`) stats.errors.forEach((error) => console.log(` ❌ ${error}`)) } console.log('\n' + '='.repeat(60)) console.log('\n✨ Migration successful!') console.log('\n📌 Next steps:') console.log(' 1. Test the application to ensure everything works') console.log(' 2. Users can now organize their notes into notebooks') console.log(' 3. Users can move labels from "Labels Migrés" to new notebooks') console.log(' 4. Consider deleting old labels field from Note model after verification\n') return stats } catch (error) { console.error('\n❌ Migration failed:', error) throw error } finally { await prisma.$disconnect() } } /** * Dry-run mode: Show what would happen without making changes */ async function dryRunMigration() { console.log('🔍 DRY RUN MODE - No changes will be made\n') const users = await prisma.user.findMany({ select: { id: true, name: true, email: true, }, }) console.log(`Found ${users.length} user(s)\n`) for (const user of users) { console.log(`\n👤 User: ${user.name || user.email}`) const labels = await prisma.label.findMany({ where: { userId: user.id }, select: { id: true, name: true }, }) const notes = await prisma.note.count({ where: { userId: user.id }, }) console.log(` Labels: ${labels.length}`) console.log(` Notes: ${notes}`) console.log(` Would create: "Labels Migrés" notebook`) console.log(` Would migrate: ${labels.length} labels`) } await prisma.$disconnect() } // Run migration const args = process.argv.slice(2) const isDryRun = args.includes('--dry-run') if (isDryRun) { dryRunMigration() .then(() => { console.log('\n✅ Dry run complete') process.exit(0) }) .catch((error) => { console.error('\n❌ Dry run failed:', error) process.exit(1) }) } else { migrateToNotebooks() .then(() => { console.log('\n✨ All done!') process.exit(0) }) .catch((error) => { console.error('\n💥 Fatal error:', error) process.exit(1) }) }