Project-Image-Uploader/backend/src/repositories/GroupRepository.js
matthias.lotz 7564525c7e feat: implement drag-and-drop reordering infrastructure
Phase 1 (Backend API):
 GroupRepository.updateImageOrder() with SQL transactions
 PUT /api/groups/:groupId/reorder API route with validation
 Manual testing: Reordering verified working (group qion_-lT1)
 Error handling: Invalid IDs, missing groups, empty arrays

Phase 2 (Frontend DnD):
 @dnd-kit/core packages installed
 ReorderService.js for API communication
 useReordering.js custom hook with optimistic updates
 ImageGalleryCard.js extended with drag handles & sortable
 ImageGallery.js with DndContext and SortableContext
 CSS styles for drag states, handles, touch-friendly mobile

Next: Integration with ModerationGroupImagesPage
2025-11-03 21:06:39 +01:00

379 lines
13 KiB
JavaScript

const dbManager = require('../database/DatabaseManager');
class GroupRepository {
// Erstelle neue Gruppe mit Bildern (Transaction)
async createGroup(groupData) {
return await dbManager.transaction(async (db) => {
// Füge Gruppe hinzu
const groupResult = await db.run(`
INSERT INTO groups (group_id, year, title, description, name, upload_date)
VALUES (?, ?, ?, ?, ?, ?)
`, [
groupData.groupId,
groupData.year,
groupData.title,
groupData.description || null,
groupData.name || null,
groupData.uploadDate
]);
// Füge Bilder hinzu
if (groupData.images && groupData.images.length > 0) {
for (const image of groupData.images) {
await db.run(`
INSERT INTO images (group_id, file_name, original_name, file_path, upload_order, file_size, mime_type, preview_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [
groupData.groupId,
image.fileName,
image.originalName,
image.filePath,
image.uploadOrder,
image.fileSize || null,
image.mimeType || null,
image.previewPath || null
]);
}
}
return groupResult.id;
});
}
// Hole Gruppe mit Bildern nach Group-ID
async getGroupById(groupId) {
const group = await dbManager.get(`
SELECT * FROM groups WHERE group_id = ?
`, [groupId]);
if (!group) {
return null;
}
const images = await dbManager.all(`
SELECT * FROM images
WHERE group_id = ?
ORDER BY upload_order ASC
`, [groupId]);
return {
groupId: group.group_id,
year: group.year,
title: group.title,
description: group.description,
name: group.name,
uploadDate: group.upload_date,
images: images.map(img => ({
fileName: img.file_name,
originalName: img.original_name,
filePath: img.file_path,
previewPath: img.preview_path,
uploadOrder: img.upload_order,
fileSize: img.file_size,
mimeType: img.mime_type
})),
imageCount: images.length
};
}
// Hole alle Gruppen (mit Paginierung)
async getAllGroups(limit = null, offset = 0) {
let sql = `
SELECT g.*, COUNT(i.id) as image_count
FROM groups g
LEFT JOIN images i ON g.group_id = i.group_id
GROUP BY g.group_id
ORDER BY g.upload_date DESC
`;
const params = [];
if (limit) {
sql += ` LIMIT ? OFFSET ?`;
params.push(limit, offset);
}
const groups = await dbManager.all(sql, params);
return {
groups: groups.map(group => ({
groupId: group.group_id,
year: group.year,
title: group.title,
description: group.description,
name: group.name,
uploadDate: group.upload_date,
imageCount: group.image_count
})),
total: groups.length
};
}
// Hole alle Gruppen mit Bildern für Slideshow (nur freigegebene)
async getAllGroupsWithImages() {
const groupFormatter = require('../utils/groupFormatter');
const groups = await dbManager.all(`
SELECT * FROM groups
WHERE approved = TRUE
ORDER BY upload_date DESC
`);
const result = [];
for (const group of groups) {
const images = await dbManager.all(`
SELECT * FROM images
WHERE group_id = ?
ORDER BY upload_order ASC
`, [group.group_id]);
result.push(groupFormatter.formatGroupDetail(group, images));
}
return result;
}
// Lösche Gruppe und alle Bilder
async deleteGroup(groupId) {
return await dbManager.transaction(async (db) => {
// Erst alle Bilddateien physisch löschen
const images = await db.all(`
SELECT * FROM images WHERE group_id = ?
`, [groupId]);
const fs = require('fs').promises;
const path = require('path');
for (const image of images) {
try {
const absolutePath = path.join(__dirname, '..', image.file_path);
await fs.unlink(absolutePath);
console.log(`✓ Bilddatei gelöscht: ${absolutePath}`);
} catch (error) {
console.warn(`⚠️ Konnte Bilddatei nicht löschen: ${image.file_path}`, error.message);
}
}
// Dann Gruppe aus Datenbank löschen (Bilder werden durch CASCADE gelöscht)
const result = await db.run(`
DELETE FROM groups WHERE group_id = ?
`, [groupId]);
console.log(`✓ Gruppe gelöscht: ${groupId} (${images.length} Bilder)`);
return result.changes > 0;
});
}
// Update Gruppe
async updateGroup(groupId, updates) {
const setClause = [];
const params = [];
if (updates.year !== undefined) {
setClause.push('year = ?');
params.push(updates.year);
}
if (updates.title !== undefined) {
setClause.push('title = ?');
params.push(updates.title);
}
if (updates.description !== undefined) {
setClause.push('description = ?');
params.push(updates.description);
}
if (updates.name !== undefined) {
setClause.push('name = ?');
params.push(updates.name);
}
if (setClause.length === 0) {
return false;
}
params.push(groupId);
const result = await dbManager.run(`
UPDATE groups SET ${setClause.join(', ')} WHERE group_id = ?
`, params);
return result.changes > 0;
}
// Gruppe Freigabe-Status aktualisieren
async updateGroupApproval(groupId, approved) {
const result = await dbManager.run(`
UPDATE groups SET approved = ? WHERE group_id = ?
`, [approved, groupId]);
return result.changes > 0;
}
// Einzelnes Bild löschen
async deleteImage(groupId, imageId) {
return await dbManager.transaction(async (db) => {
// Prüfe ob Bild existiert
const image = await db.get(`
SELECT * FROM images WHERE id = ? AND group_id = ?
`, [imageId, groupId]);
if (!image) {
return false;
}
// Lösche Datei vom Dateisystem
const fs = require('fs').promises;
const path = require('path');
try {
// Konvertiere relativen Pfad zu absolutem Pfad im Container
// image.file_path ist "/upload/dateiname.ext", wir brauchen "/usr/src/app/src/upload/dateiname.ext"
const absolutePath = path.join(__dirname, '..', image.file_path);
await fs.unlink(absolutePath);
console.log(`✓ Bilddatei gelöscht: ${absolutePath}`);
} catch (error) {
console.warn(`⚠️ Konnte Bilddatei nicht löschen: ${image.file_path}`, error.message);
// Datei-Löschfehler sollen nicht das Löschen aus der Datenbank verhindern
}
// Lösche aus Datenbank
const result = await db.run(`
DELETE FROM images WHERE id = ? AND group_id = ?
`, [imageId, groupId]);
// Aktualisiere upload_order der verbleibenden Bilder
await db.run(`
UPDATE images
SET upload_order = upload_order - 1
WHERE group_id = ? AND upload_order > ?
`, [groupId, image.upload_order]);
return result.changes > 0;
});
}
// Alle Gruppen für Moderation (mit Freigabestatus und Bildanzahl)
async getAllGroupsWithModerationInfo() {
const groupFormatter = require('../utils/groupFormatter');
const groups = await dbManager.all(`
SELECT * FROM groups
ORDER BY approved ASC, upload_date DESC
`);
const result = [];
for (const group of groups) {
const images = await dbManager.all(`
SELECT * FROM images
WHERE group_id = ?
ORDER BY upload_order ASC
`, [group.group_id]);
result.push(groupFormatter.formatGroupDetail(group, images));
}
return result;
}
// Hole Gruppe für Moderation (inkl. nicht-freigegebene)
async getGroupForModeration(groupId) {
const group = await dbManager.get(`
SELECT * FROM groups WHERE group_id = ?
`, [groupId]);
if (!group) {
return null;
}
const images = await dbManager.all(`
SELECT * FROM images
WHERE group_id = ?
ORDER BY upload_order ASC
`, [groupId]);
const groupFormatter = require('../utils/groupFormatter');
return groupFormatter.formatGroupDetail(group, images);
}
// Statistiken (erweitert um Freigabe-Status)
async getStats() {
const groupCount = await dbManager.get('SELECT COUNT(*) as count FROM groups');
const imageCount = await dbManager.get('SELECT COUNT(*) as count FROM images');
const approvedGroups = await dbManager.get('SELECT COUNT(*) as count FROM groups WHERE approved = TRUE');
const pendingGroups = await dbManager.get('SELECT COUNT(*) as count FROM groups WHERE approved = FALSE');
const latestGroup = await dbManager.get(`
SELECT upload_date FROM groups ORDER BY upload_date DESC LIMIT 1
`);
return {
totalGroups: groupCount.count,
totalImages: imageCount.count,
approvedGroups: approvedGroups.count,
pendingGroups: pendingGroups.count,
latestUpload: latestGroup ? latestGroup.upload_date : null
};
}
// Aktualisiere die Reihenfolge der Bilder in einer Gruppe
async updateImageOrder(groupId, imageIds) {
if (!groupId) {
throw new Error('Group ID is required');
}
if (!Array.isArray(imageIds) || imageIds.length === 0) {
throw new Error('Image IDs array is required and cannot be empty');
}
return await dbManager.transaction(async (db) => {
// Zunächst prüfen, ob die Gruppe existiert
const group = await db.get('SELECT group_id FROM groups WHERE group_id = ?', [groupId]);
if (!group) {
throw new Error(`Group with ID ${groupId} not found`);
}
// Alle Bilder der Gruppe laden und prüfen ob alle IDs gültig sind
const existingImages = await db.all(
'SELECT id, file_name FROM images WHERE group_id = ? ORDER BY upload_order ASC',
[groupId]
);
const existingImageIds = existingImages.map(img => img.id);
// Prüfen ob alle übergebenen IDs zur Gruppe gehören
const invalidIds = imageIds.filter(id => !existingImageIds.includes(id));
if (invalidIds.length > 0) {
throw new Error(`Invalid image IDs found: ${invalidIds.join(', ')}. These images do not belong to group ${groupId}`);
}
// Prüfen ob alle Bilder der Gruppe in der Liste enthalten sind
const missingIds = existingImageIds.filter(id => !imageIds.includes(id));
if (missingIds.length > 0) {
throw new Error(`Missing image IDs: ${missingIds.join(', ')}. All images of the group must be included in the reorder operation`);
}
// Batch-Update der upload_order Werte
let updateCount = 0;
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const newOrder = i + 1; // upload_order beginnt bei 1
const result = await db.run(
'UPDATE images SET upload_order = ? WHERE id = ? AND group_id = ?',
[newOrder, imageId, groupId]
);
if (result.changes === 0) {
throw new Error(`Failed to update image with ID ${imageId}`);
}
updateCount += result.changes;
}
return {
groupId: groupId,
updatedImages: updateCount,
newOrder: imageIds
};
});
}
}
module.exports = new GroupRepository();