Bilder von Hand sortieren

This commit is contained in:
2025-09-25 19:09:58 +00:00
parent da9d08c149
commit 0bfb8b2074
16 changed files with 462 additions and 163 deletions

View File

@@ -1,12 +1,13 @@
# Database
DATABASE_URL="mysql://rezepte_user:rezepte_pass@localhost:3307/rezepte"
# Database (inside container use service name 'mysql')
DATABASE_URL="mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte"
# Server
PORT=3001
NODE_ENV=development
# CORS Configuration
CORS_ORIGIN=*
# Limit default origin; override via compose env if needed
CORS_ORIGIN=http://localhost:3000
# Prisma
# DATABASE_URL="file:./dev.db"

View File

@@ -1 +1 @@
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA0HtB,eAAe,GAAG,CAAC"}
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AAoJtB,eAAe,GAAG,CAAC"}

63
backend/dist/app.js vendored
View File

@@ -27,28 +27,49 @@ const limiter = (0, express_rate_limit_1.default)({
message: 'Too many requests from this IP, please try again later.',
});
app.use(limiter);
const allowedOrigins = [
'http://localhost:5173',
'http://localhost:3000',
config_1.config.cors.origin
].filter(Boolean);
const corsConfig = config_1.config.cors.origin === '*'
? {
origin: true,
credentials: true,
}
: {
origin: allowedOrigins,
credentials: true,
};
app.use((0, cors_1.default)(corsConfig));
const insecureOverride = process.env.ALLOW_INSECURE_CORS === '1';
const isProd = process.env.NODE_ENV === 'production';
let allowedOrigins = [];
if (config_1.config.cors.origin.includes(',')) {
allowedOrigins = config_1.config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
}
else if (config_1.config.cors.origin === '*' && (!isProd || insecureOverride)) {
allowedOrigins = ['*'];
}
else {
allowedOrigins = [config_1.config.cors.origin];
}
allowedOrigins = Array.from(new Set(allowedOrigins.map(o => o.replace(/\/$/, ''))));
if (!isProd && !allowedOrigins.includes('*')) {
['http://localhost:5173', 'http://localhost:3000'].forEach(def => {
if (!allowedOrigins.includes(def))
allowedOrigins.push(def);
});
}
if (isProd && allowedOrigins.includes('*') && !insecureOverride) {
console.warn('[CORS] Wildcard removed in production. Set CORS_ORIGIN explicitly or ALLOW_INSECURE_CORS=1 (NOT RECOMMENDED).');
allowedOrigins = allowedOrigins.filter(o => o !== '*');
}
app.use((0, cors_1.default)({
origin: (origin, callback) => {
if (!origin)
return callback(null, true);
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin.replace(/\/$/, ''))) {
return callback(null, true);
}
console.warn(`[CORS] Blocked origin: ${origin}`);
return callback(new Error('CORS not allowed for this origin'));
},
credentials: true,
}));
app.use((req, res, next) => {
const origin = req.headers.origin;
if (config_1.config.cors.origin === '*') {
const normalized = origin?.replace(/\/$/, '');
if (allowedOrigins.includes('*')) {
res.header('Access-Control-Allow-Origin', origin || '*');
}
else if (origin && allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
else if (normalized && allowedOrigins.includes(normalized)) {
res.header('Access-Control-Allow-Origin', normalized);
}
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
@@ -80,8 +101,12 @@ app.get('/serve/*', (req, res, next) => {
resolvedPath: fullPath
});
}
const requestOrigin = req.headers.origin;
const chosenOrigin = allowedOrigins.includes('*')
? (requestOrigin || '*')
: (requestOrigin && allowedOrigins.includes(requestOrigin) ? requestOrigin : allowedOrigins[0] || 'http://localhost:3000');
res.set({
'Access-Control-Allow-Origin': 'http://localhost:5173',
'Access-Control-Allow-Origin': chosenOrigin,
'Access-Control-Allow-Credentials': 'true',
'Cache-Control': 'public, max-age=31536000',
});

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0RxB,eAAe,MAAM,CAAC"}
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiYxB,eAAe,MAAM,CAAC"}

View File

@@ -1,11 +1,44 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const multer_1 = __importDefault(require("multer"));
const multer_1 = __importStar(require("multer"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const config_1 = require("../config/config");
@@ -19,29 +52,68 @@ const getUploadsDir = (subPath) => {
: legacyUploadsDir;
return subPath ? path_1.default.join(baseDir, subPath) : baseDir;
};
const loadRecipe = async (req, res, next) => {
try {
const { recipeId } = req.params;
if (!recipeId || isNaN(Number(recipeId))) {
return res.status(400).json({ success: false, message: 'Valid recipeId is required' });
}
const recipe = await prisma.recipe.findUnique({ where: { id: Number(recipeId) } });
if (!recipe) {
return res.status(404).json({ success: false, message: 'Recipe not found' });
}
req.recipeNumber = recipe.recipeNumber;
res.locals.recipe = recipe;
next();
}
catch (err) {
next(err);
}
};
const mimeExt = {
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'image/webp': 'webp'
};
const storage = multer_1.default.diskStorage({
destination: (req, file, cb) => {
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
if (!recipeNumber) {
return cb(new Error('Recipe number is required'), '');
}
const uploadDir = getUploadsDir(recipeNumber);
if (!fs_1.default.existsSync(uploadDir)) {
fs_1.default.mkdirSync(uploadDir, { recursive: true });
try {
if (!fs_1.default.existsSync(uploadDir)) {
fs_1.default.mkdirSync(uploadDir, { recursive: true });
}
}
catch (e) {
return cb(new Error('Failed to prepare upload directory'), '');
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
if (!recipeNumber) {
return cb(new Error('Recipe number is required'), '');
}
const uploadDir = getUploadsDir(recipeNumber);
const existingFiles = fs_1.default.existsSync(uploadDir)
? fs_1.default.readdirSync(uploadDir).filter(f => f.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`)))
? fs_1.default.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`))
: [];
const nextIndex = existingFiles.length;
const filename = `${recipeNumber}_${nextIndex}.jpg`;
let maxIndex = -1;
existingFiles.forEach(f => {
const m = f.match(new RegExp(`^${recipeNumber}_(\\d+)`));
if (m && m[1] !== undefined) {
const idx = parseInt(m[1], 10);
if (!Number.isNaN(idx) && idx > maxIndex)
maxIndex = idx;
}
});
const nextIndex = maxIndex + 1;
const ext = mimeExt[file.mimetype] || 'jpg';
const filename = `${recipeNumber}_${nextIndex}.${ext}`;
cb(null, filename);
}
});
@@ -60,41 +132,31 @@ const upload = (0, multer_1.default)({
}
},
});
router.post('/upload/:recipeId', upload.array('images', 10), async (req, res, next) => {
router.post('/upload/:recipeId', loadRecipe, (req, res, next) => {
const mw = upload.array('images', 10);
mw(req, res, function (err) {
if (err) {
if (err instanceof multer_1.MulterError) {
return res.status(400).json({ success: false, message: `Upload error: ${err.message}` });
}
return res.status(400).json({ success: false, message: err.message || 'Upload failed' });
}
next();
});
}, async (req, res, next) => {
try {
const { recipeId } = req.params;
const files = req.files;
if (!recipeId) {
return res.status(400).json({
success: false,
message: 'Recipe ID is required',
});
}
const recipe = res.locals.recipe;
if (!files || files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded',
});
return res.status(400).json({ success: false, message: 'No files uploaded' });
}
const recipe = await prisma.recipe.findUnique({
where: { id: parseInt(recipeId) }
});
if (!recipe) {
return res.status(404).json({
success: false,
message: 'Recipe not found',
});
}
const imagePromises = files.map(file => {
const images = await Promise.all(files.map(file => {
const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`;
return prisma.recipeImage.create({
data: {
recipeId: parseInt(recipeId),
filePath: relativePath,
}
data: { recipeId: Number(recipeId), filePath: relativePath }
});
});
const images = await Promise.all(imagePromises);
}));
return res.status(201).json({
success: true,
data: images,
@@ -104,11 +166,8 @@ router.post('/upload/:recipeId', upload.array('images', 10), async (req, res, ne
catch (error) {
if (req.files) {
const files = req.files;
files.forEach(file => {
if (fs_1.default.existsSync(file.path)) {
fs_1.default.unlinkSync(file.path);
}
});
files.forEach(file => { if (fs_1.default.existsSync(file.path))
fs_1.default.unlinkSync(file.path); });
}
next(error);
}
@@ -234,5 +293,71 @@ router.get('/:id', async (req, res, next) => {
next(error);
}
});
router.post('/reorder/:recipeId', async (req, res, next) => {
try {
const { recipeId } = req.params;
const { order } = req.body;
if (!recipeId || isNaN(Number(recipeId))) {
return res.status(400).json({ success: false, message: 'Valid recipeId required' });
}
if (!Array.isArray(order) || order.length === 0) {
return res.status(400).json({ success: false, message: 'order (non-empty array) required' });
}
const rid = Number(recipeId);
const recipe = await prisma.recipe.findUnique({ where: { id: rid } });
if (!recipe)
return res.status(404).json({ success: false, message: 'Recipe not found' });
const images = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
if (images.length === 0)
return res.status(400).json({ success: false, message: 'No images to reorder' });
const existingIds = images.map(i => i.id).sort((a, b) => a - b);
const provided = [...order].sort((a, b) => a - b);
if (existingIds.length !== provided.length || !existingIds.every((v, i) => v === provided[i])) {
return res.status(400).json({ success: false, message: 'order must contain all image IDs exactly once' });
}
const recipeNumber = recipe.recipeNumber;
const baseDir = getUploadsDir(recipeNumber);
if (!fs_1.default.existsSync(baseDir)) {
return res.status(500).json({ success: false, message: 'Upload directory missing on server' });
}
const idToImage = new Map(images.map(i => [i.id, i]));
const newSequence = order.map((id, idx) => ({ idx, image: idToImage.get(id) }));
const tempRenames = [];
for (const { idx, image } of newSequence) {
const fileName = image.filePath.split('/').pop() || '';
const extMatch = fileName.match(/\.(jpg|jpeg|png|webp)$/i);
const ext = extMatch && extMatch[1] ? extMatch[1].toLowerCase() : 'jpg';
const finalName = `${recipeNumber}_${idx}.${ext === 'jpeg' ? 'jpg' : ext}`;
const oldFull = path_1.default.join(baseDir, path_1.default.basename(fileName));
const tempName = `${recipeNumber}__reorder_${idx}_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`;
const tempFull = path_1.default.join(baseDir, tempName);
const finalFull = path_1.default.join(baseDir, finalName);
if (!fs_1.default.existsSync(oldFull)) {
return res.status(500).json({ success: false, message: `File missing on disk: ${fileName}` });
}
fs_1.default.renameSync(oldFull, tempFull);
tempRenames.push({ from: tempFull, to: finalFull, final: `uploads/${recipeNumber}/${finalName}`, idx, ext });
}
for (const r of tempRenames) {
fs_1.default.renameSync(r.from, r.to);
}
await Promise.all(newSequence.map(({ idx, image }) => {
const renameRecord = tempRenames.find(tr => tr.idx === idx);
return prisma.recipeImage.update({
where: { id: image.id },
data: { filePath: renameRecord.final }
});
}));
const updated = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
return res.json({
success: true,
message: 'Images reordered',
data: updated,
});
}
catch (error) {
next(error);
}
});
exports.default = router;
//# sourceMappingURL=images.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import { Router, Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import multer from 'multer';
import multer, { MulterError } from 'multer';
import path from 'path';
import fs from 'fs';
import { config } from '../config/config';
@@ -21,38 +21,72 @@ const getUploadsDir = (subPath?: string): string => {
return subPath ? path.join(baseDir, subPath) : baseDir;
};
// Configure multer for file uploads
// Middleware: load recipe by :recipeId and attach recipeNumber for storage
const loadRecipe = async (req: Request, res: Response, next: NextFunction) => {
try {
const { recipeId } = req.params;
if (!recipeId || isNaN(Number(recipeId))) {
return res.status(400).json({ success: false, message: 'Valid recipeId is required' });
}
const recipe = await prisma.recipe.findUnique({ where: { id: Number(recipeId) } });
if (!recipe) {
return res.status(404).json({ success: false, message: 'Recipe not found' });
}
// Attach for later usage
(req as any).recipeNumber = recipe.recipeNumber;
res.locals.recipe = recipe;
next();
} catch (err) {
next(err);
}
};
// Map mimetypes to extensions
const mimeExt: Record<string, string> = {
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'image/webp': 'webp'
};
// Configure multer for file uploads (uses injected recipeNumber)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
const recipeNumber = (req as any).recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
if (!recipeNumber) {
return cb(new Error('Recipe number is required'), '');
}
const uploadDir = getUploadsDir(recipeNumber);
// Create directory if it doesn't exist
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
try {
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
} catch (e) {
return cb(new Error('Failed to prepare upload directory'), '');
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
const recipeNumber = (req as any).recipeNumber || req.body.recipeNumber || req.params.recipeNumber;
if (!recipeNumber) {
return cb(new Error('Recipe number is required'), '');
}
// Get existing files count to determine next index
const uploadDir = getUploadsDir(recipeNumber);
const existingFiles = fs.existsSync(uploadDir)
? fs.readdirSync(uploadDir).filter(f => f.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`)))
const existingFiles = fs.existsSync(uploadDir)
? fs.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`))
: [];
const nextIndex = existingFiles.length;
const filename = `${recipeNumber}_${nextIndex}.jpg`;
// Determine next index by scanning existing indices
let maxIndex = -1;
existingFiles.forEach(f => {
const m = f.match(new RegExp(`^${recipeNumber}_(\\d+)`));
if (m && m[1] !== undefined) {
const idx = parseInt(m[1]!, 10);
if (!Number.isNaN(idx) && idx > maxIndex) maxIndex = idx;
}
});
const nextIndex = maxIndex + 1;
const ext = mimeExt[file.mimetype] || 'jpg';
const filename = `${recipeNumber}_${nextIndex}.${ext}`;
cb(null, filename);
}
});
@@ -72,65 +106,44 @@ const upload = multer({
},
});
// Upload images for a recipe
router.post('/upload/:recipeId', upload.array('images', 10), async (req: Request, res: Response, next: NextFunction) => {
// Upload images for a recipe (preload recipe middleware before multer)
router.post('/upload/:recipeId', loadRecipe, (req, res, next) => {
const mw = upload.array('images', 10);
mw(req, res, function(err: any) {
if (err) {
if (err instanceof MulterError) {
return res.status(400).json({ success: false, message: `Upload error: ${err.message}` });
}
return res.status(400).json({ success: false, message: err.message || 'Upload failed' });
}
next();
});
}, async (req: Request, res: Response, next: NextFunction) => {
try {
const { recipeId } = req.params;
const files = req.files as Express.Multer.File[];
if (!recipeId) {
return res.status(400).json({
success: false,
message: 'Recipe ID is required',
});
}
const recipe = res.locals.recipe;
if (!files || files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded',
});
return res.status(400).json({ success: false, message: 'No files uploaded' });
}
// Get recipe to validate it exists and get recipe number
const recipe = await prisma.recipe.findUnique({
where: { id: parseInt(recipeId) }
});
if (!recipe) {
return res.status(404).json({
success: false,
message: 'Recipe not found',
});
}
// Create database entries for uploaded images
const imagePromises = files.map(file => {
const images = await Promise.all(files.map(file => {
const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`;
return prisma.recipeImage.create({
data: {
recipeId: parseInt(recipeId),
filePath: relativePath,
}
data: { recipeId: Number(recipeId), filePath: relativePath }
});
});
const images = await Promise.all(imagePromises);
}));
return res.status(201).json({
success: true,
data: images,
message: `${files.length} images uploaded successfully`,
});
} catch (error) {
// Clean up uploaded files if database operation fails
if (req.files) {
const files = req.files as Express.Multer.File[];
files.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
files.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); });
}
next(error);
}
@@ -287,4 +300,94 @@ router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
}
});
// Reorder images for a recipe
router.post('/reorder/:recipeId', async (req: Request, res: Response, next: NextFunction) => {
/*
Payload: { order: number[] }
- order: Array of image IDs in desired final sequence.
- First becomes *_0.ext (main), second *_1.ext, ...
*/
try {
const { recipeId } = req.params;
const { order } = req.body as { order?: number[] };
if (!recipeId || isNaN(Number(recipeId))) {
return res.status(400).json({ success: false, message: 'Valid recipeId required' });
}
if (!Array.isArray(order) || order.length === 0) {
return res.status(400).json({ success: false, message: 'order (non-empty array) required' });
}
const rid = Number(recipeId);
const recipe = await prisma.recipe.findUnique({ where: { id: rid } });
if (!recipe) return res.status(404).json({ success: false, message: 'Recipe not found' });
const images = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
if (images.length === 0) return res.status(400).json({ success: false, message: 'No images to reorder' });
// Validate order covers exactly all image IDs
const existingIds = images.map(i => i.id).sort((a,b)=>a-b);
const provided = [...order].sort((a,b)=>a-b);
if (existingIds.length !== provided.length || !existingIds.every((v,i)=>v===provided[i])) {
return res.status(400).json({ success: false, message: 'order must contain all image IDs exactly once' });
}
const recipeNumber = recipe.recipeNumber;
const baseDir = getUploadsDir(recipeNumber);
if (!fs.existsSync(baseDir)) {
return res.status(500).json({ success: false, message: 'Upload directory missing on server' });
}
// Build mapping: targetIndex -> image object
const idToImage = new Map(images.map(i => [i.id, i] as const));
const newSequence = order.map((id, idx) => ({ idx, image: idToImage.get(id)! }));
// Phase 1: rename to temporary names to avoid collisions
const tempRenames: { from: string; to: string; final: string; idx: number; ext: string }[] = [];
for (const { idx, image } of newSequence) {
const fileName = image.filePath.split('/').pop() || '';
const extMatch = fileName.match(/\.(jpg|jpeg|png|webp)$/i);
const ext = extMatch && extMatch[1] ? extMatch[1].toLowerCase() : 'jpg';
const finalName = `${recipeNumber}_${idx}.${ext === 'jpeg' ? 'jpg' : ext}`;
// Previous implementation incorrectly traversed ../../ resulting in /uploads/... (missing /app prefix in container)
// image.filePath is like 'uploads/R100/R100_0.png' and baseDir points to '/app/uploads/R100'
// So we join baseDir with just the filename to locate the current file.
const oldFull = path.join(baseDir, path.basename(fileName));
const tempName = `${recipeNumber}__reorder_${idx}_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`;
const tempFull = path.join(baseDir, tempName);
const finalFull = path.join(baseDir, finalName);
if (!fs.existsSync(oldFull)) {
return res.status(500).json({ success: false, message: `File missing on disk: ${fileName}` });
}
fs.renameSync(oldFull, tempFull);
tempRenames.push({ from: tempFull, to: finalFull, final: `uploads/${recipeNumber}/${finalName}`, idx, ext });
}
// Phase 2: rename temp -> final
for (const r of tempRenames) {
fs.renameSync(r.from, r.to);
}
// Update DB entries in a transaction-like sequence
// (MySQL autocommit; if one fails we attempt best-effort rollback is complex -> assume disk ops succeeded)
await Promise.all(newSequence.map(({ idx, image }) => {
const renameRecord = tempRenames.find(tr => tr.idx === idx)!;
return prisma.recipeImage.update({
where: { id: image.id },
data: { filePath: renameRecord.final }
});
}));
const updated = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } });
return res.json({
success: true,
message: 'Images reordered',
data: updated,
});
} catch (error) {
next(error);
}
});
export default router;