Files
Momento/scripts/migrate-to-notebooks.ts
Sepehr Ramezani e4d4e23dc7 chore: clean up repo for public release
- Remove BMAD framework, IDE configs, dev screenshots, test files,
  internal docs, and backup files
- Rename keep-notes/ to memento-note/
- Update all references from keep-notes to memento-note
- Add Apache 2.0 license with Commons Clause (non-commercial restriction)
- Add clean .gitignore and .env.docker.example
2026-04-20 22:48:06 +02:00

246 lines
7.0 KiB
TypeScript
Raw Permalink 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.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 '../memento-note/lib/prisma'
interface MigrationStats {
usersProcessed: number
notebooksCreated: number
labelsMigrated: number
notesAffected: number
errors: string[]
}
/**
* Main migration function
*/
async function migrateToNotebooks(): Promise<MigrationStats> {
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)
})
}