-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Email domains & Marketing campaigns #2964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
WalkthroughAdds email-domain support (CRUD, Resend integration, verification endpoint and cron), email-sender fields and campaign scheduling changes (new from, statuses, validation, QStash scheduling/broadcast), Prisma schema updates for EmailDomain and campaign fields, and multiple frontend UI components/hooks/modals to manage domains and campaign flows. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI
participant API
participant DB
participant Resend
Note right of API #bfe8d6: Create email domain
User->>UI: Submit new domain (slug, fromAddress)
UI->>API: POST /api/email-domains
API->>Resend: create domain
Resend-->>API: resendDomainId
API->>DB: create EmailDomain (resendDomainId, status: pending)
API-->>UI: 201 Created
sequenceDiagram
actor Cron
participant API
participant DB
participant Resend
Note right of API #fff3bf: Cron verification
Cron->>API: GET /api/cron/email-domains/verify
API->>DB: fetch domains needing check (limit 10)
alt domains exist
API->>Resend: fetch domain statuses (parallel)
Resend-->>API: domain status
API->>DB: update EmailDomain.status, lastChecked
else none
API-->>Cron: no domains to check
end
sequenceDiagram
actor User
participant UI
participant API
participant DB
participant QStash
Note right of API #e6f0ff: Campaign schedule & broadcast
User->>UI: Create/Update campaign (type, from, scheduledAt)
UI->>API: POST/PATCH /api/campaigns
API->>API: validateCampaign (status transitions, from-domain)
API->>DB: persist campaign (qstashMessageId if scheduled)
API->>QStash: schedule broadcast (qstash)
QStash->>API: invoke /api/cron/campaigns/broadcast
API->>DB: mark sending, process batches, mark sent
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas needing extra attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
…improve verification handling
|
/bug0 run |
|
@coderabbitai final review |
|
@devkiran: I'll perform a final comprehensive review of all changes in this PR. ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (1)
79-86: LGTM! Clean refactor with dedicated type icon component.The email column now displays the campaign type icon and name in a clear, accessible layout. The
CampaignTypeIconabstraction improves maintainability, and text truncation prevents overflow issues.
🧹 Nitpick comments (2)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (1)
27-96: Consider applying consistent pagination across all resource queries.Only the
linksquery includes atake: 100limit (line 42), whiledomains,tags,partners, andusersqueries have no pagination limits. With the expanded 1-day retention window, this inconsistency becomes more significant.If E2E tests generate large volumes of non-link resources over a 24-hour period, the unbounded queries could impact memory usage and query performance.
Consider one of the following approaches:
- Apply consistent limits across all queries (if resource volumes could be high):
// Add take limit to each query domains: prisma.domain.findMany({ // ... existing where clause take: 100, }), tags: prisma.tag.findMany({ // ... existing where clause take: 100, }), // ... etc
- Verify and document if the current approach is intentional because links are expected to be the only high-volume resource.
apps/web/app/(ee)/api/campaigns/route.ts (1)
27-44: Consider case-insensitive search for better UX.The search functionality uses
containswithout specifying a mode, which defaults to case-sensitive matching in PostgreSQL (and case-insensitive in MySQL). For a better user experience, consider addingmode: 'insensitive'to the search conditions:...(search && { OR: [ - { name: { contains: search } }, - { subject: { contains: search } }, + { name: { contains: search, mode: "insensitive" } }, + { subject: { contains: search, mode: "insensitive" } }, ], }),Note: If using a database that doesn't support case-insensitive operations (like older SQLite versions), you may need to handle this differently or keep the current behavior.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/(ee)/api/campaigns/route.ts(2 hunks)apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts(5 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx(8 hunks)apps/web/lib/api/campaigns/get-campaigns.ts(0 hunks)apps/web/lib/zod/schemas/campaigns.ts(4 hunks)
💤 Files with no reviewable changes (1)
- apps/web/lib/api/campaigns/get-campaigns.ts
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/campaigns.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
🧬 Code graph analysis (4)
apps/web/lib/zod/schemas/campaigns.ts (3)
apps/web/lib/zod/schemas/workflows.ts (1)
workflowConditionSchema(66-70)apps/web/lib/zod/schemas/groups.ts (1)
GroupSchema(51-64)apps/web/lib/zod/schemas/utils.ts (1)
parseDateSchema(37-40)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (2)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/tests/utils/resource.ts (2)
E2E_USER_ID(1-1)E2E_WORKSPACE_ID(2-2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaign-type-icon.tsx (1)
CampaignTypeIcon(4-24)packages/ui/src/timestamp-tooltip.tsx (1)
TimestampTooltip(28-50)
apps/web/app/(ee)/api/campaigns/route.ts (2)
apps/web/lib/zod/schemas/campaigns.ts (1)
getCampaignsQuerySchema(106-112)packages/prisma/index.ts (1)
prisma(3-9)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (11)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (1)
24-24: Verify the 24x increase in retention window is intentional.The retention window has been expanded from 1 hour to 1 day (24 hours), meaning E2E test data will remain in the system 24 times longer before cleanup. Given that this cron job runs every 6 hours, multiple cleanup cycles will now process the same data window.
Please confirm:
- Is this change intentional to accommodate longer-running E2E test scenarios (possibly related to email campaigns/scheduling features introduced in this PR)?
- Have you considered the potential accumulation of test data and its impact on database size and query performance?
If the change is intentional, consider updating the comment on line 16 to reflect the new retention policy for clarity.
apps/web/lib/zod/schemas/campaigns.ts (4)
53-53: LGTM! Appropriate nullable fields for optional campaign properties.The
fromandscheduledAtfields are correctly defined as nullable in the read schema, allowing campaigns to exist without these values set initially.Also applies to: 59-59
91-91: Verify iffromshould support explicit null for clearing.The
fromfield uses.email().trim().toLowerCase()without.nullish(), which means it can be omitted (via.partial()at line 104) but cannot be explicitly set tonull. In contrast,scheduledAtat line 95 usesparseDateSchema.nullish()to support bothnullandundefined.This creates an asymmetry: users can clear
scheduledAtby sendingnull, but cannot clearfromin the same way. If the intent is to allow clearing thefromfield after it's been set, consider applying.nullish()or.nullable()to thefromfield as well.Apply this diff if
fromshould support explicit clearing:- from: z.string().email().trim().toLowerCase(), + from: z.string().email().trim().toLowerCase().nullish(),
96-102: LGTM! New campaign statuses support scheduling functionality.The addition of
scheduledandcanceledstatuses aligns with the PR's campaign scheduling feature and appropriately extends the allowed status transitions.
74-74: LGTM! Appropriate addition to list schema.Including
scheduledAtin the list schema enables displaying when campaigns are scheduled to run, which is useful for the campaigns table view.apps/web/app/(ee)/api/campaigns/route.ts (1)
110-115: LGTM! Correct HTTP semantics for resource creation.Returning
201 Createdfor successful campaign creation is semantically correct and aligns with REST best practices. The response body appropriately contains the new resource identifier.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (5)
104-116: LGTM! Excellent UX with timestamp formatting and tooltip.The Created column provides a user-friendly relative timestamp display with a detailed tooltip on hover. The configuration (side="right", delayDuration=150) is appropriate for table context.
120-122: Verify menu column width is sufficient.The menu column width was reduced from 43 to 20 pixels. Given that the
RowMenuButtonat line 303 usespx-2padding plus an icon, a width of 20 may be too narrow and could cause visual cramping or button clipping.If the button appears cramped, consider reverting to the previous width or adjusting to a value that accommodates the button's actual rendered size (typically 32-40px minimum for an icon button with padding).
{ id: "menu", - minSize: 20, - size: 20, - maxSize: 20, + minSize: 40, + size: 40, + maxSize: 40, cell: ({ row }) => <RowMenuButton row={row} />, },
141-141: LGTM! Column pinning improves table usability.Pinning the
createdAtandmenucolumns to the right ensures they remain visible during horizontal scrolling, which is a standard and effective UX pattern for action-oriented tables.
275-286: LGTM! Correct conditional logic for Pause/Resume.The Pause/Resume action is appropriately restricted to non-draft transactional campaigns. This aligns with the business logic where only automated campaigns can be paused/resumed, while one-time broadcast campaigns and drafts cannot.
314-320: LGTM! Improved precision for percentage calculations.Returning percentages with two decimal places (e.g., 23.45% instead of 23%) provides more accurate metrics representation. The
Number()conversion ensures the return type remains numeric aftertoFixed().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (1)
55-57: Consider consolidating validation logic within react-hook-form.The current approach uses both react-hook-form's
requiredvalidation and a separateisDisabledmemo based on exact text matching. Therequired: truerule is redundant since the button is disabled viaisDisabled, not form validation state.You could simplify by using custom validation within react-hook-form:
- const { - register, - handleSubmit, - watch, - formState: { errors }, - } = useForm<{ confirm: string }>({ - defaultValues: { - confirm: "", - }, - }); - - const confirm = watch("confirm"); - - const isDisabled = useMemo(() => { - return confirm !== "confirm delete campaign"; - }, [confirm]); + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm<{ confirm: string }>({ + mode: "onChange", + defaultValues: { + confirm: "", + }, + });And update the validation rule:
<input className={cn( "block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm", errors.confirm && "border-red-600", )} placeholder="confirm delete campaign" type="text" autoComplete="off" - {...register("confirm", { - required: true, - })} + {...register("confirm", { + validate: (value) => value === "confirm delete campaign" || "Please type the exact phrase", + })} />Then use
isValidfor the button:<Button type="submit" variant="danger" className="h-8 w-fit px-3" text="Delete campaign" - disabled={isDisabled} + disabled={!isValid} loading={isSubmitting} />Also applies to: 91-102
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx(3 hunks)packages/ui/src/menu-item.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsxpackages/ui/src/menu-item.tsx
🔇 Additional comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (1)
65-124: LGTM! Well-structured form and deletion flow.The form structure is well-implemented with proper:
- Form submission handling via
handleSubmit- Submit button type and disabled/loading states
- Campaign summary banner for context
- Success flow with cache invalidation, toast, and navigation
packages/ui/src/menu-item.tsx (2)
23-23: No changes needed — the explicit color approach aligns with established codebase patterns.The search confirms that
packages/ui/srcconsistently uses explicit Tailwind color values (e.g.,bg-red-500,bg-red-600,hover:ring-red-100) for danger/error states acrossbutton.tsx,status-badge.tsx,progress-bar.tsx, anddub-status-badge.tsx. No design token alternatives likebg-bg-errorexist in the codebase. The menu-item.tsx change follows this established pattern perfectly and adds proper dark mode support (dark:hover:bg-red-950/20), which the default variant lacks.
16-16: Hover state consolidation is correct—no issues found.The refactoring safely consolidates hover styles to the variant level. The disabled variant's
hover:bg-bg-default(line 26) properly overrides the variant hover states, and browsers inherently suppress hover on disabled elements anyway. No usages with disabled states were found in the codebase search, but the structure ensures correct behavior when disabledTooltip is set. The styling changes are sound.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
apps/web/app/(ee)/api/campaigns/route.ts (1)
74-121: Verify email domain handling for campaign creation.The past review flagged a concern about non-deterministic email domain selection during campaign creation. Since the email domain query logic is not visible in the current code snippet, please verify that:
- If email domain selection happens during creation, it includes explicit
orderByto ensure deterministic results- Validation exists to handle cases where no verified email domain is available
- The
fromfield is properly set when a verified domain existsThis verification ensures campaigns aren't created with
from: nullwhen a verified domain should be assigned.#!/bin/bash # Description: Verify email domain handling in campaign creation flow # Search for email domain queries in campaign creation rg -nP -C5 'emailDomain.*findFirst|findFirst.*emailDomain' -g '*campaign*' -g '!*.test.ts' # Search for verified domain validation rg -nP -C5 'status.*verified|verified.*domain' -g '*campaign*' -g '!*.test.ts' # Check if from field is set during creation ast-grep --pattern 'await tx.campaign.create({ $$$ from: $VALUE $$$ })'
🧹 Nitpick comments (1)
apps/web/tests/campaigns/index.test.ts (1)
35-35: Tighten status validation in test expectations.Using
expect.any(String)for thestatusfield is overly permissive. It would be better to validate against specific expected status values to catch regressions.Consider updating to:
const expectedCampaign: Partial<Campaign> = { ...campaign, type: "transactional", - status: expect.any(String), + // Status varies by test but should be a valid CampaignStatus enum value from: null, scheduledAt: null, groups: [{ id: E2E_PARTNER_GROUP.id }],Then in each test, explicitly assert the expected status (as already done in most tests like lines 78, 91, 107, 123, 139).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/app/(ee)/api/campaigns/route.ts(3 hunks)apps/web/lib/zod/schemas/campaigns.ts(4 hunks)apps/web/tests/campaigns/index.test.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/campaigns.ts
🧬 Code graph analysis (2)
apps/web/lib/zod/schemas/campaigns.ts (3)
apps/web/lib/zod/schemas/workflows.ts (1)
workflowConditionSchema(66-70)apps/web/lib/zod/schemas/groups.ts (1)
GroupSchema(51-64)apps/web/lib/zod/schemas/utils.ts (1)
parseDateSchema(37-40)
apps/web/app/(ee)/api/campaigns/route.ts (2)
apps/web/lib/zod/schemas/campaigns.ts (2)
getCampaignsQuerySchema(102-108)CampaignSchema(49-62)packages/prisma/index.ts (1)
prisma(3-9)
⏰ 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). (1)
- GitHub Check: build
🔇 Additional comments (6)
apps/web/lib/zod/schemas/campaigns.ts (2)
10-10: LGTM!The
parseDateSchemaimport is correctly added to support the newscheduledAtfield validation inupdateCampaignSchema.
53-53: LGTM!The new
fromandscheduledAtfields are appropriately added as nullable to support email sender configuration and campaign scheduling.Also applies to: 59-59
apps/web/app/(ee)/api/campaigns/route.ts (2)
25-59: LGTM! Well-structured query refactor.The GET endpoint refactor is solid:
- Direct Prisma query with proper filtering, ordering, and pagination
- Includes necessary relations (
groups,workflow)- Results validated through
CampaignSchema- Proper optional chaining for nullable relations
One minor note: Line 56 assumes the first trigger condition is the primary one (
campaign.workflow?.triggerConditions?.[0]). This is safe due to optional chaining but assumes a single-condition workflow design.
123-128: LGTM! More RESTful status code.Changing the POST response to
201 Createdis the correct HTTP semantics for resource creation, improving API consistency.apps/web/tests/campaigns/index.test.ts (2)
57-57: LGTM!Test correctly updated to expect
201 Createdstatus, matching the API change.
178-181: LGTM! Cleaner assertion pattern.Using the spread operator to reuse
expectedCampaignimproves maintainability and reduces duplication.
| .string() | ||
| .trim() | ||
| .max(100, "Subject must be less than 100 characters."), | ||
| from: z.string().email().trim().toLowerCase(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
LGTM! Validation flow looks appropriate.
The new fields and statuses are well-structured:
fromhas proper email validation with normalization (trim, lowercase)scheduledAtusesparseDateSchema.nullish()for flexible date input- New
scheduledandcanceledstatuses support the expanded campaign lifecycle
Since the schema is .partial(), these fields are optional during updates, which is correct for PATCH semantics. The email validation ensures that when from is provided, it's valid.
Verify that business logic elsewhere (e.g., before scheduling) enforces that from must be set and valid:
Also applies to: 91-91, 96-97
🏁 Script executed:
#!/bin/bash
# Description: Check for validation logic that enforces 'from' is set before scheduling campaigns
# Search for campaign validation logic
ast-grep --pattern 'function $FUNC($$$) {
$$$
scheduledAt
$$$
from
$$$
}'
# Search for scheduling-related validation
rg -nP -C3 --type=ts 'scheduledAt.*from|from.*scheduledAt' -g '!*.test.ts' -g '!node_modules'
# Search for validation before campaign scheduling
rg -nP -C5 'status.*scheduled|schedule.*campaign' -g 'validate*.ts' -g '*validation*.ts'Length of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for updateCampaignSchema usage in the codebase
echo "=== Searching for updateCampaignSchema usage ==="
rg -n 'updateCampaignSchema' --type=ts --type=tsx -C2
echo -e "\n=== Searching for campaign API routes/handlers ==="
fd -e ts -e tsx | grep -i 'campaign' | head -20
echo -e "\n=== Searching for campaign update handlers ==="
rg -n 'PUT.*campaign|PATCH.*campaign' --type=ts -C3
echo -e "\n=== Searching for scheduling validation ==="
rg -n 'status.*scheduled|schedule' --type=ts -C2 | grep -i 'campaign\|validate\|schema' | head -30
Length of output: 6392
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine validate-campaign.ts to understand validation logic
echo "=== Contents of validate-campaign.ts ==="
cat apps/web/lib/api/campaigns/validate-campaign.ts
echo -e "\n=== PATCH handler in route.ts ==="
cat apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
echo -e "\n=== Schedule campaigns logic ==="
cat apps/web/lib/api/campaigns/schedule-campaigns.ts
Length of output: 3430
---
🏁 Script executed:
```shell
#!/bin/bash
# Use fd to find and read the PATCH route handler
echo "=== PATCH handler route.ts ==="
fd 'campaigns.*campaignId.*route' -e ts | head -1 | xargs cat
echo -e "\n=== Schedule campaigns logic ==="
cat apps/web/lib/api/campaigns/schedule-campaigns.ts
echo -e "\n=== Campaign status transitions constants ==="
cat apps/web/lib/api/campaigns/constants.ts
Length of output: 4070
---
🏁 Script executed:
```shell
#!/bin/bash
# Read the PATCH handler using different approach
echo "=== PATCH handler route.ts ==="
find apps/web -path "*campaigns*campaignId*route.ts" -type f | head -1 | xargs cat 2>/dev/null || rg -n "export const PATCH" --type=ts -A 50 -g "*campaigns*" | head -80
echo -e "\n=== Check if validateCampaign enforces from for scheduled status ==="
rg -n "validateCampaign|validateCampaignFromAddress" apps/web/app -A 5 -B 2 --type=ts | head -60
Length of output: 5851
Add validation to enforce from field is set before scheduling campaigns.
The schema and validation logic have a gap: a user can transition a campaign to "scheduled" status without providing a from field. The validateCampaign function only validates from if it's included in the update input (if (input.from)), but doesn't check if an existing campaign already has a from address before allowing a transition to scheduled status.
This is confirmed by a TODO comment in the broadcast handler: "We should make the from address required. There are existing campaign without from address"—indicating the team is aware of this gap.
Fix: In apps/web/lib/api/campaigns/validate-campaign.ts, add a check in the validateCampaign function to enforce that campaign.from exists before allowing a transition to the scheduled status:
if (input.status === CampaignStatus.scheduled) {
if (!campaign.from) {
throw new DubApiError({
code: "bad_request",
message: "Campaign must have a valid 'from' address before scheduling.",
});
}
}🤖 Prompt for AI Agents
In apps/web/lib/api/campaigns/validate-campaign.ts around where validateCampaign
handles status updates (near transition to scheduled), ensure that when
input.status === CampaignStatus.scheduled you verify the existing campaign has a
from address; if not, throw a DubApiError with code "bad_request" and message
"Campaign must have a valid 'from' address before scheduling." so campaigns
cannot be moved to scheduled without a campaign.from value.
Summary by CodeRabbit