fix: support existing meal planning bridge tables

This commit is contained in:
OpenClaw Bot
2026-05-11 23:36:52 +02:00
parent ba534cb864
commit 1828bef8f1
3 changed files with 133 additions and 35 deletions
+66 -25
View File
@@ -5,6 +5,7 @@
*/
import express from 'express';
import crypto from 'node:crypto';
import * as db from '../db.js';
import { createLogger } from '../logger.js';
import { str, num, date, oneOf, collectErrors, MAX_TITLE, MAX_TEXT, MAX_SHORT } from '../middleware/validate.js';
@@ -53,6 +54,30 @@ function handleError(res, err, context) {
res.status(err.status || 500).json({ error: err.status ? err.message : 'Internal server error.', code: err.status || 500 });
}
function currentUserId(req) {
return req.authUserId ?? req.session?.userId ?? null;
}
function tableColumns(table) {
return new Set(db.get().prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
}
function insertWithOptionalTextId(table, columns, values) {
const available = tableColumns(table);
const insertColumns = [...columns];
const insertValues = [...values];
if (available.has('id')) {
const idInfo = db.get().prepare(`PRAGMA table_info(${table})`).all().find((row) => row.name === 'id');
if (String(idInfo?.type || '').toUpperCase() !== 'INTEGER') {
insertColumns.unshift('id');
insertValues.unshift(crypto.randomUUID());
}
}
const placeholders = insertColumns.map(() => '?').join(', ');
const result = db.get().prepare(`INSERT INTO ${table} (${insertColumns.join(', ')}) VALUES (${placeholders})`).run(...insertValues);
return db.get().prepare(`SELECT * FROM ${table} WHERE rowid = ?`).get(result.lastInsertRowid);
}
router.get('/cooking-rules', (_req, res) => {
try {
const rows = db.get().prepare(`
@@ -80,7 +105,17 @@ router.put('/cooking-rules', (req, res) => {
if (!VALID_WEEKDAYS.includes(weekday)) throw Object.assign(new Error('Weekday must be 0-6.'), { status: 400 });
const mealType = VALID_MEAL_TYPES.includes(rule.meal_type || rule.mealType || 'dinner') ? (rule.meal_type || rule.mealType || 'dinner') : 'dinner';
const priority = Number.isFinite(Number(rule.priority)) ? Number(rule.priority) : 100;
insert.run(userId, weekday, mealType, priority, req.session.userId);
if (tableColumns('meal_cooking_rules').has('id')) {
const idInfo = db.get().prepare(`PRAGMA table_info(meal_cooking_rules)`).all().find((row) => row.name === 'id');
if (String(idInfo?.type || '').toUpperCase() !== 'INTEGER') {
db.get().prepare(`
INSERT INTO meal_cooking_rules (id, user_id, weekday, meal_type, priority, created_by)
VALUES (?, ?, ?, ?, ?, ?)
`).run(crypto.randomUUID(), userId, weekday, mealType, priority, currentUserId(req));
continue;
}
}
insert.run(userId, weekday, mealType, priority, currentUserId(req));
}
})();
const data = db.get().prepare('SELECT * FROM meal_cooking_rules ORDER BY weekday ASC, meal_type ASC, priority DESC').all();
@@ -127,7 +162,7 @@ router.put('/recipe-signals/:recipeId', (req, res) => {
asBool(req.body.adult_only ?? req.body.adultOnly),
Number(req.body.swap_in_count ?? req.body.swapInCount ?? 0),
Number(req.body.swap_away_count ?? req.body.swapAwayCount ?? 0),
req.session.userId
currentUserId(req)
);
const data = db.get().prepare('SELECT * FROM recipe_family_preferences WHERE recipe_id = ? AND user_id = ?').get(recipeId, userId);
res.json({ data });
@@ -162,7 +197,7 @@ router.put('/variation-meta/:recipeId', (req, res) => {
style = excluded.style,
kid_suitable_confidence = excluded.kid_suitable_confidence,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
`).run(recipeId, protein.value, style.value, kidConfidence, req.session.userId);
`).run(recipeId, protein.value, style.value, kidConfidence, currentUserId(req));
const data = db.get().prepare('SELECT * FROM recipe_variation_meta WHERE recipe_id = ?').get(recipeId);
res.json({ data });
} catch (err) { handleError(res, err, 'PUT /variation-meta/:recipeId'); }
@@ -195,16 +230,18 @@ router.put('/cook-assignments/:mealId', (req, res) => {
const mealType = VALID_MEAL_TYPES.includes(req.body.meal_type || req.body.mealType || meal.meal_type) ? (req.body.meal_type || req.body.mealType || meal.meal_type) : meal.meal_type;
const errors = collectErrors([vDate]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
db.get().prepare(`
INSERT INTO planned_meal_cooks (meal_id, user_id, planned_for_date, meal_type, source_plan_id, created_by, updated_at)
VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
ON CONFLICT(meal_id) DO UPDATE SET
user_id = excluded.user_id,
planned_for_date = excluded.planned_for_date,
meal_type = excluded.meal_type,
source_plan_id = excluded.source_plan_id,
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
`).run(mealId, userId, vDate.value, mealType, req.body.source_plan_id ?? req.body.sourcePlanId ?? null, req.session.userId);
const update = db.get().prepare(`
UPDATE planned_meal_cooks
SET user_id = ?, planned_for_date = ?, meal_type = ?, source_plan_id = ?, created_by = COALESCE(created_by, ?), updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
WHERE meal_id = ?
`).run(userId, vDate.value, mealType, req.body.source_plan_id ?? req.body.sourcePlanId ?? null, currentUserId(req), mealId);
if (update.changes === 0) {
insertWithOptionalTextId(
'planned_meal_cooks',
['meal_id', 'user_id', 'planned_for_date', 'meal_type', 'source_plan_id', 'created_by', 'updated_at'],
[mealId, userId, vDate.value, mealType, req.body.source_plan_id ?? req.body.sourcePlanId ?? null, currentUserId(req), new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')]
);
}
const data = db.get().prepare('SELECT * FROM planned_meal_cooks WHERE meal_id = ?').get(mealId);
res.json({ data });
} catch (err) { handleError(res, err, 'PUT /cook-assignments/:mealId'); }
@@ -237,14 +274,16 @@ router.post('/feedback', (req, res) => {
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
const recipeId = req.body.recipe_id || req.body.recipeId ? ensureRecipe(req.body.recipe_id ?? req.body.recipeId) : null;
const mealId = req.body.meal_id || req.body.mealId ? ensureMeal(req.body.meal_id ?? req.body.mealId) : null;
const result = db.get().prepare(`
INSERT INTO meal_plan_feedback (plan_id, meal_id, recipe_id, slot_date, meal_type, action, original_title, final_title, notes, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
const columns = ['plan_id', 'meal_id', 'recipe_id', 'slot_date', 'meal_type', 'action', 'original_title', 'final_title', 'notes', 'user_id'];
const values = [
req.body.plan_id ?? req.body.planId ?? null, mealId, recipeId, slotDate.value, mealType.value,
action.value, originalTitle.value, finalTitle.value, notes.value, req.session.userId
);
const data = db.get().prepare('SELECT * FROM meal_plan_feedback WHERE id = ?').get(result.lastInsertRowid);
action.value, originalTitle.value, finalTitle.value, notes.value, currentUserId(req),
];
if (tableColumns('meal_plan_feedback').has('type')) {
columns.push('type');
values.push(action.value);
}
const data = insertWithOptionalTextId('meal_plan_feedback', columns, values);
res.status(201).json({ data });
} catch (err) { handleError(res, err, 'POST /feedback'); }
});
@@ -270,11 +309,13 @@ router.post('/kids-cookbooks', (req, res) => {
const errors = collectErrors([title]);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
if (!content) return res.status(400).json({ error: 'Content object is required.', code: 400 });
const result = db.get().prepare(`
INSERT INTO kids_cookbooks (recipe_id, title, content_json, created_by)
VALUES (?, ?, ?, ?)
`).run(recipeId, title.value, JSON.stringify(content), req.session.userId);
const data = db.get().prepare('SELECT * FROM kids_cookbooks WHERE id = ?').get(result.lastInsertRowid);
const columns = ['recipe_id', 'title', 'content_json', 'created_by'];
const values = [recipeId, title.value, JSON.stringify(content), currentUserId(req)];
if (tableColumns('kids_cookbooks').has('payload')) {
columns.push('payload');
values.push(JSON.stringify(content));
}
const data = insertWithOptionalTextId('kids_cookbooks', columns, values);
res.status(201).json({ data: { ...data, content: JSON.parse(data.content_json) } });
} catch (err) { handleError(res, err, 'POST /kids-cookbooks'); }
});