rnotes-online/scripts/backfill-memory-card.ts

159 lines
4.8 KiB
TypeScript

/**
* Backfill script: Populate Memory Card fields for existing notes.
*
* Processes all notes where bodyJson IS NULL:
* 1. htmlToTipTapJson(content) → bodyJson
* 2. tipTapJsonToMarkdown(bodyJson) → bodyMarkdown
* 3. mapNoteTypeToCardType(type) → cardType
* 4. sortOrder * 1.0 → position
* 5. bodyFormat = 'html'
*
* Also backfills fileUrl references into File + CardAttachment records.
*
* Run: docker exec rnotes-online npx tsx scripts/backfill-memory-card.ts
*/
import { PrismaClient } from '@prisma/client';
import { htmlToTipTapJson, tipTapJsonToMarkdown, mapNoteTypeToCardType } from '../src/lib/content-convert';
const prisma = new PrismaClient();
const BATCH_SIZE = 50;
async function backfillNotes() {
console.log('=== Memory Card Backfill ===\n');
// Count notes needing backfill
const total = await prisma.note.count({
where: { bodyJson: null, content: { not: '' } },
});
console.log(`Found ${total} notes to backfill.\n`);
if (total === 0) {
console.log('Nothing to do!');
return;
}
let processed = 0;
let errors = 0;
while (true) {
const notes = await prisma.note.findMany({
where: { bodyJson: null, content: { not: '' } },
select: { id: true, content: true, type: true, sortOrder: true, fileUrl: true, mimeType: true, fileSize: true, authorId: true },
take: BATCH_SIZE,
});
if (notes.length === 0) break;
for (const note of notes) {
try {
const bodyJson = htmlToTipTapJson(note.content);
const bodyMarkdown = tipTapJsonToMarkdown(bodyJson);
const cardType = mapNoteTypeToCardType(note.type);
await prisma.note.update({
where: { id: note.id },
data: {
bodyJson,
bodyMarkdown,
bodyFormat: 'html',
cardType,
position: note.sortOrder * 1.0,
},
});
// Backfill fileUrl → File + CardAttachment
if (note.fileUrl) {
const storageKey = note.fileUrl.replace(/^\/api\/uploads\//, '');
if (storageKey && storageKey !== note.fileUrl) {
// Check if File record already exists
const existingFile = await prisma.file.findUnique({
where: { storageKey },
});
if (!existingFile) {
const file = await prisma.file.create({
data: {
storageKey,
filename: storageKey.replace(/^[a-zA-Z0-9_-]+_/, ''), // strip nanoid prefix
mimeType: note.mimeType || 'application/octet-stream',
sizeBytes: note.fileSize || 0,
authorId: note.authorId,
},
});
await prisma.cardAttachment.create({
data: {
noteId: note.id,
fileId: file.id,
role: 'primary',
position: 0,
},
});
} else {
// File exists, just link it
const existingLink = await prisma.cardAttachment.findUnique({
where: { noteId_fileId: { noteId: note.id, fileId: existingFile.id } },
});
if (!existingLink) {
await prisma.cardAttachment.create({
data: {
noteId: note.id,
fileId: existingFile.id,
role: 'primary',
position: 0,
},
});
}
}
}
}
processed++;
} catch (err) {
errors++;
console.error(` Error on note ${note.id}:`, err);
}
}
console.log(` Processed ${processed}/${total} (${errors} errors)`);
}
// Also backfill notes with empty content (set bodyJson to empty doc)
const emptyNotes = await prisma.note.count({
where: { bodyJson: null, content: '' },
});
if (emptyNotes > 0) {
await prisma.note.updateMany({
where: { bodyJson: null, content: '' },
data: {
bodyJson: { type: 'doc', content: [] },
bodyMarkdown: '',
bodyFormat: 'html',
cardType: 'note',
},
});
console.log(` Set ${emptyNotes} empty notes to empty doc.`);
}
// Update tags: set spaceId to '' where null
const nullSpaceTags = await prisma.$executeRaw`
UPDATE "Tag" SET "spaceId" = '' WHERE "spaceId" IS NULL
`;
if (nullSpaceTags > 0) {
console.log(` Updated ${nullSpaceTags} tags with null spaceId to ''.`);
}
console.log(`\n=== Done! ${processed} notes backfilled, ${errors} errors ===`);
// Verify
const remaining = await prisma.note.count({
where: { bodyJson: null, content: { not: '' } },
});
console.log(`Remaining unprocessed notes: ${remaining}`);
}
backfillNotes()
.catch(console.error)
.finally(() => prisma.$disconnect());