fix: support existing meal planning bridge tables
This commit is contained in:
@@ -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'); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user