import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { find, findMedia, user, findLike, findComment, location } from '$lib/server/db/schema'; import { eq, sql } from 'drizzle-orm'; import { getLocalR2Url, deleteFromR2 } from '$lib/server/r2'; export const GET: RequestHandler = async ({ params, locals }) => { const findId = params.findId; if (!findId) { throw error(400, 'Find ID is required'); } try { // Get the find with user info and like count const findResult = await db .select({ id: find.id, title: find.title, description: find.description, latitude: location.latitude, longitude: location.longitude, locationName: location.locationName, category: find.category, isPublic: find.isPublic, createdAt: find.createdAt, userId: find.userId, username: user.username, profilePictureUrl: user.profilePictureUrl, likeCount: sql`COALESCE(COUNT(DISTINCT ${findLike.id}), 0)`, commentCount: sql`COALESCE(( SELECT COUNT(*) FROM ${findComment} WHERE ${findComment.findId} = ${find.id} ), 0)`, isLikedByUser: locals.user ? sql`CASE WHEN EXISTS( SELECT 1 FROM ${findLike} WHERE ${findLike.findId} = ${find.id} AND ${findLike.userId} = ${locals.user.id} ) THEN 1 ELSE 0 END` : sql`0` }) .from(find) .innerJoin(location, eq(find.locationId, location.id)) .innerJoin(user, eq(find.userId, user.id)) .leftJoin(findLike, eq(find.id, findLike.findId)) .where(eq(find.id, findId)) .groupBy( find.id, location.latitude, location.longitude, location.locationName, user.username, user.profilePictureUrl ) .limit(1); if (findResult.length === 0) { throw error(404, 'Find not found'); } const findData = findResult[0]; // Check if the find is public or if user has access const isOwner = locals.user && findData.userId === locals.user.id; const isPublic = findData.isPublic === 1; if (!isPublic && !isOwner) { throw error(403, 'This find is private'); } // Get media for the find const media = await db .select({ id: findMedia.id, findId: findMedia.findId, type: findMedia.type, url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl, orderIndex: findMedia.orderIndex }) .from(findMedia) .where(eq(findMedia.findId, findId)) .orderBy(findMedia.orderIndex); // Generate signed URLs for media const mediaWithSignedUrls = await Promise.all( media.map(async (mediaItem) => { const localUrl = getLocalR2Url(mediaItem.url); const localThumbnailUrl = mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/') ? getLocalR2Url(mediaItem.thumbnailUrl) : mediaItem.thumbnailUrl; return { ...mediaItem, url: localUrl, thumbnailUrl: localThumbnailUrl }; }) ); // Generate local proxy URL for user profile picture let userProfilePictureUrl = findData.profilePictureUrl; if (userProfilePictureUrl && !userProfilePictureUrl.startsWith('http')) { userProfilePictureUrl = getLocalR2Url(userProfilePictureUrl); } return json({ ...findData, profilePictureUrl: userProfilePictureUrl, media: mediaWithSignedUrls, isLikedByUser: Boolean(findData.isLikedByUser) }); } catch (err) { console.error('Error loading find:', err); if (err instanceof Error && 'status' in err) { throw err; } throw error(500, 'Failed to load find'); } }; export const PUT: RequestHandler = async ({ params, request, locals }) => { if (!locals.user) { throw error(401, 'Unauthorized'); } const findId = params.findId; if (!findId) { throw error(400, 'Find ID is required'); } try { // First, verify the find exists and user owns it const existingFind = await db .select({ userId: find.userId }) .from(find) .where(eq(find.id, findId)) .limit(1); if (existingFind.length === 0) { throw error(404, 'Find not found'); } if (existingFind[0].userId !== locals.user.id) { throw error(403, 'You do not have permission to edit this find'); } // Parse request body const data = await request.json(); const { title, description, category, isPublic, media, mediaToDelete } = data; // Validate required fields if (!title) { throw error(400, 'Title is required'); } if (title.length > 100) { throw error(400, 'Title must be 100 characters or less'); } if (description && description.length > 500) { throw error(400, 'Description must be 500 characters or less'); } // Delete media items if specified if (mediaToDelete && Array.isArray(mediaToDelete) && mediaToDelete.length > 0) { // Get media URLs before deleting from database const mediaToRemove = await db .select({ url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl }) .from(findMedia) .where( sql`${findMedia.id} IN (${sql.join( mediaToDelete.map((id: string) => sql`${id}`), sql`, ` )})` ); // Delete from R2 storage for (const mediaItem of mediaToRemove) { try { await deleteFromR2(mediaItem.url); if (mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')) { await deleteFromR2(mediaItem.thumbnailUrl); } } catch (err) { console.error('Error deleting media from R2:', err); // Continue even if R2 deletion fails } } // Delete from database await db.delete(findMedia).where( sql`${findMedia.id} IN (${sql.join( mediaToDelete.map((id: string) => sql`${id}`), sql`, ` )})` ); } // Update the find const updatedFind = await db .update(find) .set({ title, description: description || null, category: category || null, isPublic: isPublic ? 1 : 0, updatedAt: new Date() }) .where(eq(find.id, findId)) .returning(); // Add new media records if provided if (media && Array.isArray(media) && media.length > 0) { const newMediaRecords = media .filter((item: { id?: string }) => !item.id) // Only insert media without IDs (new uploads) .map((item: { type: string; url: string; thumbnailUrl?: string }, index: number) => ({ id: crypto.randomUUID(), findId, type: item.type, url: item.url, thumbnailUrl: item.thumbnailUrl || null, orderIndex: index })); if (newMediaRecords.length > 0) { await db.insert(findMedia).values(newMediaRecords); } } return json({ success: true, find: updatedFind[0] }); } catch (err) { console.error('Error updating find:', err); if (err instanceof Error && 'status' in err) { throw err; } throw error(500, 'Failed to update find'); } }; export const DELETE: RequestHandler = async ({ params, locals }) => { if (!locals.user) { throw error(401, 'Unauthorized'); } const findId = params.findId; if (!findId) { throw error(400, 'Find ID is required'); } try { // First, verify the find exists and user owns it const existingFind = await db .select({ userId: find.userId }) .from(find) .where(eq(find.id, findId)) .limit(1); if (existingFind.length === 0) { throw error(404, 'Find not found'); } if (existingFind[0].userId !== locals.user.id) { throw error(403, 'You do not have permission to delete this find'); } // Get all media for this find to delete from R2 const mediaItems = await db .select({ url: findMedia.url, thumbnailUrl: findMedia.thumbnailUrl }) .from(findMedia) .where(eq(findMedia.findId, findId)); // Delete media from R2 storage for (const mediaItem of mediaItems) { try { await deleteFromR2(mediaItem.url); if (mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')) { await deleteFromR2(mediaItem.thumbnailUrl); } } catch (err) { console.error('Error deleting media from R2:', err); // Continue even if R2 deletion fails } } // Delete the find (cascade will handle media, likes, and comments) await db.delete(find).where(eq(find.id, findId)); return json({ success: true }); } catch (err) { console.error('Error deleting find:', err); if (err instanceof Error && 'status' in err) { throw err; } throw error(500, 'Failed to delete find'); } };