Return updated resume object from PUT /resume/{id} instead of void (#2688)

* implement PUT response

* Refactor resume router and service to use DTOs for input and output schemas

* define explicitly, the fields to be returned

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Arjun Earthperson
2026-02-06 18:49:33 -05:00
committed by GitHub
parent ddbb71fb78
commit 5e8e1349bc
5 changed files with 212 additions and 140 deletions
+89
View File
@@ -0,0 +1,89 @@
import { createSelectSchema } from "drizzle-zod";
import z from "zod";
import { schema } from "@/integrations/drizzle";
import { resumeDataSchema } from "@/schema/resume/data";
const resumeSchema = createSelectSchema(schema.resume, {
id: z.string().describe("The ID of the resume."),
name: z.string().min(1).max(64).describe("The name of the resume."),
slug: z.string().min(1).max(64).describe("The slug of the resume."),
tags: z.array(z.string()).describe("The tags of the resume."),
isPublic: z.boolean().describe("Whether the resume is public."),
isLocked: z.boolean().describe("Whether the resume is locked."),
password: z.string().min(6).max(64).nullable().describe("The password of the resume, if any."),
data: resumeDataSchema,
userId: z.string().describe("The ID of the user who owns the resume."),
createdAt: z.date().describe("The date and time the resume was created."),
updatedAt: z.date().describe("The date and time the resume was last updated."),
});
export const resumeDto = {
list: {
input: z
.object({
tags: z.array(z.string()).optional().default([]),
sort: z.enum(["lastUpdatedAt", "createdAt", "name"]).optional().default("lastUpdatedAt"),
})
.optional()
.default({ tags: [], sort: "lastUpdatedAt" }),
output: z.array(resumeSchema.omit({ data: true, password: true, userId: true })),
},
getById: {
input: resumeSchema.pick({ id: true }),
output: resumeSchema
.omit({ password: true, userId: true, createdAt: true, updatedAt: true })
.extend({ hasPassword: z.boolean() }),
},
getBySlug: {
input: z.object({ username: z.string(), slug: z.string() }),
output: resumeSchema.omit({ password: true, userId: true, createdAt: true, updatedAt: true }),
},
create: {
input: resumeSchema
.pick({ name: true, slug: true, tags: true })
.extend({ withSampleData: z.boolean().default(false) }),
output: z.string().describe("The ID of the created resume."),
},
import: {
input: resumeSchema.pick({ data: true }),
output: z.string().describe("The ID of the imported resume."),
},
update: {
input: resumeSchema
.pick({ name: true, slug: true, tags: true, data: true, isPublic: true })
.partial()
.extend({ id: z.string() }),
output: resumeSchema.omit({ password: true, userId: true, createdAt: true, updatedAt: true }),
},
setLocked: {
input: resumeSchema.pick({ id: true, isLocked: true }),
output: z.void(),
},
setPassword: {
input: resumeSchema.pick({ id: true }).extend({ password: z.string().min(6).max(64) }),
output: z.void(),
},
removePassword: {
input: resumeSchema.pick({ id: true }),
output: z.void(),
},
duplicate: {
input: resumeSchema.pick({ id: true, name: true, slug: true, tags: true }),
output: z.string().describe("The ID of the duplicated resume."),
},
delete: {
input: resumeSchema.pick({ id: true }),
output: z.void(),
},
};
+24 -89
View File
@@ -1,8 +1,8 @@
import z from "zod";
import { resumeDataSchema } from "@/schema/resume/data";
import { sampleResumeData } from "@/schema/resume/sample";
import { generateRandomName, slugify } from "@/utils/string";
import { protectedProcedure, publicProcedure, serverOnlyProcedure } from "../context";
import { resumeDto } from "../dto/resume";
import { resumeService } from "../services/resume";
const tagsRouter = {
@@ -63,29 +63,8 @@ export const resumeRouter = {
summary: "List all resumes",
description: "List of all the resumes for the authenticated user.",
})
.input(
z
.object({
tags: z.array(z.string()).optional().default([]),
sort: z.enum(["lastUpdatedAt", "createdAt", "name"]).optional().default("lastUpdatedAt"),
})
.optional()
.default({ tags: [], sort: "lastUpdatedAt" }),
)
.output(
z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
isPublic: z.boolean(),
isLocked: z.boolean(),
createdAt: z.date(),
updatedAt: z.date(),
}),
),
)
.input(resumeDto.list.input.optional().default({ tags: [], sort: "lastUpdatedAt" }))
.output(resumeDto.list.output)
.handler(async ({ input, context }) => {
return await resumeService.list({
userId: context.user.id,
@@ -102,26 +81,15 @@ export const resumeRouter = {
summary: "Get resume by ID",
description: "Get a resume, along with its data, by its ID.",
})
.input(z.object({ id: z.string() }))
.output(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
data: resumeDataSchema,
isPublic: z.boolean(),
isLocked: z.boolean(),
hasPassword: z.boolean(),
}),
)
.input(resumeDto.getById.input)
.output(resumeDto.getById.output)
.handler(async ({ context, input }) => {
return await resumeService.getById({ id: input.id, userId: context.user.id });
}),
getByIdForPrinter: serverOnlyProcedure
.route({ tags: ["Internal"], summary: "Get resume by ID for printer" })
.input(z.object({ id: z.string() }))
.input(resumeDto.getById.input)
.handler(async ({ input }) => {
return await resumeService.getByIdForPrinter({ id: input.id });
}),
@@ -134,18 +102,8 @@ export const resumeRouter = {
summary: "Get resume by username and slug",
description: "Get a resume, along with its data, by its username and slug.",
})
.input(z.object({ username: z.string(), slug: z.string() }))
.output(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
data: resumeDataSchema,
isPublic: z.boolean(),
isLocked: z.boolean(),
}),
)
.input(resumeDto.getBySlug.input)
.output(resumeDto.getBySlug.output)
.handler(async ({ input, context }) => {
return await resumeService.getBySlug({ ...input, currentUserId: context.user?.id });
}),
@@ -158,15 +116,8 @@ export const resumeRouter = {
summary: "Create a new resume",
description: "Create a new resume, with the ability to initialize it with sample data.",
})
.input(
z.object({
name: z.string().min(1).max(64),
slug: z.string().min(1).max(64),
tags: z.array(z.string()),
withSampleData: z.boolean().default(false),
}),
)
.output(z.string().describe("The ID of the created resume."))
.input(resumeDto.create.input)
.output(resumeDto.create.output)
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
@@ -192,8 +143,8 @@ export const resumeRouter = {
summary: "Import a resume",
description: "Import a resume from a file.",
})
.input(z.object({ data: resumeDataSchema }))
.output(z.string().describe("The ID of the imported resume."))
.input(resumeDto.import.input)
.output(resumeDto.import.output)
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
@@ -222,17 +173,8 @@ export const resumeRouter = {
summary: "Update a resume",
description: "Update a resume, along with its data, by its ID.",
})
.input(
z.object({
id: z.string(),
name: z.string().optional(),
slug: z.string().optional(),
tags: z.array(z.string()).optional(),
data: resumeDataSchema.optional(),
isPublic: z.boolean().optional(),
}),
)
.output(z.void())
.input(resumeDto.update.input)
.output(resumeDto.update.output)
.errors({
RESUME_SLUG_ALREADY_EXISTS: {
message: "A resume with this slug already exists.",
@@ -259,8 +201,8 @@ export const resumeRouter = {
summary: "Set resume locked status",
description: "Toggle the locked status of a resume, by its ID.",
})
.input(z.object({ id: z.string(), isLocked: z.boolean() }))
.output(z.void())
.input(resumeDto.setLocked.input)
.output(resumeDto.setLocked.output)
.handler(async ({ context, input }) => {
return await resumeService.setLocked({
id: input.id,
@@ -277,8 +219,8 @@ export const resumeRouter = {
summary: "Set password on a resume",
description: "Set a password on a resume to protect it from unauthorized access when shared publicly.",
})
.input(z.object({ id: z.string(), password: z.string().min(6).max(64) }))
.output(z.void())
.input(resumeDto.setPassword.input)
.output(resumeDto.setPassword.output)
.handler(async ({ context, input }) => {
return await resumeService.setPassword({
id: input.id,
@@ -295,8 +237,8 @@ export const resumeRouter = {
summary: "Remove password from a resume",
description: "Remove password protection from a resume.",
})
.input(z.object({ id: z.string() }))
.output(z.void())
.input(resumeDto.removePassword.input)
.output(resumeDto.removePassword.output)
.handler(async ({ context, input }) => {
return await resumeService.removePassword({
id: input.id,
@@ -312,15 +254,8 @@ export const resumeRouter = {
summary: "Duplicate a resume",
description: "Duplicate a resume, by its ID.",
})
.input(
z.object({
id: z.string(),
name: z.string().optional(),
slug: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
)
.output(z.string().describe("The ID of the duplicated resume."))
.input(resumeDto.duplicate.input)
.output(resumeDto.duplicate.output)
.handler(async ({ context, input }) => {
const original = await resumeService.getById({ id: input.id, userId: context.user.id });
@@ -342,8 +277,8 @@ export const resumeRouter = {
summary: "Delete a resume",
description: "Delete a resume, by its ID.",
})
.input(z.object({ id: z.string() }))
.output(z.void())
.input(resumeDto.delete.input)
.output(resumeDto.delete.output)
.handler(async ({ context, input }) => {
return await resumeService.delete({ id: input.id, userId: context.user.id });
}),
+22 -10
View File
@@ -14,7 +14,7 @@ import { hasResumeAccess } from "../helpers/resume-access";
import { getStorageService } from "./storage";
const tags = {
list: async (input: { userId: string }): Promise<string[]> => {
list: async (input: { userId: string }) => {
const result = await db
.select({ tags: schema.resume.tags })
.from(schema.resume)
@@ -50,7 +50,7 @@ const statistics = {
};
},
increment: async (input: { id: string; views?: boolean; downloads?: boolean }): Promise<void> => {
increment: async (input: { id: string; views?: boolean; downloads?: boolean }) => {
const views = input.views ? 1 : 0;
const downloads = input.downloads ? 1 : 0;
const lastViewedAt = input.views ? sql`now()` : undefined;
@@ -233,7 +233,7 @@ export const resumeService = {
tags: string[];
locale: Locale;
data?: ResumeData;
}): Promise<string> => {
}) => {
const id = generateId();
input.data = input.data ?? defaultResumeData;
@@ -269,7 +269,7 @@ export const resumeService = {
tags?: string[];
data?: ResumeData;
isPublic?: boolean;
}): Promise<void> => {
}) => {
const [resume] = await db
.select({ isLocked: schema.resume.isLocked })
.from(schema.resume)
@@ -286,7 +286,7 @@ export const resumeService = {
};
try {
await db
const [resume] = await db
.update(schema.resume)
.set(updateData)
.where(
@@ -295,7 +295,19 @@ export const resumeService = {
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;
} catch (error) {
if (get(error, "cause.constraint") === "resume_slug_user_id_unique") {
throw new ORPCError("RESUME_SLUG_ALREADY_EXISTS", { status: 400 });
@@ -305,14 +317,14 @@ export const resumeService = {
}
},
setLocked: async (input: { id: string; userId: string; isLocked: boolean }): Promise<void> => {
setLocked: async (input: { id: string; userId: string; isLocked: boolean }) => {
await db
.update(schema.resume)
.set({ isLocked: input.isLocked })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
setPassword: async (input: { id: string; userId: string; password: string }): Promise<void> => {
setPassword: async (input: { id: string; userId: string; password: string }) => {
const hashedPassword = await hashPassword(input.password);
await db
@@ -321,14 +333,14 @@ export const resumeService = {
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
removePassword: async (input: { id: string; userId: string }): Promise<void> => {
removePassword: async (input: { id: string; userId: string }) => {
await db
.update(schema.resume)
.set({ password: null })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
delete: async (input: { id: string; userId: string }): Promise<void> => {
delete: async (input: { id: string; userId: string }) => {
const storageService = getStorageService();
const deleteResumePromise = db