159 lines
4.8 KiB
TypeScript
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());
|