Files
Momento/scripts/migrate-to-notebooks.ts
Sepehr Ramezani aa6a214f37 feat: rename keep-notes to memento-note, migrate to PostgreSQL, fix MCP bugs
- Rename directory keep-notes -> memento-note with all code references
- Prisma: SQLite -> PostgreSQL (both app and MCP server schemas)
- Sync MCP schema with main app (add missing fields, relations, indexes)
- Delete 17 SQLite migrations (clean slate for PostgreSQL)
- Remove SQLite dependencies (@libsql/client, better-sqlite3, etc.)
- Fix MCP server: hardcoded Windows DB paths -> DATABASE_URL env var
- Fix MCP server: .dockerignore excluded index-sse.js (SSE mode broken)
- MCP Dockerfile: node:20 -> node:22
- Docker Compose: add postgres service, remove SQLite volume
- Generate favicon.ico, icon-192.png, icon-512.png, apple-icon.png
- Update layout.tsx icons and manifest.json for PNG icons
- Update all .env files for PostgreSQL
- Rewrite README.md with updated sections
- Remove mcp-server/node_modules and prisma/client-generated from git tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-20 20:58:04 +02:00

246 lines
7.0 KiB
TypeScript
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.
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)
})
}