jedmund-svelte/src/routes/api/media/+server.ts
Justin Edmund 8ec4c582c1 fix: eliminate remaining any types in API routes
- use Prisma.JsonValue and Prisma input types throughout
- add proper type guards for array and object checks
- replace any with Record<string, unknown> where appropriate
- all API/RSS routes now have zero any type errors
2025-11-23 05:28:05 -08:00

218 lines
4.9 KiB
TypeScript

import type { RequestHandler } from './$types'
import type { Prisma } from '@prisma/client'
import { prisma } from '$lib/server/database'
import {
jsonResponse,
errorResponse,
getPaginationParams,
getPaginationMeta,
checkAdminAuth
} from '$lib/server/api-utils'
import { logger } from '$lib/server/logger'
// GET /api/media - List all media with pagination and filters
export const GET: RequestHandler = async (event) => {
// Check authentication
if (!checkAdminAuth(event)) {
return errorResponse('Unauthorized', 401)
}
try {
const { page, limit } = getPaginationParams(event.url)
const skip = (page - 1) * limit
// Get filter parameters
const mimeType = event.url.searchParams.get('mimeType')
const unused = event.url.searchParams.get('unused') === 'true'
const search = event.url.searchParams.get('search')
const publishedFilter = event.url.searchParams.get('publishedFilter')
const sort = event.url.searchParams.get('sort') || 'newest'
const albumId = event.url.searchParams.get('albumId')
// Build where clause
const whereConditions: Prisma.MediaWhereInput[] = []
// Handle mime type filtering
if (mimeType && mimeType !== 'all') {
switch (mimeType) {
case 'image':
// JPG, PNG images (excluding GIF which is in video)
whereConditions.push({
OR: [
{ mimeType: { startsWith: 'image/jpeg' } },
{ mimeType: { startsWith: 'image/jpg' } },
{ mimeType: { startsWith: 'image/png' } },
{ mimeType: { startsWith: 'image/webp' } }
]
})
break
case 'video':
// MP4, MOV, GIF
whereConditions.push({
OR: [{ mimeType: { startsWith: 'video/' } }, { mimeType: { equals: 'image/gif' } }]
})
break
case 'audio':
// WAV, MP3, M4A
whereConditions.push({
mimeType: { startsWith: 'audio/' }
})
break
case 'vector':
// SVG
whereConditions.push({
mimeType: { equals: 'image/svg+xml' }
})
break
}
}
if (unused) {
whereConditions.push({ usedIn: { equals: [] } })
}
if (search) {
whereConditions.push({ filename: { contains: search, mode: 'insensitive' } })
}
// Filter by album if specified
if (albumId) {
whereConditions.push({
albums: {
some: {
albumId: parseInt(albumId)
}
}
})
}
// Handle published filter
if (publishedFilter && publishedFilter !== 'all') {
switch (publishedFilter) {
case 'unpublished':
// Media that is not used anywhere and not marked as photography
whereConditions.push({
AND: [
{ usedIn: { equals: [] } },
{ isPhotography: false },
{
usage: {
none: {}
}
},
{
albums: {
none: {}
}
}
]
})
break
case 'photos':
// Media marked as photography or used in albums
whereConditions.push({
OR: [
{ isPhotography: true },
{ usedIn: { array_contains: 'album' } },
{
albums: {
some: {}
}
}
]
})
break
case 'universe':
// Media used in blog posts or essays (check both usedIn and usage relation)
whereConditions.push({
OR: [
{ usedIn: { array_contains: 'post' } },
{ usedIn: { array_contains: 'essay' } },
{
usage: {
some: {
contentType: { in: ['post', 'essay'] }
}
}
}
]
})
break
}
}
// Combine all conditions with AND
const where =
whereConditions.length > 0
? whereConditions.length === 1
? whereConditions[0]
: { AND: whereConditions }
: {}
// Build orderBy clause based on sort parameter
let orderBy: Prisma.MediaOrderByWithRelationInput = { createdAt: 'desc' } // default to newest
switch (sort) {
case 'oldest':
orderBy = { createdAt: 'asc' }
break
case 'name-asc':
orderBy = { filename: 'asc' }
break
case 'name-desc':
orderBy = { filename: 'desc' }
break
case 'size-asc':
orderBy = { size: 'asc' }
break
case 'size-desc':
orderBy = { size: 'desc' }
break
case 'newest':
default:
orderBy = { createdAt: 'desc' }
break
}
// Get total count
const total = await prisma.media.count({ where })
// Get media items
const media = await prisma.media.findMany({
where,
orderBy,
skip,
take: limit,
select: {
id: true,
filename: true,
mimeType: true,
size: true,
url: true,
thumbnailUrl: true,
width: true,
height: true,
dominantColor: true,
colors: true,
aspectRatio: true,
usedIn: true,
isPhotography: true,
createdAt: true,
description: true,
exifData: true
}
})
const pagination = getPaginationMeta(total, page, limit)
logger.info('Media list retrieved', { total, page, limit })
return jsonResponse({
media,
pagination
})
} catch (error) {
logger.error('Failed to retrieve media', error as Error)
return errorResponse('Failed to retrieve media', 500)
}
}