feat(api/server): add posts PATCH and optimistic concurrency (updatedAt) for posts and projects

This commit is contained in:
Justin Edmund 2025-08-31 11:03:27 -07:00
parent 3aec443534
commit f5a440a2ca
2 changed files with 80 additions and 0 deletions

View file

@ -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)) {

View file

@ -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 = {}