Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds partner-focused pages and routing, implements partner comments end-to-end (DB model, APIs, actions, hooks, UI), filters bounties by partner eligibility via program enrollment, introduces enrollment update action + audit, refactors the messaging composer into a shared MessageInput, and applies multiple UI/redirect/navigation tweaks. Changes
Sequence Diagram(s)sequenceDiagram
actor U as User
participant UI as Partner Comments UI
participant Action as createPartnerCommentAction
participant DB as Prisma
U->>UI: submit comment (text)
UI->>UI: show optimistic comment (tmp id)
UI->>Action: createPartnerCommentAction({ partnerId, text, createdAt })
Action->>DB: INSERT PartnerComment (resolve programId)
DB-->>Action: created comment with user
Action-->>UI: { comment }
UI->>UI: replace optimistic entry and revalidate /api/partners/{id}/comments
sequenceDiagram
actor C as Client
participant API as GET /api/bounties
participant EN as getProgramEnrollmentOrThrow
participant DB as Prisma
C->>API: GET /api/bounties?workspaceId&partnerId=...
API->>EN: if partnerId present -> lookup enrollment (includeProgram)
EN-->>API: enrollment or null
API->>DB: query bounties with filter: groups none OR groupId in [enrollment.groupId, program.defaultGroupId]
DB-->>API: bounties[]
API-->>C: JSON bounties
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-stats.tsx
Outdated
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (1)
134-145: Add rel="noopener noreferrer" to target=_blank Commissions link; confirm final route.Missing rel is a security gap (reverse‑tabnabbing). Also please re‑confirm that this is the intended canonical route for Commissions in this PR.
Apply:
- <Link - href={`/${workspaceSlug}/program/commissions?partnerId=${partnerId}`} - target="_blank" + <Link + href={`/${workspaceSlug}/program/commissions?partnerId=${partnerId}`} + target="_blank" + rel="noopener noreferrer"Optionally verify the route shape used elsewhere:
#!/bin/bash # Inspect commissions routes/usages rg -nP -C2 '/program/(partners/[^/]+/commissions|commissions\?partnerId=)' apps/web | sed -n '1,200p' fd -H 'commissions' apps/web/app | sed -n '1,200p'
🧹 Nitpick comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (4)
32-49: Wheel listener should be non‑passive to allow preventDefault.Chrome may ignore preventDefault on passive listeners; set passive: false and mirror options in removeEventListener.
useEffect(() => { if (!containerRef.current) return; - const handleWheel = (event: WheelEvent) => { + const handleWheel = (event: WheelEvent) => { if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return; event.preventDefault(); containerRef.current?.scrollBy({ left: event.deltaY, behavior: "smooth", }); }; - containerRef.current.addEventListener("wheel", handleWheel); + const wheelOptions: AddEventListenerOptions = { passive: false }; + containerRef.current.addEventListener("wheel", handleWheel, wheelOptions); - return () => - containerRef.current?.removeEventListener("wheel", handleWheel); + return () => + containerRef.current?.removeEventListener("wheel", handleWheel, wheelOptions); }, []);
51-55: Initialize scroll progress on mount to avoid a stale gradient until first scroll/resize.Call update once after mount.
const { scrollProgress, updateScrollProgress } = useScrollProgress( containerRef, { direction: "horizontal" }, ); + + useEffect(() => { + updateScrollProgress(); + }, [updateScrollProgress]);
99-106: Make active‑tab detection robust for nested routes; add aria‑current for a11y.endsWith fails if viewing a nested child (e.g., /comments/123). Use last segment and expose aria-current.
- {tabs.map(({ id, label, icon: Icon, badge }) => { - const isSelected = pathname.endsWith(`/${id}`); + {tabs.map(({ id, label, icon: Icon, badge }) => { + const lastSeg = pathname.replace(/\/$/, "").split("/").pop(); + const isSelected = lastSeg === id || pathname.includes(`/${id}/`); return ( <Link key={id} href={`/${workspaceSlug}/program/partners/${partnerId}/${id}`} data-selected={isSelected} + aria-current={isSelected ? "page" : undefined} className={cn(Also applies to: 120-129
103-105: Optional: centralize partner URL generationThere's already a getPartnerUrl helper in apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (around line 91). Import and use it here instead of the inline template — or extract it to a shared util — to avoid string-template drift across files.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (3)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-partner-comments-count.ts (1)
usePartnerCommentsCount(5-31)packages/ui/src/hooks/use-scroll-progress.ts (1)
useScrollProgress(6-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build
- GitHub Check: Vade Review
🔇 Additional comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/partner-nav.tsx (1)
74-85: Comments badge logic LGTM.Hides on zero, caps at 99+, and renders correctly as string/number. Nice.
Also applies to: 115-119
…rolled-partner-page
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/lib/actions/partners/update-partner-enrollment.ts (1)
13-17: Validate or dropworkspaceIdto match the schema.
workspaceIdis required by the schema but ignored at runtime. Either validate it againstctx.workspace.idor remove it from the schema.Option A — validate:
- .action(async ({ parsedInput, ctx }) => { + .action(async ({ parsedInput, ctx }) => { const { workspace, user } = ctx; - const { partnerId, tenantId } = parsedInput; + const { partnerId, tenantId, workspaceId } = parsedInput; + if (workspaceId !== workspace.id) { + throw new Error("workspaceId mismatch"); + }Option B — drop from schema:
-const updatePartnerEnrollmentSchema = z.object({ - workspaceId: z.string(), +const updatePartnerEnrollmentSchema = z.object({ partnerId: z.string(), tenantId: z.string().nullable(), });Also applies to: 23-25
🧹 Nitpick comments (4)
apps/web/lib/actions/partners/update-partner-enrollment.ts (4)
26-31: EnsureprogramIdis the canonical ID (not a slug) before write queries.
getProgramEnrollmentOrThrowaccepts slugs or IDs, but Prisma writes below require the ID. Guard by resolving the ID from the fetched enrollment.- const programId = getDefaultProgramIdOrThrow(workspace); - - const { partner } = await getProgramEnrollmentOrThrow({ - partnerId, - programId, - includePartner: true, - }); + const programIdentifier = getDefaultProgramIdOrThrow(workspace); + const enrollment = await getProgramEnrollmentOrThrow({ + partnerId, + programId: programIdentifier, + includePartner: true, + includeProgram: true, + }); + const { partner } = enrollment; + const programId = enrollment.program.id; // canonical IDAlso applies to: 34-38, 46-49
64-77: Limit PII/noise in audit metadata.Logging the full
partnerobject may capture PII and bloat logs. Prefer a minimal, stable subset.Example:
- targets: [ - { - type: "partner", - id: partnerId, - metadata: partner, - }, - ], + targets: [ + { + type: "partner", + id: partnerId, + metadata: { + id: partner.id, + name: partner.name, + handle: partner.handle, + companyName: partner.companyName, + }, + }, + ],
39-59: Short-circuit no-op writes whentenantIdis unchanged.Avoid unnecessary writes and downstream logging when the value doesn’t change.
- const programEnrollment = await prisma.$transaction(async (tx) => { + if (enrollment.tenantId === tenantId) { + return; // nothing to do + } + const programEnrollment = await prisma.$transaction(async (tx) => {
61-63: ScalerecordLinkfor large link sets.If
programEnrollment.linkscan be large, chunk or stream to avoid timeouts/limits in the background task. Also confirmrecordLinkaccepts an array.Example (conceptual):
- waitUntil( - Promise.allSettled([ - recordLink(programEnrollment.links), + const batches = []; + for (let i = 0; i < programEnrollment.links.length; i += 500) { + batches.push(programEnrollment.links.slice(i, i + 500)); + } + waitUntil( + Promise.allSettled([ + ...batches.map((batch) => recordLink(batch)), recordAuditLog({ /* ... */ }), ]), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/lib/actions/partners/update-partner-enrollment.ts(1 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/zod/schemas/partners.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/update-partner-enrollment.ts (1)
apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (2)
apps/web/lib/actions/partners/update-partner-enrollment.ts (2)
61-79: Background tasks wiring LGTM.Passing a single promise via
waitUntil(Promise.allSettled([...]))correctly retains both async side-effects post-response.
16-17: Confirm DB allows settingtenantIdto NULL.You update both Links and ProgramEnrollment to a nullable
tenantId. Verify the columns are nullable and not part of non-null FKs/unique constraints.Also applies to: 41-45, 51-52
| @@ -0,0 +1,91 @@ | |||
| import { MAX_MESSAGE_LENGTH } from "@/lib/zod/schemas/messages"; | |||
There was a problem hiding this comment.
The MessageInput component imports MAX_MESSAGE_LENGTH from messages schema but is used for partner comments, which should use MAX_PROGRAM_PARTNER_COMMENT_LENGTH instead.
View Details
📝 Patch Details
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
index 2e73c3ce9..87789b954 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx
@@ -7,6 +7,7 @@ import { usePartnerComments } from "@/lib/swr/use-partner-comments";
import useUser from "@/lib/swr/use-user";
import useWorkspace from "@/lib/swr/use-workspace";
import { PartnerCommentProps } from "@/lib/types";
+import { MAX_PROGRAM_PARTNER_COMMENT_LENGTH } from "@/lib/zod/schemas/programs";
import { ThreeDots } from "@/ui/shared/icons";
import { MessageInput } from "@/ui/shared/message-input";
import { Button, LoadingSpinner, Popover, Trash } from "@dub/ui";
@@ -33,6 +34,7 @@ export function ProgramPartnerCommentsPageClient() {
return (
<div className="mt-4">
<MessageInput
+ maxLength={MAX_PROGRAM_PARTNER_COMMENT_LENGTH}
onSendMessage={(text) => {
if (!user) return false;
diff --git a/apps/web/ui/shared/message-input.tsx b/apps/web/ui/shared/message-input.tsx
index 4a7218b9b..fde89f3ce 100644
--- a/apps/web/ui/shared/message-input.tsx
+++ b/apps/web/ui/shared/message-input.tsx
@@ -11,12 +11,14 @@ export function MessageInput({
placeholder = "Type a message...",
sendButtonText = "Send",
className,
+ maxLength = MAX_MESSAGE_LENGTH,
}: {
onSendMessage: (message: string) => void | false;
autoFocus?: boolean;
placeholder?: string;
sendButtonText?: string;
className?: string;
+ maxLength?: number;
}) {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const selectionStartRef = useRef<number | null>(null);
@@ -42,7 +44,7 @@ export function MessageInput({
className="placeholder:text-content-subtle block max-h-24 w-full resize-none border-none p-3 text-base focus:ring-0 sm:text-sm"
placeholder={placeholder}
value={typedMessage}
- maxLength={MAX_MESSAGE_LENGTH}
+ maxLength={maxLength}
onChange={(e) => setTypedMessage(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
Analysis
MessageInput uses wrong maxLength constant for partner comments validation
What fails: MessageInput component in apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx uses MAX_MESSAGE_LENGTH (2000) from messages schema, but partner comments are validated server-side against MAX_PROGRAM_PARTNER_COMMENT_LENGTH (2000) from programs schema.
How to reproduce:
- Navigate to partner comments page at
[slug]/program/partners/[partnerId]/comments - Compare client-side maxLength validation in MessageInput component vs server-side validation in createPartnerCommentSchema
Result: Semantic mismatch between client and server validation constants. Currently both are 2000 chars so no functional issue, but if either constant changes independently, validation will become inconsistent.
Expected: Partner comment input should use MAX_PROGRAM_PARTNER_COMMENT_LENGTH for semantic correctness and future-proofing against independent constant changes.
Evidence: MessageInput imports from /lib/zod/schemas/messages.ts but partner comments validate against /lib/zod/schemas/programs.ts schema in createPartnerCommentSchema.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
apps/web/lib/actions/partners/update-partner-comment.ts (1)
13-17: Validate or drop workspaceId from schema
updatePartnerCommentSchemaincludesworkspaceIdbut the action ignores it. Either assert equality withctx.workspace.idhere or remove it from the schema to avoid cross-tenant confusion.apps/web/lib/actions/partners/delete-partner-comment.ts (2)
10-13: Validate or drop workspaceIdSchema includes
workspaceIdbut action doesn’t verify it againstctx.workspace.id. Validate here or remove from schema.
9-22: Enforce ownership and fix Prisma delete selector
deleteneeds a unique selector;{ id, programId }isn’t unique. Also enforceuserIdto prevent deleting others’ comments. UsedeleteMany+countcheck, and return a result.export const deletePartnerCommentAction = authActionClient .schema(deletePartnerCommentSchema) .action(async ({ parsedInput, ctx }) => { - const { workspace } = ctx; + const { workspace, user } = ctx; const { commentId } = parsedInput; const programId = getDefaultProgramIdOrThrow(workspace); - await prisma.partnerComment.delete({ - where: { - id: commentId, - programId, - }, - }); + const { count } = await prisma.partnerComment.deleteMany({ + where: { id: commentId, programId, userId: user.id }, + }); + if (count === 0) { + throw new Error("Comment not found or not owned by you."); + } + return { success: true }; });apps/web/lib/actions/partners/create-partner-comment.ts (1)
17-18: Don’t accept client-supplied createdAt; set on serverClient timestamps are spoofable and race-prone. Set
createdAtserver-side (or rely on DB default) and remove it from input.- const { partnerId, text, createdAt } = parsedInput; + const { partnerId, text, workspaceId } = parsedInput; + if (workspaceId !== workspace.id) { + throw new Error("workspaceId mismatch"); + } ... - const comment = await prisma.partnerComment.create({ + const comment = await prisma.partnerComment.create({ data: { programId, partnerId, userId: user.id, text, - createdAt, },
🧹 Nitpick comments (10)
apps/web/lib/actions/partners/create-partner-comment.ts (1)
13-15: Schema alignment requiredAfter dropping
createdAtfrom input, updatecreatePartnerCommentSchemaaccordingly and trim text to prevent whitespace-only comments.apps/web/lib/zod/schemas/programs.ts (3)
188-191: Align date handling with JSON transportThese are serialized to ISO strings over HTTP. Use
z.coerce.date()to accept strings and Dates seamlessly.- createdAt: z.date(), - updatedAt: z.date(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(),
195-209: Remove clientcreatedAt; trim comment textLet the server/DB set timestamps. Also trim text to disallow whitespace-only.
-export const createPartnerCommentSchema = z.object({ - workspaceId: z.string(), - partnerId: z.string(), - text: z.string().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), - createdAt: z.coerce - .date() - .refine( - (date) => - date.getTime() <= Date.now() && - date.getTime() >= Date.now() - 1000 * 60, - { - message: "Comment timestamp must be within the last 60 seconds", - }, - ), -}); +export const createPartnerCommentSchema = z.object({ + workspaceId: z.string(), + partnerId: z.string(), + text: z.string().trim().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), +});
211-215: Trim on update as wellAdd
.trim()to ensure updates can’t set whitespace-only text.-export const updatePartnerCommentSchema = z.object({ +export const updatePartnerCommentSchema = z.object({ workspaceId: z.string(), id: z.string(), - text: z.string().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), + text: z.string().trim().min(1).max(MAX_PROGRAM_PARTNER_COMMENT_LENGTH), });apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts (2)
27-28: Date schema vs JSON: preferz.coerce.date()in schema
NextResponse.jsonreturns strings; downstream code re-parses withnew Date(...). UpdatePartnerCommentSchematoz.coerce.date()(see schema comment) so server- and client-side validation are consistent. No code change needed here if schema is updated.
14-25: Consider pagination to avoid unbounded result setsIf comment volumes grow, add
take/skip(or cursor) with query params.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx (4)
61-66: Stop sending clientcreatedAtAfter moving timestamping server-side, remove
createdAtfrom the action call.- const result = await createPartnerComment({ - workspaceId: workspaceId!, - partnerId, - text, - createdAt, - }); + const result = await createPartnerComment({ + workspaceId: workspaceId!, + partnerId, + text, + });
73-81: Replace optimistic entry to avoid duplicates; don’t revalidate immediatelyFilter out the optimistic item when inserting the real one; also set
revalidate: falseto avoid double-fetch churn.- return data - ? [result.data.comment, ...data] - : [result.data.comment]; + const real = result.data.comment; + return data + ? [real, ...data.filter((c) => c.id !== optimisticComment.id)] + : [real]; }, { optimisticData: (data) => data ? [optimisticComment, ...data] : [optimisticComment], rollbackOnError: true, + revalidate: false, },
191-239: Hide delete action for undelivered optimistic commentsAvoid sending delete mutations for temporary
tmp_*comments.- {comment ? ( + {comment && comment.delivered !== false ? ( <Popover content={ <div className="grid w-full grid-cols-1 gap-px p-2 sm:w-48"> <Button text="Delete comment" variant="danger-outline" onClick={async () => { setOpenPopover(false); if ( !confirm("Are you sure you want to delete this comment?") ) return; await deleteComment({ workspaceId: workspaceId!, commentId: comment.id, }); }} icon={<Trash className="size-4" />} className="h-9 justify-start px-2 font-medium" /> </div> } align="end" openPopover={openPopover} setOpenPopover={setOpenPopover} > <Button
155-157: Avatar/name fallback robustnessHandle null names and URL-encode to prevent broken avatars.
- src={comment.user.image || `${OG_AVATAR_URL}${comment.user.name}`} - alt={`${comment.user.name} avatar`} + src={ + comment.user.image || + `${OG_AVATAR_URL}${encodeURIComponent(comment.user.name || "User")}` + } + alt={`${comment.user.name || "User"} avatar`}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx(1 hunks)apps/web/lib/actions/partners/create-partner-comment.ts(1 hunks)apps/web/lib/actions/partners/delete-partner-comment.ts(1 hunks)apps/web/lib/actions/partners/update-partner-comment.ts(1 hunks)apps/web/lib/swr/use-partner-comments.ts(1 hunks)apps/web/lib/types.ts(2 hunks)apps/web/lib/zod/schemas/programs.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/swr/use-partner-comments.ts
- apps/web/lib/types.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/lib/actions/partners/delete-partner-comment.ts (1)
apps/web/lib/zod/schemas/programs.ts (1)
deletePartnerCommentSchema(217-220)
apps/web/lib/actions/partners/update-partner-comment.ts (1)
apps/web/lib/zod/schemas/programs.ts (2)
updatePartnerCommentSchema(211-215)PartnerCommentSchema(178-191)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/users.ts (1)
UserSchema(3-7)
apps/web/app/(ee)/api/partners/[partnerId]/comments/route.ts (2)
apps/web/lib/auth/workspace.ts (1)
withWorkspace(41-435)apps/web/lib/zod/schemas/programs.ts (1)
PartnerCommentSchema(178-191)
apps/web/lib/actions/partners/create-partner-comment.ts (2)
apps/web/lib/zod/schemas/programs.ts (2)
createPartnerCommentSchema(195-209)PartnerCommentSchema(178-191)apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-97)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/comments/page-client.tsx (7)
apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-45)apps/web/lib/swr/use-partner-comments.ts (1)
usePartnerComments(6-34)apps/web/lib/actions/partners/create-partner-comment.ts (1)
createPartnerCommentAction(13-42)apps/web/ui/shared/message-input.tsx (1)
MessageInput(8-91)apps/web/lib/actions/partners/delete-partner-comment.ts (1)
deletePartnerCommentAction(9-23)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)packages/ui/src/popover.tsx (1)
Popover(25-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: api-tests
- GitHub Check: Vade Review
| const comment = await prisma.partnerComment.update({ | ||
| where: { | ||
| id, | ||
| programId, | ||
| userId: user.id, | ||
| }, | ||
| data: { | ||
| text, | ||
| }, | ||
| include: { | ||
| user: true, | ||
| }, |
There was a problem hiding this comment.
Fix Prisma update: non-unique WHERE; enforce ownership via updateMany + fetch
update requires a unique selector. { id, programId, userId } isn’t a unique input. Use updateMany guarded by { id, programId, userId }, check count, then fetch the record to return. This also correctly enforces ownership.
- const comment = await prisma.partnerComment.update({
- where: {
- id,
- programId,
- userId: user.id,
- },
- data: {
- text,
- },
- include: {
- user: true,
- },
- });
+ const { count } = await prisma.partnerComment.updateMany({
+ where: { id, programId, userId: user.id },
+ data: { text },
+ });
+ if (count === 0) {
+ throw new Error("Comment not found or not owned by you.");
+ }
+ const comment = await prisma.partnerComment.findUnique({
+ where: { id },
+ include: { user: true },
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const comment = await prisma.partnerComment.update({ | |
| where: { | |
| id, | |
| programId, | |
| userId: user.id, | |
| }, | |
| data: { | |
| text, | |
| }, | |
| include: { | |
| user: true, | |
| }, | |
| const { count } = await prisma.partnerComment.updateMany({ | |
| where: { id, programId, userId: user.id }, | |
| data: { text }, | |
| }); | |
| if (count === 0) { | |
| throw new Error("Comment not found or not owned by you."); | |
| } | |
| const comment = await prisma.partnerComment.findUnique({ | |
| where: { id }, | |
| include: { user: true }, | |
| }); |
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-partner-comment.ts around lines 20 to
31, the prisma.partnerComment.update call uses a non-unique selector ({ id,
programId, userId }) which Prisma rejects; replace it with
prisma.partnerComment.updateMany({ where: { id, programId, userId }, data: {
text } }), check the returned count and throw a NotFound/Forbidden error if
count === 0, then retrieve the updated comment with
prisma.partnerComment.findUnique or findFirst (include: { user: true }) and
return that record so ownership is enforced and a proper comment object is
returned.
🧩 Analysis chain
Verify PartnerComment unique keys before merging
Confirm there’s no composite unique on (id, programId, userId) so the updateMany approach is required.
🏁 Script executed:
#!/bin/bash
# Verify PartnerComment uniques (no composite on programId/userId)
fd -HI --type f 'schema.prisma' packages/prisma | while read -r f; do
echo "---- $f ----"
sed -n '1,240p' "$f" | sed -n '/model \(PartnerComment\|Comment\)/,/^}/p'
done
rg -n --hidden -S '(@@unique|@@id).*(programId|userId).*' packages/prisma || trueLength of output: 730
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Listing .prisma files under packages/prisma:"
if command -v fd >/dev/null 2>&1; then
fd -HI -e prisma packages/prisma || true
else
find packages/prisma -type f -name '*.prisma' -print || true
fi
echo
echo "Search for 'model PartnerComment' (case-sensitive) in prisma schema files:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma || true
echo
echo "If found, print the PartnerComment model blocks:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma | cut -d: -f1 | uniq | while read -r f; do
echo "---- $f ----"
sed -n '/model\s\+PartnerComment\b/,/}/p' "$f" || true
done
echo
echo "Search for any occurrence of 'PartnerComment' anywhere in the repo (case-sensitive):"
rg -n --hidden -S 'PartnerComment' || true
echo
echo "Search for @@unique or @@id lines mentioning programId, userId, or id across prisma schema files:"
rg -n --hidden -S '(@@unique|@@id).*(programId|userId|id)' packages/prisma || true
echo
echo "If a PartnerComment model was found, show any field-level @unique or @id annotations inside it:"
rg -n --hidden -S 'model\s+PartnerComment\b' packages/prisma | cut -d: -f1 | uniq | while read -r f; do
echo "---- field-level @unique/@id in $f ----"
sed -n '/model\s\+PartnerComment\b/,/}/p' "$f" | rg -n --hidden -S '@unique|@id' || true
done
echo
echo "Search for prisma.client usages of partnerComment (update/updateMany) in the codebase:"
rg -n --hidden -S 'prisma\.partnerComment\.(update|updateMany|findUnique|findFirst|findMany|upsert|delete)' || trueLength of output: 8684
Fix prisma.partnerComment.update where — PartnerComment.id is the only unique key
packages/prisma/schema/comment.prisma shows PartnerComment has id @id and no composite @@unique on (programId, userId); using where: { id, programId, userId } is invalid. Change to one of:
- Use the unique id: prisma.partnerComment.update({ where: { id }, data: { text }, include: { user: true } })
- If you must ensure programId+userId match: use updateMany({ where: { id, programId, userId }, data: { text } }) then fetch the record with findUnique({ where: { id }, include: { user: true } }) (or perform both in a transaction).
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-partner-comment.ts around lines 20 to
31, the prisma.partnerComment.update call uses a non-unique composite where {
id, programId, userId } although PartnerComment.id is the only unique key;
replace the where with the unique key (where: { id }) and keep data: { text }
and include: { user: true } OR if you need to enforce programId+userId matching,
switch to updateMany({ where: { id, programId, userId }, data: { text } }) and
then fetch the updated record with findUnique({ where: { id }, include: { user:
true } }) (or perform both operations inside a transaction).
Summary by CodeRabbit
New Features
Improvements
Style