#!/usr/bin/env node // Dumps a legacy mule-image Postgres into the snapshot JSON shape `run.mjs` // consumes. Intended to run on the homecloud server where the legacy stack // lives (since `DATABASE_URL` points at the in-network Postgres there). // // DATABASE_URL=postgres://mulita:mulita@db:5432/mulita \ // node migrate/legacy_export.mjs > snapshot.json // // Uses the `pg` npm package which the legacy mule-image already ships in // its backend image; if you're running this from a separate workstation, // `npm install pg` first. import pg from 'pg'; import process from 'node:process'; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error('DATABASE_URL required'); process.exit(2); } const client = new pg.Client({ connectionString: DATABASE_URL }); await client.connect(); // ── photos ─────────────────────────────────────────────────────────────────── // Mule-image's photos table is the source of all per-file metadata. The // migrator only reads fields that have a clean PhotoPrism mapping; the // rest (auto-tags, ML-derived fields, etc.) are skipped per the merge plan. const photoRows = await client.query(` SELECT p.filepath, p.user_title, p.user_notes, p.rating, p.is_picked, p.is_discarded, p.is_hidden, p.taken_at, COALESCE( array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}' ) AS tags FROM photos p LEFT JOIN photo_tags pt ON pt.photo_id = p.id LEFT JOIN tags t ON t.id = pt.tag_id WHERE p.is_active IS NULL OR p.is_active = true GROUP BY p.id `); // ── heaps + memberships ───────────────────────────────────────────────────── const heapRows = await client.query(` SELECT h.id, h.name FROM heaps h WHERE h.is_active = true `); const heapMembers = await client.query(` SELECT hp.heap_id, p.filepath FROM heap_photos hp JOIN photos p ON p.id = hp.photo_id `); const membersByHeap = new Map(); for (const r of heapMembers.rows) { if (!membersByHeap.has(r.heap_id)) membersByHeap.set(r.heap_id, []); membersByHeap.get(r.heap_id).push(r.filepath); } // ── assemble ───────────────────────────────────────────────────────────────── const snapshot = { photos: photoRows.rows.map((r) => ({ filepath: r.filepath, user_title: r.user_title, user_notes: r.user_notes, rating: r.rating, is_picked: Boolean(r.is_picked), is_discarded: Boolean(r.is_discarded), is_hidden: Boolean(r.is_hidden), taken_at: r.taken_at ? new Date(r.taken_at).toISOString() : null, tags: r.tags ?? [] })), heaps: heapRows.rows.map((h) => ({ name: h.name, photo_filepaths: membersByHeap.get(h.id) ?? [] })) }; process.stdout.write(JSON.stringify(snapshot, null, 2)); await client.end();