From f5a440a2caa09d5b3cad50998a9f3f9fc404ab12 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 31 Aug 2025 11:03:27 -0700 Subject: [PATCH] feat(api/server): add posts PATCH and optimistic concurrency (updatedAt) for posts and projects --- src/routes/api/posts/[id]/+server.ts | 64 +++++++++++++++++++++++++ src/routes/api/projects/[id]/+server.ts | 16 +++++++ 2 files changed, 80 insertions(+) diff --git a/src/routes/api/posts/[id]/+server.ts b/src/routes/api/posts/[id]/+server.ts index f1edce5..2095119 100644 --- a/src/routes/api/posts/[id]/+server.ts +++ b/src/routes/api/posts/[id]/+server.ts @@ -52,6 +52,16 @@ export const PUT: RequestHandler = async (event) => { const data = await event.request.json() + // Concurrency control: require matching updatedAt if provided + if (data.updatedAt) { + const existing = await prisma.post.findUnique({ where: { id }, select: { updatedAt: true } }) + if (!existing) return errorResponse('Post not found', 404) + const incoming = new Date(data.updatedAt) + if (existing.updatedAt.getTime() !== incoming.getTime()) { + return errorResponse('Conflict: post has changed', 409) + } + } + // Update publishedAt if status is changing to published if (data.status === 'published') { const currentPost = await prisma.post.findUnique({ @@ -141,6 +151,60 @@ export const PUT: RequestHandler = async (event) => { } } +// PATCH /api/posts/[id] - Partially update a post +export const PATCH: RequestHandler = async (event) => { + if (!checkAdminAuth(event)) { + return errorResponse('Unauthorized', 401) + } + + try { + const id = parseInt(event.params.id) + if (isNaN(id)) { + return errorResponse('Invalid post ID', 400) + } + + const data = await event.request.json() + + // Check for existence and concurrency + const existing = await prisma.post.findUnique({ where: { id } }) + if (!existing) return errorResponse('Post not found', 404) + if (data.updatedAt) { + const incoming = new Date(data.updatedAt) + if (existing.updatedAt.getTime() !== incoming.getTime()) { + return errorResponse('Conflict: post has changed', 409) + } + } + + const updateData: any = {} + + if (data.status !== undefined) { + updateData.status = data.status + if (data.status === 'published' && !existing.publishedAt) { + updateData.publishedAt = new Date() + } else if (data.status === 'draft') { + updateData.publishedAt = null + } + } + if (data.title !== undefined) updateData.title = data.title + if (data.slug !== undefined) updateData.slug = data.slug + if (data.type !== undefined) updateData.postType = data.type + if (data.content !== undefined) updateData.content = data.content + if (data.featuredImage !== undefined) updateData.featuredImage = data.featuredImage + if (data.attachedPhotos !== undefined) + updateData.attachments = data.attachedPhotos && data.attachedPhotos.length > 0 ? data.attachedPhotos : null + if (data.tags !== undefined) updateData.tags = data.tags + if (data.publishedAt !== undefined) updateData.publishedAt = data.publishedAt + + const post = await prisma.post.update({ where: { id }, data: updateData }) + + logger.info('Post partially updated', { id: post.id, fields: Object.keys(updateData) }) + return jsonResponse(post) + } catch (error) { + logger.error('Failed to partially update post', error as Error) + return errorResponse('Failed to update post', 500) + } +} + // DELETE /api/posts/[id] - Delete a post export const DELETE: RequestHandler = async (event) => { if (!checkAdminAuth(event)) { diff --git a/src/routes/api/projects/[id]/+server.ts b/src/routes/api/projects/[id]/+server.ts index 68908a0..b136589 100644 --- a/src/routes/api/projects/[id]/+server.ts +++ b/src/routes/api/projects/[id]/+server.ts @@ -71,6 +71,14 @@ export const PUT: RequestHandler = async (event) => { 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 }, @@ -197,6 +205,14 @@ export const PATCH: RequestHandler = async (event) => { 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: any = {}