| [b5bae24] | 1 | /**
|
|---|
| 2 | * P58 — One-shot migration: convert markdown post.content to sanitized HTML.
|
|---|
| 3 | *
|
|---|
| 4 | * Run on the server AFTER deploying P58:
|
|---|
| 5 | * node scripts/migrate-posts-to-html.js
|
|---|
| 6 | *
|
|---|
| 7 | * Idempotent: posts whose content already starts with '<' are detected as
|
|---|
| 8 | * HTML and skipped. Markdown posts are rendered via MarkdownService, then
|
|---|
| 9 | * sanitized via HtmlSanitizerService, then written back. Original markdown
|
|---|
| 10 | * is preserved in posts.content_legacy_md so we can roll back if needed.
|
|---|
| 11 | *
|
|---|
| 12 | * Re-running the script after the column is added is safe — the second run
|
|---|
| 13 | * will see HTML content and skip everything.
|
|---|
| 14 | */
|
|---|
| 15 |
|
|---|
| 16 | import db from '../src/config/database.js';
|
|---|
| 17 | import MarkdownService from '../src/services/MarkdownService.js';
|
|---|
| 18 | import HtmlSanitizerService from '../src/services/HtmlSanitizerService.js';
|
|---|
| 19 |
|
|---|
| 20 | function ensureBackupColumn() {
|
|---|
| 21 | // Try to add the legacy_md backup column; SQLite throws if it exists.
|
|---|
| 22 | try {
|
|---|
| 23 | db.exec(`ALTER TABLE posts ADD COLUMN content_legacy_md TEXT`);
|
|---|
| 24 | console.log(' + added posts.content_legacy_md column');
|
|---|
| 25 | } catch (e) {
|
|---|
| 26 | if (!/duplicate column/i.test(e.message)) throw e;
|
|---|
| 27 | }
|
|---|
| 28 | }
|
|---|
| 29 |
|
|---|
| 30 | function looksLikeHtml(s) {
|
|---|
| 31 | if (!s) return false;
|
|---|
| 32 | // Heuristic: starts with a tag, OR contains common block-level tags. Doesn't
|
|---|
| 33 | // need to be airtight — false positives just mean we skip a post that's
|
|---|
| 34 | // already mostly HTML, which is fine.
|
|---|
| 35 | const t = s.trimStart();
|
|---|
| 36 | return t.startsWith('<') || /<\/?(p|h[1-6]|ul|ol|li|blockquote|figure|img|div)\b/i.test(s);
|
|---|
| 37 | }
|
|---|
| 38 |
|
|---|
| 39 | function migrateOne(post) {
|
|---|
| 40 | if (!post.content || !post.content.trim()) {
|
|---|
| 41 | return { id: post.id, action: 'empty' };
|
|---|
| 42 | }
|
|---|
| 43 | if (looksLikeHtml(post.content)) {
|
|---|
| 44 | return { id: post.id, action: 'skipped-already-html' };
|
|---|
| 45 | }
|
|---|
| 46 | if (post.content_legacy_md) {
|
|---|
| 47 | // Already migrated (legacy backup exists) but content somehow markdown again?
|
|---|
| 48 | return { id: post.id, action: 'skipped-already-backed-up' };
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | const renderedHtml = MarkdownService.render(post.content);
|
|---|
| 52 | const cleanHtml = HtmlSanitizerService.sanitize(renderedHtml);
|
|---|
| 53 |
|
|---|
| 54 | db.prepare(`
|
|---|
| 55 | UPDATE posts SET content = ?, content_legacy_md = ?, updated_at = updated_at
|
|---|
| 56 | WHERE id = ?
|
|---|
| 57 | `).run(cleanHtml, post.content, post.id);
|
|---|
| 58 |
|
|---|
| 59 | return { id: post.id, action: 'migrated', from: post.content.length, to: cleanHtml.length };
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | function main() {
|
|---|
| 63 | console.log('PrutFolio P58 — markdown → HTML post migration\n');
|
|---|
| 64 | ensureBackupColumn();
|
|---|
| 65 |
|
|---|
| 66 | const posts = db.prepare(`SELECT id, slug, content, content_legacy_md FROM posts`).all();
|
|---|
| 67 | console.log(` found ${posts.length} posts\n`);
|
|---|
| 68 |
|
|---|
| 69 | const stats = { migrated: 0, skipped: 0, empty: 0 };
|
|---|
| 70 | for (const p of posts) {
|
|---|
| 71 | const r = migrateOne(p);
|
|---|
| 72 | const tag = r.action.startsWith('skipped') ? 'skipped' :
|
|---|
| 73 | r.action === 'empty' ? 'empty' : 'migrated';
|
|---|
| 74 | stats[tag]++;
|
|---|
| 75 | const detail = r.action === 'migrated' ? ` (${r.from} md → ${r.to} html chars)` : ` [${r.action}]`;
|
|---|
| 76 | console.log(` - ${p.slug}${detail}`);
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | console.log(`\nDone. migrated=${stats.migrated} skipped=${stats.skipped} empty=${stats.empty}`);
|
|---|
| 80 | console.log('Backup column posts.content_legacy_md retains original markdown.');
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | main();
|
|---|