diff --git a/src/integrations/orpc/dto/resume.ts b/src/integrations/orpc/dto/resume.ts index 32ea4b55d..4f8bedbca 100644 --- a/src/integrations/orpc/dto/resume.ts +++ b/src/integrations/orpc/dto/resume.ts @@ -2,6 +2,7 @@ import { createSelectSchema } from "drizzle-zod"; import z from "zod"; import { schema } from "@/integrations/drizzle"; import { resumeDataSchema } from "@/schema/resume/data"; +import { jsonPatchOperationSchema } from "@/utils/resume/patch"; const resumeSchema = createSelectSchema(schema.resume, { id: z.string().describe("The ID of the resume."), @@ -81,14 +82,7 @@ export const resumeDto = { 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(), - }), - ) + .array(jsonPatchOperationSchema) .min(1) .describe("An array of JSON Patch (RFC 6902) operations to apply to the resume data."), }), diff --git a/src/integrations/orpc/router/resume.ts b/src/integrations/orpc/router/resume.ts index 1ae22c41e..fe7488d52 100644 --- a/src/integrations/orpc/router/resume.ts +++ b/src/integrations/orpc/router/resume.ts @@ -1,4 +1,3 @@ -import type { Operation } from "fast-json-patch"; import z from "zod"; import { sampleResumeData } from "@/schema/resume/sample"; import { generateRandomName, slugify } from "@/utils/string"; @@ -215,7 +214,7 @@ export const resumeRouter = { return await resumeService.patch({ id: input.id, userId: context.user.id, - operations: input.operations as Operation[], + operations: input.operations, }); }), diff --git a/src/integrations/orpc/services/ai.ts b/src/integrations/orpc/services/ai.ts index 508f9bcfd..732bcfbc8 100644 --- a/src/integrations/orpc/services/ai.ts +++ b/src/integrations/orpc/services/ai.ts @@ -15,9 +15,9 @@ import { defaultResumeData, resumeDataSchema } from "@/schema/resume/data"; export const aiProviderSchema = z.enum(["ollama", "openai", "gemini", "anthropic", "vercel-ai-gateway"]); -export type AIProvider = z.infer; +type AIProvider = z.infer; -export type GetModelInput = { +type GetModelInput = { provider: AIProvider; model: string; apiKey: string; @@ -49,9 +49,9 @@ export const fileInputSchema = z.object({ data: z.string(), // base64 encoded }); -export type TestConnectionInput = z.infer; +type TestConnectionInput = z.infer; -export async function testConnection(input: TestConnectionInput): Promise { +async function testConnection(input: TestConnectionInput): Promise { const RESPONSE_OK = "1"; const result = await generateText({ @@ -63,11 +63,11 @@ export async function testConnection(input: TestConnectionInput): Promise & { +type ParsePdfInput = z.infer & { file: z.infer; }; -export async function parsePdf(input: ParsePdfInput): Promise { +async function parsePdf(input: ParsePdfInput): Promise { const model = getModel(input); const result = await generateText({ @@ -101,12 +101,12 @@ export async function parsePdf(input: ParsePdfInput): Promise { }); } -export type ParseDocxInput = z.infer & { +type ParseDocxInput = z.infer & { file: z.infer; mediaType: "application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; }; -export async function parseDocx(input: ParseDocxInput): Promise { +async function parseDocx(input: ParseDocxInput): Promise { const model = getModel(input); const result = await generateText({ diff --git a/src/integrations/orpc/services/storage.ts b/src/integrations/orpc/services/storage.ts index 81da60266..a942b444c 100644 --- a/src/integrations/orpc/services/storage.ts +++ b/src/integrations/orpc/services/storage.ts @@ -82,7 +82,7 @@ export function isImageFile(mimeType: string): boolean { return IMAGE_MIME_TYPES.includes(mimeType); } -export interface ProcessedImage { +interface ProcessedImage { data: Uint8Array; contentType: string; } @@ -316,7 +316,7 @@ export function getStorageService(): StorageService { // High-level upload types type UploadType = "picture" | "screenshot" | "pdf"; -export interface UploadFileInput { +interface UploadFileInput { userId: string; data: Uint8Array; contentType: string; @@ -324,7 +324,7 @@ export interface UploadFileInput { resumeId?: string; } -export interface UploadFileResult { +interface UploadFileResult { url: string; key: string; } diff --git a/src/utils/resume/move-item.ts b/src/utils/resume/move-item.ts index c90c4f743..94f904bb0 100644 --- a/src/utils/resume/move-item.ts +++ b/src/utils/resume/move-item.ts @@ -8,7 +8,7 @@ import { getSectionTitle as getDefaultSectionTitle } from "./section"; // ============================================================================ /** Target section that an item can be moved to */ -export type MoveTargetSection = { +type MoveTargetSection = { sectionId: string; sectionTitle: string; /** Whether this is a standard section (true) or custom section (false) */ @@ -16,7 +16,7 @@ export type MoveTargetSection = { }; /** Page with its compatible sections for the move menu */ -export type MoveTargetPage = { +type MoveTargetPage = { pageIndex: number; sections: MoveTargetSection[]; }; diff --git a/src/utils/resume/patch.ts b/src/utils/resume/patch.ts index ffcc5d299..de6d367b8 100644 --- a/src/utils/resume/patch.ts +++ b/src/utils/resume/patch.ts @@ -1,8 +1,21 @@ import type { JsonPatchError, Operation } from "fast-json-patch"; import jsonpatch from "fast-json-patch"; +import z from "zod"; import { type ResumeData, resumeDataSchema } from "@/schema/resume/data"; -export type { Operation }; +/** + * A Zod schema that models JSON Patch (RFC 6902) operations as a discriminated union on `op`. + * This ensures required fields (`value` for add/replace/test, `from` for move/copy) are + * validated at the request boundary rather than failing later at the `fast-json-patch` layer. + */ +export const jsonPatchOperationSchema = z.discriminatedUnion("op", [ + z.object({ op: z.literal("add"), path: z.string(), value: z.any() }), + z.object({ op: z.literal("remove"), path: z.string() }), + z.object({ op: z.literal("replace"), path: z.string(), value: z.any() }), + z.object({ op: z.literal("move"), path: z.string(), from: z.string() }), + z.object({ op: z.literal("copy"), path: z.string(), from: z.string() }), + z.object({ op: z.literal("test"), path: z.string(), value: z.any() }), +]); /** * A structured error thrown when a JSON Patch operation fails.