- add ProjectUpdateBody interface for partial updates - use Prisma.ProjectUpdateInput for update operations - replace all remaining any types in projects endpoints - consistent use of proper types across all API routes
321 lines
9.7 KiB
TypeScript
321 lines
9.7 KiB
TypeScript
import type { RequestHandler } from './$types'
|
|
import type { Prisma } from '@prisma/client'
|
|
import { prisma } from '$lib/server/database'
|
|
import {
|
|
jsonResponse,
|
|
errorResponse,
|
|
checkAdminAuth,
|
|
parseRequestBody
|
|
} from '$lib/server/api-utils'
|
|
import { logger } from '$lib/server/logger'
|
|
import { ensureUniqueSlug } from '$lib/server/database'
|
|
import {
|
|
updateMediaUsage,
|
|
removeMediaUsage,
|
|
extractMediaIds,
|
|
type MediaUsageReference
|
|
} from '$lib/server/media-usage.js'
|
|
|
|
// Type for project update request body (partial of ProjectCreateBody)
|
|
interface ProjectUpdateBody {
|
|
title?: string
|
|
subtitle?: string
|
|
description?: string
|
|
year?: number
|
|
client?: string
|
|
role?: string
|
|
featuredImage?: string
|
|
logoUrl?: string
|
|
gallery?: Prisma.JsonValue
|
|
externalUrl?: string
|
|
caseStudyContent?: Prisma.JsonValue
|
|
backgroundColor?: string
|
|
highlightColor?: string
|
|
projectType?: string
|
|
displayOrder?: number
|
|
status?: string
|
|
password?: string | null
|
|
slug?: string
|
|
}
|
|
|
|
// GET /api/projects/[id] - Get a single project
|
|
export const GET: RequestHandler = async (event) => {
|
|
const id = parseInt(event.params.id)
|
|
if (isNaN(id)) {
|
|
return errorResponse('Invalid project ID', 400)
|
|
}
|
|
|
|
try {
|
|
const project = await prisma.project.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!project) {
|
|
return errorResponse('Project not found', 404)
|
|
}
|
|
|
|
return jsonResponse(project)
|
|
} catch (error) {
|
|
logger.error('Failed to retrieve project', error as Error)
|
|
return errorResponse('Failed to retrieve project', 500)
|
|
}
|
|
}
|
|
|
|
// PUT /api/projects/[id] - Update a project
|
|
export const PUT: RequestHandler = async (event) => {
|
|
// Check authentication
|
|
if (!checkAdminAuth(event)) {
|
|
return errorResponse('Unauthorized', 401)
|
|
}
|
|
|
|
const id = parseInt(event.params.id)
|
|
if (isNaN(id)) {
|
|
return errorResponse('Invalid project ID', 400)
|
|
}
|
|
|
|
try {
|
|
const body = await parseRequestBody<ProjectUpdateBody>(event.request)
|
|
if (!body) {
|
|
return errorResponse('Invalid request body', 400)
|
|
}
|
|
|
|
// Check if project exists
|
|
const existing = await prisma.project.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!existing) {
|
|
return errorResponse('Project not found', 404)
|
|
}
|
|
|
|
// Handle slug update
|
|
let slug = existing.slug
|
|
if (body.slug && body.slug !== existing.slug) {
|
|
slug = await ensureUniqueSlug(body.slug, 'project', id)
|
|
}
|
|
|
|
// Concurrency control: if updatedAt provided, ensure it matches current
|
|
if (body.updatedAt) {
|
|
const incoming = new Date(body.updatedAt)
|
|
if (existing.updatedAt.getTime() !== incoming.getTime()) {
|
|
return errorResponse('Conflict: project has changed', 409)
|
|
}
|
|
}
|
|
|
|
// Update project
|
|
const project = await prisma.project.update({
|
|
where: { id },
|
|
data: {
|
|
slug,
|
|
title: body.title !== undefined ? body.title : existing.title,
|
|
subtitle: body.subtitle !== undefined ? body.subtitle : existing.subtitle,
|
|
description: body.description !== undefined ? body.description : existing.description,
|
|
year: body.year !== undefined ? body.year : existing.year,
|
|
client: body.client !== undefined ? body.client : existing.client,
|
|
role: body.role !== undefined ? body.role : existing.role,
|
|
featuredImage:
|
|
body.featuredImage !== undefined ? body.featuredImage : existing.featuredImage,
|
|
logoUrl: body.logoUrl !== undefined ? body.logoUrl : existing.logoUrl,
|
|
gallery: body.gallery !== undefined ? body.gallery : existing.gallery,
|
|
externalUrl: body.externalUrl !== undefined ? body.externalUrl : existing.externalUrl,
|
|
caseStudyContent:
|
|
body.caseStudyContent !== undefined ? body.caseStudyContent : existing.caseStudyContent,
|
|
backgroundColor:
|
|
body.backgroundColor !== undefined ? body.backgroundColor : existing.backgroundColor,
|
|
highlightColor:
|
|
body.highlightColor !== undefined ? body.highlightColor : existing.highlightColor,
|
|
projectType: body.projectType !== undefined ? body.projectType : existing.projectType,
|
|
displayOrder: body.displayOrder !== undefined ? body.displayOrder : existing.displayOrder,
|
|
status: body.status !== undefined ? body.status : existing.status,
|
|
password: body.password !== undefined ? body.password : existing.password,
|
|
publishedAt:
|
|
body.status === 'published' && !existing.publishedAt ? new Date() : existing.publishedAt
|
|
}
|
|
})
|
|
|
|
// Update media usage tracking
|
|
try {
|
|
// Remove all existing usage for this project first
|
|
await removeMediaUsage('project', id)
|
|
|
|
// Track all current media usage in the updated project
|
|
const usageReferences: MediaUsageReference[] = []
|
|
|
|
// Track featured image
|
|
const featuredImageIds = extractMediaIds(project, 'featuredImage')
|
|
featuredImageIds.forEach((mediaId) => {
|
|
usageReferences.push({
|
|
mediaId,
|
|
contentType: 'project',
|
|
contentId: id,
|
|
fieldName: 'featuredImage'
|
|
})
|
|
})
|
|
|
|
// Track logo
|
|
const logoIds = extractMediaIds(project, 'logoUrl')
|
|
logoIds.forEach((mediaId) => {
|
|
usageReferences.push({
|
|
mediaId,
|
|
contentType: 'project',
|
|
contentId: id,
|
|
fieldName: 'logoUrl'
|
|
})
|
|
})
|
|
|
|
// Track gallery images
|
|
const galleryIds = extractMediaIds(project, 'gallery')
|
|
galleryIds.forEach((mediaId) => {
|
|
usageReferences.push({
|
|
mediaId,
|
|
contentType: 'project',
|
|
contentId: id,
|
|
fieldName: 'gallery'
|
|
})
|
|
})
|
|
|
|
// Track media in case study content
|
|
const contentIds = extractMediaIds(project, 'caseStudyContent')
|
|
contentIds.forEach((mediaId) => {
|
|
usageReferences.push({
|
|
mediaId,
|
|
contentType: 'project',
|
|
contentId: id,
|
|
fieldName: 'content'
|
|
})
|
|
})
|
|
|
|
if (usageReferences.length > 0) {
|
|
await updateMediaUsage(usageReferences)
|
|
}
|
|
} catch (error) {
|
|
logger.warn('Failed to update media usage tracking for project', { projectId: id, error })
|
|
}
|
|
|
|
logger.info('Project updated', { id: project.id, slug: project.slug })
|
|
|
|
return jsonResponse(project)
|
|
} catch (error) {
|
|
logger.error('Failed to update project', error as Error)
|
|
return errorResponse('Failed to update project', 500)
|
|
}
|
|
}
|
|
|
|
// PATCH /api/projects/[id] - Partially update a project
|
|
export const PATCH: RequestHandler = async (event) => {
|
|
// Check authentication
|
|
if (!checkAdminAuth(event)) {
|
|
return errorResponse('Unauthorized', 401)
|
|
}
|
|
|
|
const id = parseInt(event.params.id)
|
|
if (isNaN(id)) {
|
|
return errorResponse('Invalid project ID', 400)
|
|
}
|
|
|
|
try {
|
|
const body = await parseRequestBody<ProjectUpdateBody>(event.request)
|
|
if (!body) {
|
|
return errorResponse('Invalid request body', 400)
|
|
}
|
|
|
|
// Check if project exists
|
|
const existing = await prisma.project.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!existing) {
|
|
return errorResponse('Project not found', 404)
|
|
}
|
|
|
|
// Concurrency control: if updatedAt provided, ensure it matches current
|
|
if (body.updatedAt) {
|
|
const incoming = new Date(body.updatedAt)
|
|
if (existing.updatedAt.getTime() !== incoming.getTime()) {
|
|
return errorResponse('Conflict: project has changed', 409)
|
|
}
|
|
}
|
|
|
|
// Build update data object with only provided fields
|
|
const updateData: Prisma.ProjectUpdateInput = {}
|
|
|
|
// Handle status update specially
|
|
if (body.status !== undefined) {
|
|
updateData.status = body.status
|
|
// Set publishedAt if changing to published for the first time
|
|
if (body.status === 'published' && !existing.publishedAt) {
|
|
updateData.publishedAt = new Date()
|
|
}
|
|
// Clear publishedAt if changing to draft
|
|
else if (body.status === 'draft') {
|
|
updateData.publishedAt = null
|
|
}
|
|
}
|
|
|
|
// Add other fields if provided
|
|
if (body.title !== undefined) updateData.title = body.title
|
|
if (body.subtitle !== undefined) updateData.subtitle = body.subtitle
|
|
if (body.description !== undefined) updateData.description = body.description
|
|
if (body.year !== undefined) updateData.year = body.year
|
|
if (body.client !== undefined) updateData.client = body.client
|
|
if (body.role !== undefined) updateData.role = body.role
|
|
if (body.featuredImage !== undefined) updateData.featuredImage = body.featuredImage
|
|
if (body.logoUrl !== undefined) updateData.logoUrl = body.logoUrl
|
|
if (body.gallery !== undefined) updateData.gallery = body.gallery
|
|
if (body.externalUrl !== undefined) updateData.externalUrl = body.externalUrl
|
|
if (body.caseStudyContent !== undefined) updateData.caseStudyContent = body.caseStudyContent
|
|
if (body.backgroundColor !== undefined) updateData.backgroundColor = body.backgroundColor
|
|
if (body.highlightColor !== undefined) updateData.highlightColor = body.highlightColor
|
|
if (body.projectType !== undefined) updateData.projectType = body.projectType
|
|
if (body.displayOrder !== undefined) updateData.displayOrder = body.displayOrder
|
|
if (body.password !== undefined) updateData.password = body.password
|
|
|
|
// Handle slug update if provided
|
|
if (body.slug && body.slug !== existing.slug) {
|
|
updateData.slug = await ensureUniqueSlug(body.slug, 'project', id)
|
|
}
|
|
|
|
// Update project
|
|
const project = await prisma.project.update({
|
|
where: { id },
|
|
data: updateData
|
|
})
|
|
|
|
logger.info('Project partially updated', { id: project.id, fields: Object.keys(updateData) })
|
|
|
|
return jsonResponse(project)
|
|
} catch (error) {
|
|
logger.error('Failed to update project', error as Error)
|
|
return errorResponse('Failed to update project', 500)
|
|
}
|
|
}
|
|
|
|
// DELETE /api/projects/[id] - Delete a project
|
|
export const DELETE: RequestHandler = async (event) => {
|
|
// Check authentication
|
|
if (!checkAdminAuth(event)) {
|
|
return errorResponse('Unauthorized', 401)
|
|
}
|
|
|
|
const id = parseInt(event.params.id)
|
|
if (isNaN(id)) {
|
|
return errorResponse('Invalid project ID', 400)
|
|
}
|
|
|
|
try {
|
|
// Remove media usage tracking first
|
|
await removeMediaUsage('project', id)
|
|
|
|
// Delete the project
|
|
await prisma.project.delete({
|
|
where: { id }
|
|
})
|
|
|
|
logger.info('Project deleted', { id })
|
|
|
|
return new Response(null, { status: 204 })
|
|
} catch (error) {
|
|
logger.error('Failed to delete project', error as Error)
|
|
return errorResponse('Failed to delete project', 500)
|
|
}
|
|
}
|