Feature: Implement Atomic Resume Patching API (#2692)

This commit is contained in:
Amruth Pillai
2026-02-08 00:16:11 +01:00
committed by GitHub
parent 5e8e1349bc
commit cc01fb9418
8 changed files with 594 additions and 205 deletions
+20
View File
@@ -77,6 +77,26 @@ export const resumeDto = {
output: z.void(),
},
patch: {
input: z.object({
id: z.string().describe("The ID of the resume to patch."),
operations: z
.array(
z.object({
op: z.enum(["add", "remove", "replace", "move", "copy", "test"]),
path: z.string(),
value: z.any().optional(),
from: z.string().optional(),
}),
)
.min(1)
.describe("An array of JSON Patch (RFC 6902) operations to apply to the resume data."),
}),
output: resumeSchema
.omit({ password: true, userId: true, createdAt: true, updatedAt: true })
.extend({ hasPassword: z.boolean() }),
},
duplicate: {
input: resumeSchema.pick({ id: true, name: true, slug: true, tags: true }),
output: z.string().describe("The ID of the duplicated resume."),
+26
View File
@@ -1,3 +1,4 @@
import type { Operation } from "fast-json-patch";
import z from "zod";
import { sampleResumeData } from "@/schema/resume/sample";
import { generateRandomName, slugify } from "@/utils/string";
@@ -193,6 +194,31 @@ export const resumeRouter = {
});
}),
patch: protectedProcedure
.route({
method: "PATCH",
path: "/resume/{id}",
tags: ["Resume"],
summary: "Patch a resume",
description:
"Apply JSON Patch (RFC 6902) operations to partially update a resume's data. This allows you to make small, targeted changes without sending the entire resume object.",
})
.input(resumeDto.patch.input)
.output(resumeDto.patch.output)
.errors({
INVALID_PATCH_OPERATIONS: {
message: "The patch operations are invalid or produced an invalid resume.",
status: 400,
},
})
.handler(async ({ context, input }) => {
return await resumeService.patch({
id: input.id,
userId: context.user.id,
operations: input.operations as Operation[],
});
}),
setLocked: protectedProcedure
.route({
method: "POST",
+50
View File
@@ -1,6 +1,7 @@
import { ORPCError } from "@orpc/client";
import { and, arrayContains, asc, desc, eq, sql } from "drizzle-orm";
import { get } from "es-toolkit/compat";
import type { Operation } from "fast-json-patch";
import { match } from "ts-pattern";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
@@ -9,6 +10,7 @@ import { defaultResumeData } from "@/schema/resume/data";
import { env } from "@/utils/env";
import type { Locale } from "@/utils/locale";
import { hashPassword } from "@/utils/password";
import { applyResumePatches, ResumePatchError } from "@/utils/resume/patch";
import { generateId } from "@/utils/string";
import { hasResumeAccess } from "../helpers/resume-access";
import { getStorageService } from "./storage";
@@ -317,6 +319,54 @@ export const resumeService = {
}
},
patch: async (input: { id: string; userId: string; operations: Operation[] }) => {
const [existing] = await db
.select({ data: schema.resume.data, isLocked: schema.resume.isLocked })
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
if (!existing) throw new ORPCError("NOT_FOUND");
if (existing.isLocked) throw new ORPCError("RESUME_LOCKED");
let patchedData: ResumeData;
try {
patchedData = applyResumePatches(existing.data, input.operations);
} catch (error) {
if (error instanceof ResumePatchError) {
throw new ORPCError("INVALID_PATCH_OPERATIONS", {
status: 400,
message: error.message,
data: { code: error.code, index: error.index, operation: error.operation },
});
}
throw new ORPCError("INVALID_PATCH_OPERATIONS", {
status: 400,
message: error instanceof Error ? error.message : "Failed to apply patch operations",
});
}
const [resume] = await db
.update(schema.resume)
.set({ data: patchedData })
.where(
and(eq(schema.resume.id, input.id), eq(schema.resume.isLocked, false), eq(schema.resume.userId, input.userId)),
)
.returning({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
});
return resume;
},
setLocked: async (input: { id: string; userId: string; isLocked: boolean }) => {
await db
.update(schema.resume)