## 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>
104 lines
3.7 KiB
JavaScript
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();
|
|
}
|