From 5e8e1349bcbdc557ddf1b2f6e74f6cf92a99dfce Mon Sep 17 00:00:00 2001 From: Arjun Earthperson Date: Fri, 6 Feb 2026 18:49:33 -0500 Subject: [PATCH] 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 --- package.json | 9 +- pnpm-lock.yaml | 109 ++++++++++++++-------- src/integrations/orpc/dto/resume.ts | 89 ++++++++++++++++++ src/integrations/orpc/router/resume.ts | 113 +++++------------------ src/integrations/orpc/services/resume.ts | 32 +++++-- 5 files changed, 212 insertions(+), 140 deletions(-) create mode 100644 src/integrations/orpc/dto/resume.ts diff --git a/package.json b/package.json index 69dfb2706..e1d93750f 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.37", - "@ai-sdk/google": "^3.0.21", - "@ai-sdk/openai": "^3.0.25", + "@ai-sdk/anthropic": "^3.0.38", + "@ai-sdk/google": "^3.0.22", + "@ai-sdk/openai": "^3.0.26", "@aws-sdk/client-s3": "^3.985.0", "@better-auth/core": "1.5.0-beta.11", "@better-auth/passkey": "1.5.0-beta.11", @@ -66,7 +66,7 @@ "@tiptap/pm": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/starter-kit": "^3.19.0", - "ai": "^6.0.74", + "ai": "^6.0.75", "ai-sdk-ollama": "^3.4.0", "bcrypt": "^6.0.0", "better-auth": "1.5.0-beta.11", @@ -75,6 +75,7 @@ "cmdk": "^1.1.1", "dompurify": "^3.3.1", "drizzle-orm": "^1.0.0-beta.15-859cf75", + "drizzle-zod": "^0.8.3", "es-toolkit": "^1.44.0", "fast-deep-equal": "^3.1.3", "fuse.js": "^7.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9565f2a2f..c256a580d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,14 +12,14 @@ importers: .: dependencies: '@ai-sdk/anthropic': - specifier: ^3.0.37 - version: 3.0.37(zod@4.3.6) + specifier: ^3.0.38 + version: 3.0.38(zod@4.3.6) '@ai-sdk/google': - specifier: ^3.0.21 - version: 3.0.21(zod@4.3.6) + specifier: ^3.0.22 + version: 3.0.22(zod@4.3.6) '@ai-sdk/openai': - specifier: ^3.0.25 - version: 3.0.25(zod@4.3.6) + specifier: ^3.0.26 + version: 3.0.26(zod@4.3.6) '@aws-sdk/client-s3': specifier: ^3.985.0 version: 3.985.0 @@ -117,11 +117,11 @@ importers: specifier: ^3.19.0 version: 3.19.0 ai: - specifier: ^6.0.74 - version: 6.0.74(zod@4.3.6) + specifier: ^6.0.75 + version: 6.0.75(zod@4.3.6) ai-sdk-ollama: specifier: ^3.4.0 - version: 3.4.0(ai@6.0.74(zod@4.3.6))(zod@4.3.6) + version: 3.4.0(ai@6.0.75(zod@4.3.6))(zod@4.3.6) bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -143,6 +143,9 @@ importers: drizzle-orm: specifier: ^1.0.0-beta.15-859cf75 version: 1.0.0-beta.9-e89174b(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/mssql@9.1.9)(@types/pg@8.16.0)(mssql@11.0.1)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) + drizzle-zod: + specifier: ^0.8.3 + version: 0.8.3(drizzle-orm@1.0.0-beta.9-e89174b(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/mssql@9.1.9)(@types/pg@8.16.0)(mssql@11.0.1)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(zod@4.3.6) es-toolkit: specifier: ^1.44.0 version: 1.44.0 @@ -327,26 +330,26 @@ importers: packages: - '@ai-sdk/anthropic@3.0.37': - resolution: {integrity: sha512-tEgcJPw+a6obbF+SHrEiZsx3DNxOHqeY8bK4IpiNsZ8YPZD141R34g3lEAaQnmNN5mGsEJ8SXoEDabuzi8wFJQ==} + '@ai-sdk/anthropic@3.0.38': + resolution: {integrity: sha512-9MchyPRPni0WzrFeIGNevZpQVfWxaS+MQFupIXYQo9VgHnuO1Vyrp9SBmjkkuoAdBs7GomsWqLZCcNMJAVbdFA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.36': - resolution: {integrity: sha512-2r1Q6azvqMYxQ1hqfWZmWg4+8MajoldD/ty65XdhCaCoBfvDu7trcvxXDfTSU+3/wZ1JIDky46SWYFOHnTbsBw==} + '@ai-sdk/gateway@3.0.37': + resolution: {integrity: sha512-w7rxPd2WOuSr4he2ALvtCAM/W1/33rGdv3kkU52tCGj/bJSYJnJDpnTvoWRLNESp9HEkkiNFfBLFZY0PB9n4aQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/google@3.0.21': - resolution: {integrity: sha512-qQuvcbDqDPZojtoT45UFCQVH2w3m6KJKKjqJduUsvhN5ZqOXste0h4HgHK8hwGuDfv96Jr9QQEpspbgp6iu5Uw==} + '@ai-sdk/google@3.0.22': + resolution: {integrity: sha512-g1N5P/jfTiH4qwdv4WT3hkKzzAbITFz457NomtBfjP8Q3SCzdbU9oPK5ACBMG8RN5mc2QPL6DLtM3Hf5T8KPmw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@3.0.25': - resolution: {integrity: sha512-DsaN46R98+D1W3lU3fKuPU3ofacboLaHlkAwxJPgJ8eup1AJHmPK1N1y10eJJbJcF6iby8Tf/vanoZxc9JPUfw==} + '@ai-sdk/openai@3.0.26': + resolution: {integrity: sha512-W/hiwxIfG29IO0Fob1HwWpFssMsNrxWoX8A7DwNGOtKArDBmJNuGzQeU/k0Fnh8WyvZEnfxkjO4oXkSXfVBayg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -357,10 +360,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.14': + resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@3.0.7': resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@antfu/ni@25.0.0': resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} hasBin: true @@ -4323,8 +4336,8 @@ packages: peerDependencies: ai: ^6.0.70 - ai@6.0.74: - resolution: {integrity: sha512-psGKcE4J0WEXZEvZrc6yc4BzGZW4sfMrbXZgYxyL6Mj8eES5oCaoSGEQfYeNdZvj10EEEXr9rsnUAWZdHiQ/DQ==} + ai@6.0.75: + resolution: {integrity: sha512-pT1xtwaE5Un261GO9Mt9DZ2rv26v+gQZNRXo433CNAMo0JDYs2ef+5MvcNkBe1jXCY9XrXxUG/VT6Y6/9S2SWg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -5142,6 +5155,12 @@ packages: sqlite3: optional: true + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -8190,29 +8209,29 @@ packages: snapshots: - '@ai-sdk/anthropic@3.0.37(zod@4.3.6)': + '@ai-sdk/anthropic@3.0.38(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@4.3.6) zod: 4.3.6 - '@ai-sdk/gateway@3.0.36(zod@4.3.6)': + '@ai-sdk/gateway@3.0.37(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@4.3.6) '@vercel/oidc': 3.1.0 zod: 4.3.6 - '@ai-sdk/google@3.0.21(zod@4.3.6)': + '@ai-sdk/google@3.0.22(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@4.3.6) zod: 4.3.6 - '@ai-sdk/openai@3.0.25(zod@4.3.6)': + '@ai-sdk/openai@3.0.26(zod@4.3.6)': dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@4.3.6) zod: 4.3.6 '@ai-sdk/provider-utils@4.0.13(zod@4.3.6)': @@ -8222,10 +8241,21 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 + '@ai-sdk/provider-utils@4.0.14(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + '@ai-sdk/provider@3.0.7': dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@antfu/ni@25.0.0': dependencies: ansis: 4.2.0 @@ -12839,20 +12869,20 @@ snapshots: agent-base@7.1.4: {} - ai-sdk-ollama@3.4.0(ai@6.0.74(zod@4.3.6))(zod@4.3.6): + ai-sdk-ollama@3.4.0(ai@6.0.75(zod@4.3.6))(zod@4.3.6): dependencies: '@ai-sdk/provider': 3.0.7 '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) - ai: 6.0.74(zod@4.3.6) + ai: 6.0.75(zod@4.3.6) ollama: 0.6.3 transitivePeerDependencies: - zod - ai@6.0.74(zod@4.3.6): + ai@6.0.75(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.36(zod@4.3.6) - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@ai-sdk/gateway': 3.0.37(zod@4.3.6) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@4.3.6) '@opentelemetry/api': 1.9.0 zod: 4.3.6 @@ -13553,6 +13583,11 @@ snapshots: postgres: 3.4.7 prisma: 7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + drizzle-zod@0.8.3(drizzle-orm@1.0.0-beta.9-e89174b(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/mssql@9.1.9)(@types/pg@8.16.0)(mssql@11.0.1)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(zod@4.3.6): + dependencies: + drizzle-orm: 1.0.0-beta.9-e89174b(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/mssql@9.1.9)(@types/pg@8.16.0)(mssql@11.0.1)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)) + zod: 4.3.6 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/src/integrations/orpc/dto/resume.ts b/src/integrations/orpc/dto/resume.ts new file mode 100644 index 000000000..b56260afd --- /dev/null +++ b/src/integrations/orpc/dto/resume.ts @@ -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(), + }, +}; diff --git a/src/integrations/orpc/router/resume.ts b/src/integrations/orpc/router/resume.ts index 7e904ec29..55c3f9d96 100644 --- a/src/integrations/orpc/router/resume.ts +++ b/src/integrations/orpc/router/resume.ts @@ -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 }); }), diff --git a/src/integrations/orpc/services/resume.ts b/src/integrations/orpc/services/resume.ts index bd2331a73..47ba86c39 100644 --- a/src/integrations/orpc/services/resume.ts +++ b/src/integrations/orpc/services/resume.ts @@ -14,7 +14,7 @@ import { hasResumeAccess } from "../helpers/resume-access"; import { getStorageService } from "./storage"; const tags = { - list: async (input: { userId: string }): Promise => { + 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 => { + 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 => { + }) => { const id = generateId(); input.data = input.data ?? defaultResumeData; @@ -269,7 +269,7 @@ export const resumeService = { tags?: string[]; data?: ResumeData; isPublic?: boolean; - }): Promise => { + }) => { 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`${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 => { + 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 => { + 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 => { + 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 => { + delete: async (input: { id: string; userId: string }) => { const storageService = getStorageService(); const deleteResumePromise = db