Files
formulation/scripts/migrate-recipe-ids.mjs
T
nicholas 3c2bc768e0
Build & Deploy Formulation / Build & Push Image (push) Successful in 1m3s
Build & Deploy Formulation / deploy (push) Successful in 19s
refactor: simplify
## Summary

- update recipe data, improve mobile
- simplify app, API; remove bad MCP layer
- consolidate database schema into initial migration

Co-authored-by: Nicholas Ward <nicholaspward@outlook.com>
2026-08-30 20:22:09 -05:00

104 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
import path from "node:path";
import { randomBytes } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { writeSiteProjection } from "./lib/site-projection.mjs";
const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
const recipeIdPattern = /^[0-9a-hjkmnp-tv-z]{26}$/;
function encode(value, length) {
let output = "";
for (let index = 0; index < length; index += 1) {
output = alphabet[value % 32] + output;
value = Math.floor(value / 32);
}
return output;
}
function newRecipeId() {
const bytes = randomBytes(16);
let suffix = "";
for (const byte of bytes) suffix += alphabet[byte & 31];
return `${encode(Date.now(), 10)}${suffix}`;
}
function replaceIds(value, ids) {
if (typeof value === "string") return ids.get(value) ?? value;
if (Array.isArray(value)) return value.map((entry) => replaceIds(entry, ids));
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, replaceIds(entry, ids)]));
}
return value;
}
function migrateJsonColumn(database, table, keyColumn, jsonColumn, ids) {
const rows = database.prepare(`SELECT ${keyColumn} AS row_key, ${jsonColumn} AS json_value FROM ${table}`).all();
const update = database.prepare(`UPDATE ${table} SET ${jsonColumn}=? WHERE ${keyColumn}=?`);
for (const row of rows) {
if (!row.json_value) continue;
try {
const current = JSON.parse(row.json_value);
const migrated = replaceIds(current, ids);
update.run(JSON.stringify(migrated), row.row_key);
} catch {
// Non-JSON source text is left untouched.
}
}
}
const root = path.resolve(import.meta.dirname, "..");
const database = new DatabaseSync(path.join(root, "var", "recipe-book.sqlite"));
const rows = database.prepare("SELECT id FROM recipes ORDER BY id").all();
const existing = new Set(rows.map((row) => row.id));
const ids = new Map();
for (const { id } of rows) {
if (recipeIdPattern.test(id)) continue;
let replacement = newRecipeId();
while (existing.has(replacement)) replacement = newRecipeId();
existing.add(replacement);
ids.set(id, replacement);
}
if (!ids.size) {
console.log("Recipe IDs are already immutable; nothing to migrate.");
database.close();
process.exit(0);
}
database.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
try {
const recipeIdTables = [
"recipe_components",
"recipe_items",
"item_prep_actions",
"recipe_steps",
"recipe_equipment",
"step_equipment",
"recipe_measure_conversions",
"collection_recipes",
"recipe_media",
];
for (const [oldId, newId] of ids) {
for (const table of recipeIdTables) database.prepare(`UPDATE ${table} SET recipe_id=? WHERE recipe_id=?`).run(newId, oldId);
database.prepare("UPDATE recipe_items SET subrecipe_id=? WHERE subrecipe_id=?").run(newId, oldId);
database.prepare("UPDATE source_mappings SET subject_id=? WHERE subject_type='recipe' AND subject_id=?").run(newId, oldId);
database.prepare("UPDATE recipes SET id=? WHERE id=?").run(newId, oldId);
}
migrateJsonColumn(database, "recipes", "id", "source_json", ids);
migrateJsonColumn(database, "collections", "id", "source_json", ids);
migrateJsonColumn(database, "source_mappings", "id", "source_json", ids);
database.exec("COMMIT; PRAGMA foreign_keys=ON");
const violations = database.prepare("PRAGMA foreign_key_check").all();
if (violations.length) throw new Error(`Foreign-key validation failed: ${JSON.stringify(violations)}`);
writeSiteProjection(database);
console.log(`Migrated ${ids.size} recipe IDs to immutable identifiers.`);
} catch (error) {
try { database.exec("ROLLBACK"); } catch {}
database.exec("PRAGMA foreign_keys=ON");
throw error;
} finally {
database.close();
}