Schema migrates dev to prod only; catalog rows merge by updated_at and images by mtime with union merge and conflict reporting. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.3 KiB
JavaScript
152 lines
4.3 KiB
JavaScript
/**
|
|
* Backfill catalog updated_at from linked image file mtimes where possible.
|
|
*
|
|
* Dev: npm run dev:backfill-updated-at
|
|
* Prod: npm run harmonize:backfill-updated-at
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const pg = require('pg');
|
|
const {
|
|
assertDevDatabase,
|
|
assertProdDatabase,
|
|
loadDevPgConfig,
|
|
loadProdPgConfig,
|
|
rootDir,
|
|
} = require('./db-env');
|
|
const { printCliResult } = require('./lib/cli-result');
|
|
|
|
const { Client } = pg;
|
|
|
|
function parseArgs(argv) {
|
|
return { prod: argv.includes('--prod') };
|
|
}
|
|
|
|
function resolveImageDir(prod) {
|
|
if (prod) {
|
|
const envPath = path.join(rootDir, 'infra', 'docker', '.env.prod');
|
|
if (fs.existsSync(envPath)) {
|
|
for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith('IMAGE_DIR=')) {
|
|
const val = trimmed.slice('IMAGE_DIR='.length).trim().replace(/^["']|["']$/g, '');
|
|
if (val) return path.resolve(val);
|
|
}
|
|
}
|
|
}
|
|
return path.resolve('\\\\192.168.10.122\\Gallery\\data\\images');
|
|
}
|
|
require('dotenv').config({ path: path.join(rootDir, '.env') });
|
|
return path.resolve(process.env.IMAGE_DIR || path.join(rootDir, 'data', 'images'));
|
|
}
|
|
|
|
function fileMtimeMs(imageDir, relPath) {
|
|
if (!relPath || typeof relPath !== 'string') return null;
|
|
const abs = path.join(imageDir, relPath.replace(/^\//, ''));
|
|
try {
|
|
if (!fs.existsSync(abs)) return null;
|
|
return fs.statSync(abs).mtimeMs;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function maxMtime(imageDir, paths) {
|
|
let max = null;
|
|
for (const rel of paths) {
|
|
const ms = fileMtimeMs(imageDir, rel);
|
|
if (ms != null && (max == null || ms > max)) max = ms;
|
|
}
|
|
return max;
|
|
}
|
|
|
|
async function main() {
|
|
const { prod } = parseArgs(process.argv.slice(2));
|
|
const config = prod ? loadProdPgConfig() : loadDevPgConfig();
|
|
if (prod) assertProdDatabase(config.database);
|
|
else assertDevDatabase(config.database);
|
|
|
|
const imageDir = resolveImageDir(prod);
|
|
const client = new Client(config);
|
|
await client.connect();
|
|
|
|
const fallback = new Date();
|
|
let updated = 0;
|
|
|
|
const { rows: artists } = await client.query(
|
|
'SELECT id, portrait_path, portrait_thumb_path FROM artists',
|
|
);
|
|
for (const row of artists) {
|
|
const ms = maxMtime(imageDir, [row.portrait_path, row.portrait_thumb_path]);
|
|
const ts = ms != null ? new Date(ms) : fallback;
|
|
await client.query('UPDATE artists SET updated_at = $1 WHERE id = $2', [ts, row.id]);
|
|
updated += 1;
|
|
}
|
|
|
|
const { rows: paintings } = await client.query(
|
|
'SELECT id, image_path, thumbnail_path FROM paintings',
|
|
);
|
|
for (const row of paintings) {
|
|
const ms = maxMtime(imageDir, [row.image_path, row.thumbnail_path]);
|
|
const ts = ms != null ? new Date(ms) : fallback;
|
|
await client.query('UPDATE paintings SET updated_at = $1 WHERE id = $2', [ts, row.id]);
|
|
updated += 1;
|
|
}
|
|
|
|
const childUpdates = [
|
|
['artist_periods', 'artist_id', 'artists'],
|
|
['painting_influences', 'painting_id', 'paintings'],
|
|
['painting_influence_sources', 'painting_id', 'paintings'],
|
|
['painting_annotations', 'painting_id', 'paintings'],
|
|
];
|
|
|
|
for (const [child, fk, parent] of childUpdates) {
|
|
const res = await client.query(
|
|
`UPDATE ${child} c
|
|
SET updated_at = GREATEST(c.updated_at, p.updated_at)
|
|
FROM ${parent} p
|
|
WHERE c.${fk} = p.id`,
|
|
);
|
|
updated += res.rowCount || 0;
|
|
}
|
|
|
|
for (const table of ['historical_eras', 'art_movements']) {
|
|
const res = await client.query(
|
|
`UPDATE ${table} SET updated_at = $1 WHERE updated_at IS NOT NULL`,
|
|
[fallback],
|
|
);
|
|
updated += res.rowCount || 0;
|
|
}
|
|
|
|
await client.query(
|
|
`UPDATE art_movements m
|
|
SET updated_at = GREATEST(m.updated_at, e.updated_at)
|
|
FROM historical_eras e
|
|
WHERE m.era_id = e.id`,
|
|
);
|
|
|
|
await client.query(
|
|
`UPDATE artists a
|
|
SET updated_at = GREATEST(a.updated_at, m.updated_at)
|
|
FROM art_movements m
|
|
WHERE a.movement_id = m.id`,
|
|
);
|
|
|
|
await client.end();
|
|
|
|
printCliResult({
|
|
script: 'backfill-updated-at',
|
|
ok: true,
|
|
summary: `Backfilled updated_at on ${config.database} (${updated} row touches).`,
|
|
details: [`Image dir: ${imageDir}`],
|
|
});
|
|
}
|
|
|
|
main().catch((err) => {
|
|
printCliResult({
|
|
script: 'backfill-updated-at',
|
|
ok: false,
|
|
summary: err.message,
|
|
});
|
|
});
|