Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 29, 2025

Summary by CodeRabbit

  • New Features

    • Export links to CSV with background processing for large exports and emailed downloadable links.
    • Downloadable export links with configurable expiry included in export emails.
  • Improvements

    • More consistent CSV formatting across exports.
    • Export modal shows session email on background submissions and clearer submission state.
    • Storage handling improved for more reliable uploads/deletes and public/private buckets.
  • Chores

    • Added environment variables for storage buckets.

@vercel
Copy link
Contributor

vercel bot commented Oct 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 29, 2025 5:48pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Centralizes export file creation with a new createDownloadableExport helper; adds batched async link exporting (QStash + cron) and formatLinksForExport; centralizes link filter validation via validateLinksQueryFilters; and migrates storage APIs to an object-argument shape while removing the legacy storageV2 implementation.

Changes

Cohort / File(s) Summary
New export helper
apps/web/lib/api/create-downloadable-export.ts
New helper that uploads a string body to storage (private bucket), sets Content-Disposition, generates a 7-day signed download URL, and returns { fileKey, downloadUrl }.
Links export & validators
apps/web/lib/api/links/format-links-for-export.ts, apps/web/lib/api/links/get-links-count.ts, apps/web/lib/api/links/get-links-for-workspace.ts, apps/web/lib/api/links/validate-links-query-filters.ts, apps/web/lib/zod/schemas/links.ts
Added formatLinksForExport; consolidated/changed getLinksCount and getLinksForWorkspace signatures (promoted top-level params, optional start/end filtering); added validateLinksQueryFilters to centralize domain/folder validation and folderIds derivation; exportColumns now include per-column transform functions.
Async exports (links)
apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts, apps/web/app/(ee)/api/cron/links/export/route.ts, apps/web/app/api/links/export/route.ts
New fetchLinksBatch async generator; export route uses getLinksCount and publishes QStash job when over threshold (202); cron route streams batches, formats rows, creates downloadable export, and emails downloadUrl.
Exports (partners/commissions/partners UI)
apps/web/app/(ee)/api/cron/commissions/export/route.ts, apps/web/app/(ee)/api/cron/partners/export/route.ts, apps/web/app/(ee)/api/partners/export/route.ts, apps/web/ui/modals/export-links-modal.tsx, packages/email/src/templates/export-ready.tsx, apps/web/lib/api/utils/generate-export-filename.ts
Replaced manual storageV2 upload + signed-url flows with createDownloadableExport; inlined batch formatting; UI modal handles 202 responses and uses session email; email template accepts optional program/workspace, expiresInDays, and supports "links". Filename util comment updated only.
Link routes / counts
apps/web/app/api/links/route.ts, apps/web/app/api/links/count/route.ts
Replaced ad-hoc query parsing and folder/domain checks with validateLinksQueryFilters; pass consolidated filters + folderIds into link APIs; added getLinksCount usage for export-threshold logic.
Storage client & API shape
apps/web/lib/storage.ts, apps/web/lib/storage-v2.ts
Introduced new StorageClient with object-argument APIs (upload({key,body,opts,bucket}), delete({key,bucket}), getSignedUploadUrl/getSignedDownloadUrl); removed legacy storageV2 implementation and its exports.
Storage API call updates (many call sites)
apps/web/app/**, apps/web/lib/**, apps/web/scripts/** (e.g., app/(ee)/api/*, app/api/*, lib/actions/*, lib/api/*, scripts/*)
Updated many call sites to the new storage API shapes: storage.upload(...) → storage.upload({ key, body, opts, bucket? }), storage.delete(key) → storage.delete({ key, bucket? }), and storage.getSignedUrl(key) → storage.getSignedUploadUrl({ key }). No behavioral changes beyond argument shape in most places.
Env example
apps/web/.env.example
Added STORAGE_PUBLIC_BUCKET and STORAGE_PRIVATE_BUCKET environment variables.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Modal as Export Modal
    participant API as /api/links/export
    participant QStash as QStash
    participant Cron as /cron/links/export
    participant Formatter as formatLinksForExport
    participant Storage as createDownloadableExport
    participant Email as Email Service

    User->>Modal: Request export
    Modal->>API: POST filters

    alt count > MAX (async)
        API->>API: getLinksCount(filters)
        API->>QStash: Publish job (filters + user/workspace)
        API-->>Modal: 202 Accepted
        Modal->>Modal: Show toast (session email)

        QStash->>Cron: Deliver job
        loop per page
            Cron->>Cron: fetchLinksBatch(filters)
            Cron->>Formatter: format batch
        end
        Cron->>Cron: convert rows → CSV
        Cron->>Storage: createDownloadableExport({ body: CSV, fileKey, fileName })
        Storage-->>Cron: { fileKey, downloadUrl }
        Cron->>Email: send ExportReady(email, downloadUrl)
    else <= MAX (sync)
        API->>API: getLinksForWorkspace(filters)
        API->>Formatter: format rows
        API->>API: convertToCSV
        API-->>User: 200 CSV attachment
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Files/areas needing extra attention:
    • Removal of storageV2 and createDownloadableExport integration (ensure all callers now use storage.getSignedDownloadUrl/getSignedUploadUrl correctly).
    • validateLinksQueryFilters: folder access, domain validation, and folderIds derivation used by many routes.
    • QStash job payload shape and cron route verification (signature parsing, schema compatibility).
    • getLinksCount / getLinksForWorkspace signature changes and all callers for correct param shapes.
    • exportLinksColumns transforms and formatLinksForExport mapping correctness for special columns (tags/link/date).

Possibly related PRs

Suggested reviewers

  • TWilson023

Poem

🐰
I found a key and stitched a file,
Batched the links and hopped a mile,
One download URL, seven days to play—
I mailed it out and danced away. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Async links export" accurately captures the primary feature being implemented in this changeset. The main additions include a new async generator fetchLinksBatch, a new POST endpoint for /api/cron/links/export that processes large link exports asynchronously via QStash, and supporting utilities like formatLinksForExport and createDownloadableExport. While the PR includes substantial refactoring work (storage API signature changes, centralized query validation, email template updates), these are supporting changes to enable the core async export functionality. The title is concise, clear, and specific enough for a teammate reviewing git history to understand the primary objective without being vague or misleading.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch async-links-export

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@devkiran devkiran marked this pull request as ready for review October 29, 2025 07:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/api/links/get-links-count.ts (1)

31-50: Bug: Unintended exclusion of links in folders when no folder filter is provided

When neither folderId nor folderIds is provided, the where clause forces folderId: null, excluding all links inside folders. This regresses the default “no folder filter” behavior.

Fix: only add a folder predicate when folderIds exist or folderId is explicitly set.

-      ...(folderIds
-        ? [
-            {
-              OR: [
-                {
-                  folderId: {
-                    in: folderIds,
-                  },
-                },
-                {
-                  folderId: null,
-                },
-              ],
-            },
-          ]
-        : [
-            {
-              folderId: folderId || null,
-            },
-          ]),
+      ...(folderIds
+        ? [
+            {
+              OR: [
+                { folderId: { in: folderIds } },
+                { folderId: null },
+              ],
+            },
+          ]
+        : folderId
+          ? [
+              {
+                folderId,
+              },
+            ]
+          : []),
🧹 Nitpick comments (17)
apps/web/lib/api/links/get-links-count.ts (2)

51-60: Make search case-insensitive and safer for large datasets

Add mode: "insensitive" to contains predicates to match common UX expectations and avoid case surprises. Optional: consider adding a length guard (e.g., min 2 chars) to prevent full scans.

-              OR: [
-                { shortLink: { contains: search } },
-                { url: { contains: search } },
-              ],
+              OR: [
+                { shortLink: { contains: search, mode: "insensitive" } },
+                { url: { contains: search, mode: "insensitive" } },
+              ],

90-112: Clarify withTags interaction with specific tag filters

withTags adds tags: { some: {} }, but it gets overridden if tagIds/tagNames are present because the latter spread comes later. If that’s intended, add a note; otherwise, combine conditions under a single tags key.

-      ...(withTags && {
-        tags: {
-          some: {},
-        },
-      }),
-      ...(combinedTagIds && combinedTagIds.length > 0
-        ? {
-            tags: { some: { tagId: { in: combinedTagIds } } },
-          }
-        : tagNames
-          ? {
-              tags: {
-                some: {
-                  tag: {
-                    name: {
-                      in: tagNames,
-                    },
-                  },
-                },
-              },
-            }
-          : {}),
+      ...(() => {
+        if (combinedTagIds && combinedTagIds.length > 0) {
+          return { tags: { some: { tagId: { in: combinedTagIds } } } };
+        }
+        if (tagNames) {
+          return { tags: { some: { tag: { name: { in: tagNames } } } } };
+        }
+        return withTags ? { tags: { some: {} } } : {};
+      })(),
apps/web/lib/zod/schemas/links.ts (2)

224-229: Handle string dates too (not only Date instances)

Exports may pass ISO strings; current parser returns empty string for them. Coerce strings to ISO (or pass-through if already ISO).

-    parse: (value: unknown) =>
-      value instanceof Date ? value.toISOString() : "",
+    parse: (value: unknown) => {
+      if (value instanceof Date) return value.toISOString();
+      if (typeof value === "string") {
+        const d = new Date(value);
+        return isNaN(d.getTime()) ? "" : d.toISOString();
+      }
+      return "";
+    },

Apply to both createdAt and updatedAt.

Also applies to: 237-242


218-222: Revenue units: confirm cents vs. dollars

saleAmount is documented as cents in LinkSchema; parsing with Number will emit raw cents. If end-users expect dollars, convert here (e.g., (Number(value ?? 0) / 100).toFixed(2)), or rename the column to “Revenue (cents)”.

apps/web/app/(ee)/api/partners/export/route.ts (1)

52-57: Optional: include a filename in Content-Disposition

Improve UX by adding a deterministic filename (consistent with cron exports).

-    return new Response(convertToCSV(formattedPartners), {
+    return new Response(convertToCSV(formattedPartners), {
       headers: {
         "Content-Type": "text/csv",
-        "Content-Disposition": "attachment",
+        "Content-Disposition": `attachment; filename="${generateExportFilename("partners")}"`,
       },
     });

Note: add generateExportFilename import if you adopt this.

apps/web/app/(ee)/api/cron/commissions/export/route.ts (1)

68-82: Memory footprint for very large exports

allCommissions accumulates all rows before CSV conversion. If exports can be extremely large, consider streaming CSV generation or chunked uploads to reduce peak memory.

apps/web/app/(ee)/api/cron/partners/export/route.ts (1)

68-79: Memory: consider CSV streaming for very large partner sets

As with commissions, streaming can lower peak memory and time-to-first-byte.

apps/web/lib/api/links/format-links-for-export.ts (2)

30-32: Avoid mutating the input columns array

You already reassign a filtered array, which is fine here. For extra safety, sort a shallow copy to avoid side effects if callers reuse the same array.

-  const sortedColumns = columns.sort(
+  const sortedColumns = [...columns].sort(
     (a, b) => (columnOrderMap[a] || 999) - (columnOrderMap[b] || 999),
   );

50-56: Minor: avoid repeated .find by precomputing a lookup map

Perf/readability nit: build a Map<string, (value: unknown) => unknown> once and use it inside the loop.

-  const parseFn = exportLinksColumns.find((c) => c.id === column);
-  if (parseFn) {
-    result[columnIdToLabel[column]] = parseFn.parse(value);
+  const columnDef = columnsById.get(column);
+  if (columnDef?.parse) {
+    result[columnIdToLabel[column]] = columnDef.parse(value);

With:

const columnsById = new Map(exportLinksColumns.map(c => [c.id, c]));
packages/email/src/templates/partner-export-ready.tsx (1)

21-35: LGTM: workspace/program fallback + links support look solid.

Optional: pass an expiresInDays prop so the email copy stays in sync with the 7‑day signed URL TTL.

Apply within this range:

 export default function ExportReady({
   email = "[email protected]",
   downloadUrl = "https://dev.dubassets.com/exports/partners/xxxx.csv",
   exportType = "partners",
   program,
   workspace,
+  expiresInDays = 7,
 }: {
   email: string;
   downloadUrl: string;
   exportType: "partners" | "commissions" | "links";
   program?: {
     name: string;
   };
   workspace?: {
     name: string;
   };
+  expiresInDays?: number;
 }) {
   const contextName = program?.name || workspace?.name || "your workspace";

And add the line below outside this range near the commented expiry note:

<Text className="text-sm leading-6 text-neutral-500">
  This download link will expire in {expiresInDays} days.
</Text>
apps/web/lib/api/create-downloadable-export.ts (2)

26-29: Harden Content-Disposition against header injection.

Sanitize fileName before embedding into the header.

-    headers: {
-      "Content-Disposition": `attachment; filename="${fileName}"`,
-    },
+    headers: {
+      // Prevent CRLF/quote injection; add RFC 5987 filename* for non-ASCII.
+      "Content-Disposition": (() => {
+        const safe = fileName.replace(/[\r\n"]/g, "_");
+        return `attachment; filename="${safe}"; filename*=UTF-8''${encodeURIComponent(
+          safe,
+        )}`;
+      })(),
+    },

19-29: Future-proof for very large exports: accept streams.

If storageV2.upload supports ReadableStream/AsyncIterable, overload body to accept a stream to avoid buffering the entire CSV in memory.

Please confirm storageV2.upload accepts streams; if yes, I can send a patch to add a streaming overload.

apps/web/lib/api/links/get-links-for-workspace.ts (2)

139-145: Support single-bound date filters (gte or lte).

Current filter applies only when both startDate and endDate are present. Allow one-sided ranges.

-      ...(startDate &&
-        endDate && {
-          createdAt: {
-            gte: startDate,
-            lte: endDate,
-          },
-        }),
+      ...((startDate || endDate) && {
+        createdAt: {
+          ...(startDate && { gte: startDate }),
+          ...(endDate && { lte: endDate }),
+        },
+      }),

67-168: Operational note: index for createdAt range scans.

Heavy exports will lean on createdAt; ensure an index exists on (projectId, createdAt) for Link. If not, consider adding it.

apps/web/lib/api/links/validate-links-query-filters.ts (1)

59-64: Remove redundant optional chaining.

Line 60 uses optional chaining (folderIds?.filter) after already confirming Array.isArray(folderIds) on line 59, making the ?. operator unnecessary.

Apply this diff:

  if (Array.isArray(folderIds)) {
-    folderIds = folderIds?.filter((id) => id !== "");
+    folderIds = folderIds.filter((id) => id !== "");
    if (folderIds.length === 0) {
      folderIds = undefined;
    }
  }
apps/web/app/api/links/export/route.ts (2)

77-82: Add filename to Content-Disposition header.

The Content-Disposition header on line 80 is set to "attachment" without a filename, causing browsers to prompt with a generic name. Including a meaningful filename improves user experience.

Apply this diff:

+    const timestamp = new Date().toISOString().split('T')[0];
+    const filename = `links-export-${timestamp}.csv`;
+
    return new Response(csvData, {
      headers: {
        "Content-Type": "application/csv",
-        "Content-Disposition": "attachment",
+        "Content-Disposition": `attachment; filename="${filename}"`,
      },
    });

45-56: Consider passing validated filters instead of raw searchParams to the background job, and return a meaningful message for the 202 response.

Line 22 validates searchParams into filters using linksExportQuerySchema.parse(), but line 48 sends the raw unparsed searchParams to the background job. While the background job re-validates them using the same schema (line 21 in apps/web/app/api/cron/links/export/route.ts), this results in redundant parsing and validation. Pass the already-validated filters object instead to avoid the duplicate validation overhead and maintain consistency.

Additionally, line 55 returns an empty JSON object with status 202. Returning a meaningful message would improve the developer and user experience:

-      return NextResponse.json({}, { status: 202 });
+      return NextResponse.json(
+        { message: "Export is being processed. You will receive an email when it's ready." },
+        { status: 202 }
+      );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d86b60 and 6db8466.

📒 Files selected for processing (17)
  • apps/web/app/(ee)/api/cron/commissions/export/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/links/export/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/partners/export/route.ts (2 hunks)
  • apps/web/app/(ee)/api/partners/export/route.ts (1 hunks)
  • apps/web/app/api/links/count/route.ts (1 hunks)
  • apps/web/app/api/links/export/route.ts (1 hunks)
  • apps/web/app/api/links/route.ts (1 hunks)
  • apps/web/lib/api/create-downloadable-export.ts (1 hunks)
  • apps/web/lib/api/links/format-links-for-export.ts (1 hunks)
  • apps/web/lib/api/links/get-links-count.ts (1 hunks)
  • apps/web/lib/api/links/get-links-for-workspace.ts (3 hunks)
  • apps/web/lib/api/links/validate-links-query-filters.ts (1 hunks)
  • apps/web/lib/api/utils/generate-export-filename.ts (1 hunks)
  • apps/web/lib/zod/schemas/links.ts (1 hunks)
  • apps/web/ui/modals/export-links-modal.tsx (5 hunks)
  • packages/email/src/templates/partner-export-ready.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
apps/web/lib/api/create-downloadable-export.ts (1)
apps/web/lib/storage-v2.ts (1)
  • storageV2 (92-92)
apps/web/app/(ee)/api/cron/commissions/export/route.ts (4)
apps/web/lib/api/commissions/format-commissions-for-export.ts (1)
  • formatCommissionsForExport (34-69)
apps/web/lib/api/create-downloadable-export.ts (1)
  • createDownloadableExport (13-49)
apps/web/lib/api/utils/generate-random-string.ts (1)
  • generateRandomString (3-14)
apps/web/lib/api/utils/generate-export-filename.ts (1)
  • generateExportFilename (5-14)
apps/web/lib/api/links/format-links-for-export.ts (2)
apps/web/lib/zod/schemas/links.ts (1)
  • exportLinksColumns (186-256)
apps/web/lib/api/links/utils/transform-link.ts (1)
  • transformLink (20-53)
apps/web/app/(ee)/api/cron/partners/export/route.ts (4)
apps/web/lib/api/partners/format-partners-for-export.ts (1)
  • formatPartnersForExport (19-68)
apps/web/lib/api/create-downloadable-export.ts (1)
  • createDownloadableExport (13-49)
apps/web/lib/api/utils/generate-random-string.ts (1)
  • generateRandomString (3-14)
apps/web/lib/api/utils/generate-export-filename.ts (1)
  • generateExportFilename (5-14)
apps/web/app/api/links/route.ts (3)
apps/web/lib/zod/schemas/links.ts (1)
  • getLinksQuerySchemaExtended (844-861)
apps/web/lib/api/links/validate-links-query-filters.ts (1)
  • validateLinksQueryFilters (14-70)
apps/web/lib/api/links/get-links-for-workspace.ts (1)
  • getLinksForWorkspace (16-171)
apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1)
apps/web/lib/api/links/get-links-for-workspace.ts (2)
  • GetLinksForWorkspaceProps (8-14)
  • getLinksForWorkspace (16-171)
apps/web/app/(ee)/api/cron/links/export/route.ts (11)
apps/web/lib/zod/schemas/links.ts (1)
  • linksExportQuerySchema (264-284)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/links/validate-links-query-filters.ts (1)
  • validateLinksQueryFilters (14-70)
apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1)
  • fetchLinksBatch (6-28)
apps/web/lib/api/links/format-links-for-export.ts (1)
  • formatLinksForExport (21-61)
apps/web/lib/api/create-downloadable-export.ts (1)
  • createDownloadableExport (13-49)
apps/web/lib/api/utils/generate-random-string.ts (1)
  • generateRandomString (3-14)
apps/web/lib/api/utils/generate-export-filename.ts (1)
  • generateExportFilename (5-14)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/partner-export-ready.tsx (1)
  • ExportReady (17-69)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-178)
apps/web/lib/api/links/validate-links-query-filters.ts (3)
apps/web/lib/zod/schemas/links.ts (1)
  • getLinksQuerySchemaExtended (844-861)
apps/web/lib/types.ts (1)
  • Folder (513-513)
apps/web/lib/api/domains/get-domain-or-throw.ts (1)
  • getDomainOrThrow (7-52)
apps/web/app/api/links/count/route.ts (3)
apps/web/lib/zod/schemas/links.ts (1)
  • getLinksCountQuerySchema (172-184)
apps/web/lib/api/links/validate-links-query-filters.ts (1)
  • validateLinksQueryFilters (14-70)
apps/web/lib/api/links/get-links-count.ts (1)
  • getLinksCount (11-135)
apps/web/lib/api/links/get-links-for-workspace.ts (1)
apps/web/lib/zod/schemas/links.ts (1)
  • getLinksQuerySchemaExtended (844-861)
apps/web/lib/api/links/get-links-count.ts (1)
apps/web/lib/zod/schemas/links.ts (1)
  • getLinksCountQuerySchema (172-184)
apps/web/app/api/links/export/route.ts (5)
apps/web/lib/zod/schemas/links.ts (1)
  • linksExportQuerySchema (264-284)
apps/web/lib/api/links/validate-links-query-filters.ts (1)
  • validateLinksQueryFilters (14-70)
apps/web/lib/api/links/get-links-count.ts (1)
  • getLinksCount (11-135)
apps/web/lib/api/links/get-links-for-workspace.ts (1)
  • getLinksForWorkspace (16-171)
apps/web/lib/api/links/format-links-for-export.ts (1)
  • formatLinksForExport (21-61)
🔇 Additional comments (19)
apps/web/lib/api/utils/generate-export-filename.ts (1)

4-4: Documentation accuracy improved.

The comment example has been updated to match the actual output format generated by the function. This makes it easier for developers to understand the expected filename format at a glance.

apps/web/ui/modals/export-links-modal.tsx (3)

16-16: LGTM! Session integration added correctly.

The import and usage of useSession from next-auth/react is appropriate for accessing the current user's email to provide personalized feedback for async exports.

Also applies to: 45-45


54-54: LGTM! Correct use of react-hook-form's isSubmitting.

Using isSubmitting from formState is the idiomatic approach for tracking form submission status in react-hook-form, replacing any custom loading state management.


245-245: LGTM! Button loading state correctly bound to form submission.

Using isSubmitting for the button loading state provides appropriate visual feedback during form submission and is consistent with the formState destructuring on line 54.

apps/web/lib/api/links/get-links-count.ts (1)

63-71: Semantics: groupBy === "domain" ignores domain filter

When groupBy === "domain", the domain filter is intentionally dropped. Verify that callers expect grouped counts across all domains rather than a single domain bucket when domain is passed.

apps/web/app/(ee)/api/partners/export/route.ts (1)

20-20: LGTM on destructuring columns/filters

Immutable destructuring is clearer and prevents accidental mutation. No issues.

apps/web/app/api/links/count/route.ts (1)

10-16: Good consolidation to a single filters object

Centralizing validation with validateLinksQueryFilters and passing folderIds into getLinksCount simplifies the route and enforces access checks.

Please confirm booleanQuerySchema coerces strings to booleans so showArchived/withTags are actual booleans at this point.

Also applies to: 18-22

apps/web/app/(ee)/api/cron/commissions/export/route.ts (1)

75-83: Efficient batch formatting; nice switch to createDownloadableExport

The batching + single upload path is clean. One concern: createDownloadableExport references expiresIn internally (per helper), ensure it’s defined or passed via options to avoid runtime errors.

apps/web/app/(ee)/api/cron/partners/export/route.ts (1)

75-86: Solid refactor to createDownloadableExport

Consistent with commissions. Verify createDownloadableExport’s signed URL TTL configuration (expiresIn) is available to avoid undefined reference.

apps/web/lib/api/links/format-links-for-export.ts (1)

1-4: Verify import path for transformLink

Ensure ./utils re-exports transformLink (from ./utils/transform-link). If not, adjust the import to the concrete path.

apps/web/app/api/links/route.ts (1)

21-34: LGTM: centralizing filter validation and folder scope.

Validator usage + forwarding unified filters keeps GET lean and consistent with export paths.

Ensure getLinksQuerySchemaBase provides sane defaults for page/pageSize to prevent negative skip.

apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1)

13-27: LGTM: simple, correct batch pagination.

Clean loop with deterministic termination; works well with large datasets.

apps/web/app/(ee)/api/cron/links/export/route.ts (1)

136-143: Clarify that error handling guard is optional given current TypeScript configuration.

The file has useUnknownInCatchVariables disabled in its TypeScript configuration (apps/web/tsconfig.json does not enable it, and no root config overrides it). With TypeScript 5.4.4, the catch parameter error is currently typed as any, making error.message type-safe without additional guards.

However, the suggested defensive pattern—checking error instanceof Error—is a valid best practice and aligns with some patterns already present in the codebase (e.g., claim-bounty-modal.tsx, add-partner-link-modal.tsx). The inconsistency across the codebase suggests this could be standardized, but it remains optional under the current configuration rather than a critical fix.

apps/web/lib/api/links/get-links-for-workspace.ts (1)

163-165: The review comment is incorrect. Schema validation prevents sortBy from being undefined.

The API route validates the query parameters with getLinksQuerySchemaExtended.parse(searchParams) before calling getLinksForWorkspace. The schema defines sortBy with .optional().default("createdAt"), which means any undefined value is automatically replaced with the default "createdAt" string. By the time the code reaches lines 163-165, sortBy is guaranteed to be a valid string, not undefined.

The current code is safe and requires no changes.

Likely an incorrect or invalid review comment.

apps/web/lib/api/links/validate-links-query-filters.ts (2)

1-12: LGTM! Clean interface design.

The interface appropriately extends the query schema with required authentication context (userId and workspace), and all imports are necessary for the validation logic.


14-57: LGTM! Well-structured validation flow.

The validation logic correctly handles domain verification, folder access checks, and conditional folder ID derivation. The comment on lines 45-48 clearly explains when folder IDs need to be computed.

apps/web/app/api/links/export/route.ts (3)

1-15: LGTM! Good threshold for async processing.

The imports correctly reflect the refactored architecture with centralized validation and export formatting. The 1000-link threshold for triggering background processing is a reasonable balance between user experience and server load.


22-42: LGTM! Clean validation flow.

The refactored code correctly uses the centralized validateLinksQueryFilters helper and properly derives the folder context. The type cast to number on line 38 is safe since no groupBy parameter is present in the filters.


58-72: LGTM! Correct synchronous export implementation.

The synchronous export path properly:

  • Conditionally applies date filtering based on the interval
  • Sets appropriate search mode for mega folders (exact) vs regular folders (fuzzy)
  • Uses the validated folderIds from the centralized validation
  • Limits pageSize to MAX_LINKS_TO_EXPORT (safe since count was verified ≤ 1000)

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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/ui/modals/export-links-modal.tsx (1)

102-108: Handle undefined email in async export message.

The toast message may display "(undefined)" if session?.user?.email is unavailable when the form is submitted. This issue was already flagged in a previous review.

Apply the suggested diff from the previous review to provide a fallback for the email display.

🧹 Nitpick comments (3)
apps/web/lib/api/links/format-links-for-export.ts (1)

50-56: Consider minor refactors for clarity.

Two optional improvements:

  1. The variable parseFn on line 50 contains the full column definition object, not just the parse function. Consider renaming to columnDef for clarity.

  2. The else branch (lines 54-55) is unreachable since columns is filtered at lines 26-28 to only include valid columns from exportLinksColumns. The parseFn will always be truthy here.

Apply this diff to improve clarity and remove dead code:

-      const parseFn = exportLinksColumns.find((c) => c.id === column);
+      const columnDef = exportLinksColumns.find((c) => c.id === column);
 
-      if (parseFn) {
-        result[columnIdToLabel[column]] = parseFn.parse(value);
-      } else {
-        result[columnIdToLabel[column]] = value;
-      }
+      result[columnIdToLabel[column]] = columnDef!.parse(value);
apps/web/app/(ee)/api/cron/links/export/route.ts (1)

88-111: Chunk CSV assembly to reduce peak memory usage.

Building allLinks in memory before converting to CSV can cause OOM on large exports. As suggested in a previous review, convert each batch to CSV and append, writing the header once.

Apply this diff to implement chunked CSV assembly:

-    // Fetch links in batches and build CSV
-    const allLinks: Record<string, any>[] = [];
+    // Fetch links in batches and build CSV without buffering all rows
+    let totalCount = 0;
+    const csvParts: string[] = [];
+    let headerWritten = false;
+
     const searchMode: "fuzzy" | "exact" =
       selectedFolder?.type === "mega" ? "exact" : "fuzzy";
 
     const linksFilters = {
       ...filters,
       ...(interval !== "all" && {
         startDate,
         endDate,
       }),
       searchMode,
       includeDashboard: false,
       includeUser: false,
       includeWebhooks: false,
       workspaceId,
       folderIds,
     };
 
-    for await (const { links } of fetchLinksBatch(linksFilters)) {
-      allLinks.push(...formatLinksForExport(links, columns));
-    }
-
-    const csvData = convertToCSV(allLinks);
+    for await (const { links } of fetchLinksBatch(linksFilters)) {
+      const rows = formatLinksForExport(links, columns);
+      totalCount += rows.length;
+      if (rows.length > 0) {
+        const chunk = convertToCSV(rows);
+        if (!headerWritten) {
+          csvParts.push(chunk);
+          headerWritten = true;
+        } else {
+          // Strip header row from subsequent chunks
+          csvParts.push(chunk.split(/\r?\n/).slice(1).join("\n"));
+        }
+      }
+    }
+    const csvData = csvParts.join("\n");

Also update the log message:

     return logAndRespond(
-      `Export (${allLinks.length} links) generated and email sent to user.`,
+      `Export (${totalCount} links) generated and email sent to user.`,
     );
apps/web/app/api/links/export/route.ts (1)

77-82: Add filename to Content-Disposition for better UX.

The Content-Disposition header is set to just "attachment", which will result in a generic filename. Include a meaningful filename to improve the user experience.

Apply this diff:

     return new Response(csvData, {
       headers: {
         "Content-Type": "application/csv",
-        "Content-Disposition": "attachment",
+        "Content-Disposition": `attachment; filename="links-export-${new Date().toISOString().split('T')[0]}.csv"`,
       },
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d86b60 and 6db8466.

📒 Files selected for processing (17)
  • apps/web/app/(ee)/api/cron/commissions/export/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/links/export/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/partners/export/route.ts (2 hunks)
  • apps/web/app/(ee)/api/partners/export/route.ts (1 hunks)
  • apps/web/app/api/links/count/route.ts (1 hunks)
  • apps/web/app/api/links/export/route.ts (1 hunks)
  • apps/web/app/api/links/route.ts (1 hunks)
  • apps/web/lib/api/create-downloadable-export.ts (1 hunks)
  • apps/web/lib/api/links/format-links-for-export.ts (1 hunks)
  • apps/web/lib/api/links/get-links-count.ts (1 hunks)
  • apps/web/lib/api/links/get-links-for-workspace.ts (3 hunks)
  • apps/web/lib/api/links/validate-links-query-filters.ts (1 hunks)
  • apps/web/lib/api/utils/generate-export-filename.ts (1 hunks)
  • apps/web/lib/zod/schemas/links.ts (1 hunks)
  • apps/web/ui/modals/export-links-modal.tsx (5 hunks)
  • packages/email/src/templates/partner-export-ready.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-29T08:57:50.042Z
Learnt from: devkiran
PR: dubinc/dub#3025
File: apps/web/lib/zod/schemas/links.ts:251-255
Timestamp: 2025-10-29T08:57:50.042Z
Learning: In the Dub codebase, the `archived` field for links comes from the database as a number (0 or 1), not as a boolean or string, so checking `value === 1` in the parse function is the correct approach.

Applied to files:

  • apps/web/lib/zod/schemas/links.ts
🔇 Additional comments (29)
apps/web/lib/api/utils/generate-export-filename.ts (1)

3-4: LGTM!

The added example in the comment effectively clarifies the filename format. The timestamp sanitization logic is correct and properly handles the ISO 8601 format.

apps/web/ui/modals/export-links-modal.tsx (4)

16-16: LGTM!

The import is correctly added and necessary for session retrieval.


54-54: LGTM!

Correctly changed to isSubmitting which is the proper React Hook Form property for tracking submission state.


245-245: LGTM!

Correctly updated to use isSubmitting to match the formState destructuring, ensuring consistent form submission state tracking.


45-45: Verify if session loading state handling is necessary beyond existing optional chaining.

The review raises a defensive programming concern: while session?.user?.email uses optional chaining and won't crash, it could display "undefined" in the toast if the session hasn't loaded when the form is submitted. However, this modal exists only in the authenticated dashboard context, optional chaining is already present, and you note this was previously flagged elsewhere.

Determine whether the existing optional chaining and authentication context are sufficient, or if explicit status checking from useSession() is needed to prevent the "undefined" text from appearing in the toast message.

apps/web/lib/zod/schemas/links.ts (1)

186-256: LGTM! Parse functions properly handle type coercion.

The parse functions added to each export column are well-implemented:

  • String/Number coercions with fallbacks handle null/undefined safely
  • Date checks use instanceof before calling toISOString()
  • Array handling checks Array.isArray() before join
  • The archived parsing correctly handles numeric database values (0 or 1)

Based on learnings.

apps/web/lib/api/links/get-links-count.ts (1)

6-25: LGTM! Clean refactor to a typed parameter object.

The consolidation of multiple parameters into a single GetLinksCountParams interface improves maintainability and makes the function signature cleaner.

apps/web/lib/api/links/validate-links-query-filters.ts (1)

1-70: LGTM! Effective centralization of filter validation logic.

This new utility consolidates validation that was previously duplicated across multiple routes. The logic correctly:

  • Validates domain ownership
  • Verifies folder access permissions
  • Derives appropriate folderIds for filtering based on context
  • Cleans up empty array elements
apps/web/app/api/links/count/route.ts (1)

10-22: LGTM! Clean integration with centralized validation.

The refactored route effectively uses validateLinksQueryFilters to replace scattered validation logic, making the code more maintainable.

apps/web/app/(ee)/api/partners/export/route.ts (1)

20-20: LGTM! Improved immutability.

Changing from let to const correctly reflects that the destructured bindings are not reassigned.

apps/web/app/(ee)/api/cron/partners/export/route.ts (1)

2-2: LGTM! Improved export workflow with shared helper.

The refactor to use createDownloadableExport consolidates the upload and URL generation logic, making the export flow cleaner and more maintainable.

Also applies to: 76-86

apps/web/app/(ee)/api/cron/commissions/export/route.ts (1)

3-3: LGTM! Consistent export pattern.

The refactor mirrors the partners export flow, using the shared createDownloadableExport helper for consistency across export endpoints.

Also applies to: 78-88

apps/web/lib/api/links/format-links-for-export.ts (1)

21-61: LGTM! Well-structured export formatting.

The function effectively:

  • Filters and sorts columns by predefined order
  • Handles special cases like link construction and tag name mapping
  • Applies per-column parse functions for type coercion
  • Uses human-friendly labels as keys in the output
apps/web/lib/api/links/get-links-for-workspace.ts (2)

8-14: LGTM!

The new exported interface cleanly extends the query schema and adds workspace/filtering context. The optional date range fields align with the downstream export flows.


139-145: The all-or-nothing date filter is intentional and safe. The getStartEndDates() utility always returns both startDate and endDate (never partial), and both callers pass them together or not at all—never individually. Partial ranges cannot occur with current caller patterns.

packages/email/src/templates/partner-export-ready.tsx (1)

21-34: LGTM!

Making program optional and adding workspace with a fallback chain (contextName) correctly generalizes the template for links, partners, and commissions exports. The default "your workspace" is a reasonable fallback.

apps/web/lib/api/create-downloadable-export.ts (3)

3-10: LGTM!

The interface is well-structured and the 7-day expiration is appropriate for export downloads.


19-33: LGTM!

The blob upload flow correctly sets Content-Disposition for the filename and handles upload failures appropriately.


36-48: LGTM!

The signed URL generation correctly uses the 7-day expiry and includes proper error handling. Returning both fileKey and downloadUrl gives callers flexibility.

apps/web/app/(ee)/api/cron/links/export/fetch-links-batch.ts (1)

6-28: LGTM!

The async generator correctly pages through links, yielding full batches and stopping when a partial page is returned. The termination conditions (empty batch or batch < pageSize) are sound.

apps/web/app/api/links/route.ts (1)

21-34: LGTM!

Refactoring to validateLinksQueryFilters centralizes domain/folder validation and eliminates duplication. The selectedFolder.type driving searchMode is a clean approach.

apps/web/app/(ee)/api/cron/links/export/route.ts (5)

21-24: LGTM!

The payload schema cleanly extends the export query schema with required workspaceId and userId for the background job.


29-38: LGTM!

The signature verification flow is correct: raw body extraction → verification → JSON parsing and validation.


40-72: LGTM!

The user and workspace validation correctly handles missing entities by logging and skipping the export, which is appropriate for an async background job.


74-86: LGTM!

The filter validation and date range computation follow the expected pattern, with appropriate startOfDay/endOfDay boundaries.


113-143: LGTM!

The export upload, email notification, and error handling are correctly implemented. The workspace prop passed to the email template aligns with the template changes.

apps/web/app/api/links/export/route.ts (3)

15-28: LGTM!

The 1000-link threshold for async export is reasonable, and the filter validation is consistent with other routes.


58-75: LGTM!

The direct export path for smaller datasets correctly applies filters and formats links for CSV export.


44-56: The review comment is incorrect; no changes needed.

The searchParams parameter is already a plain object (Record<string, string>) returned by getSearchParams(), not a URLSearchParams. The function converts URLSearchParams to a plain object before passing it to the handler. Spreading a plain object into the QStash JSON body works correctly as written.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/.env.example (1)

94-101: Fix dotenv key order to satisfy linter.

Reorder the STORAGE_* keys alphabetically within this block.

Based on static analysis hints.

 # Use for storing image assets
 STORAGE_ACCESS_KEY_ID=
-STORAGE_SECRET_ACCESS_KEY=
-STORAGE_ENDPOINT=
-STORAGE_BASE_URL=
-STORAGE_PUBLIC_BUCKET=
-STORAGE_PRIVATE_BUCKET=
+STORAGE_BASE_URL=
+STORAGE_ENDPOINT=
+STORAGE_PRIVATE_BUCKET=
+STORAGE_PUBLIC_BUCKET=
+STORAGE_SECRET_ACCESS_KEY=
apps/web/lib/actions/partners/program-resources/add-program-resource.ts (1)

95-123: Move file size validation before upload to prevent orphaned storage objects.

File size is validated after uploading to S3. If the validation fails, the blob remains in storage. The storage.delete() method exists but is never called, resulting in orphaned objects.

Apply this refactor to validate before uploading:

+      // Validate file size from base64 string before upload
+      const base64Data = file.replace(/^data:.+;base64,/, "");
+      const fileSize = Math.ceil((base64Data.length * 3) / 4);
+      if (fileSize / 1024 / 1024 > 10) {
+        throw new Error("File size is too large");
+      }
+
       // Upload the file to storage
       const fileKey = `programs/${program.id}/${resourceType}s/${slugify(name || resourceType)}-${nanoid(4)}${extension ? `.${extension}` : ""}`;
       const uploadResult = await storage.upload({
         key: fileKey,
         body: file,
         opts:
           resourceType === "logo"
             ? {
                 headers: {
                   "Content-Disposition": "attachment",
                   ...(extension === "svg" && {
                     "Content-Type": "image/svg+xml",
                   }),
                 },
               }
             : undefined,
       });

       if (!uploadResult || !uploadResult.url) {
         throw new Error(`Failed to upload ${resourceType}`);
       }
-
-      // Extract file size from base64 string
-      const base64Data = file.replace(/^data:.+;base64,/, "");
-      const fileSize = Math.ceil((base64Data.length * 3) / 4);
-
-      if (fileSize / 1024 / 1024 > 10)
-        throw new Error("File size is too large");

Additionally, consider changing "Content-Disposition": "attachment" to "inline" for logos to allow browser display instead of forcing download.

apps/web/lib/storage.ts (1)

49-66: Prevent Buffer uploads from crashing at runtime.

Line 50 now calls uploadBody.size.toString(), but Buffer bodies (common in our server routes) don’t define .size, so this throws TypeError: Cannot read properties of undefined (reading 'toString') before we ever reach R2. Please guard the Content-Length derivation and fall back to Buffer.byteLength (or omit the header when unknown).

-    const headers = {
-      "Content-Length": uploadBody.size.toString(),
-      ...opts?.headers,
-    };
-
-    if (opts?.contentType) {
-      headers["Content-Type"] = opts.contentType;
-    }
+    let contentLength: number | undefined;
+    if (uploadBody instanceof Blob) {
+      contentLength = uploadBody.size;
+    } else if (Buffer.isBuffer(uploadBody)) {
+      contentLength = uploadBody.byteLength;
+    }
+
+    const headers: Record<string, string> = {
+      ...opts?.headers,
+    };
+
+    if (contentLength !== undefined) {
+      headers["Content-Length"] = contentLength.toString();
+    }
+
+    if (opts?.contentType) {
+      headers["Content-Type"] = opts.contentType;
+    }
🧹 Nitpick comments (3)
apps/web/lib/actions/partners/program-resources/delete-program-resource.ts (1)

54-63: Guard deletes and confirm bucket default.

Add a stored-URL check before deleting and confirm the correct bucket (public vs private) is used by default here.

Example:

-import { storage } from "@/lib/storage";
+import { storage, isStored } from "@/lib/storage";

-    if ((resourceType === "logo" || resourceType === "file") && resource.url) {
+    if ((resourceType === "logo" || resourceType === "file") && resource.url && isStored(resource.url)) {
       try {
-        await storage.delete({ key: resource.url.replace(`${R2_URL}/`, "") });
+        await storage.delete({
+          key: resource.url.replace(`${R2_URL}/`, ""),
+          // bucket: process.env.STORAGE_PUBLIC_BUCKET, // if required by the new API
+        });
       } catch (error) {
         console.error(
           "Failed to delete program resource file from storage:",
           error,
         );
       }
     }
apps/web/app/api/domains/route.ts (1)

151-155: Upload change looks good; consider setting content-type/extension.

Current call is fine. Optionally include an extension in key or set headers (e.g., Content-Type) to ensure correct rendering downstream.

Example:

await storage.upload({
  key: `domains/${domainId}/logo_${nanoid(7)}.png`, // or detect from data URL
  body: logo,
  // opts: { headers: { "Content-Type": "image/png" } }
});
apps/web/lib/actions/partners/program-resources/add-program-resource.ts (1)

101-110: Consider Content-Disposition: inline for images.

For logos meant to render in browsers, inline usually beats attachment. Keep attachment for generic file downloads.

-  "Content-Disposition": "attachment",
+  "Content-Disposition": "inline",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e13c0d and ecc399f.

📒 Files selected for processing (44)
  • apps/web/.env.example (1 hunks)
  • apps/web/app/(ee)/api/admin/ban/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/domains/delete/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (2 hunks)
  • apps/web/app/(ee)/api/customers/[id]/route.ts (2 hunks)
  • apps/web/app/(ee)/api/customers/route.ts (1 hunks)
  • apps/web/app/api/domains/[domain]/route.ts (2 hunks)
  • apps/web/app/api/domains/route.ts (1 hunks)
  • apps/web/app/api/links/bulk/route.ts (1 hunks)
  • apps/web/app/api/oauth/apps/[appId]/route.ts (3 hunks)
  • apps/web/app/api/oauth/apps/route.ts (1 hunks)
  • apps/web/app/api/resumes/upload-url/route.ts (1 hunks)
  • apps/web/app/api/user/route.ts (3 hunks)
  • apps/web/app/api/workspaces/[idOrSlug]/route.ts (2 hunks)
  • apps/web/app/api/workspaces/[idOrSlug]/upload-url/route.ts (1 hunks)
  • apps/web/app/api/workspaces/route.ts (1 hunks)
  • apps/web/lib/actions/add-edit-integration.ts (2 hunks)
  • apps/web/lib/actions/partners/create-program.ts (2 hunks)
  • apps/web/lib/actions/partners/onboard-partner.ts (1 hunks)
  • apps/web/lib/actions/partners/program-resources/add-program-resource.ts (1 hunks)
  • apps/web/lib/actions/partners/program-resources/delete-program-resource.ts (1 hunks)
  • apps/web/lib/actions/partners/update-group-branding.ts (3 hunks)
  • apps/web/lib/actions/partners/update-partner-profile.ts (1 hunks)
  • apps/web/lib/actions/partners/update-program.ts (2 hunks)
  • apps/web/lib/actions/partners/upload-bounty-submission-file.ts (1 hunks)
  • apps/web/lib/actions/partners/upload-email-image.ts (1 hunks)
  • apps/web/lib/actions/partners/upload-lander-image.ts (1 hunks)
  • apps/web/lib/api/conversions/track-lead.ts (1 hunks)
  • apps/web/lib/api/conversions/track-sale.ts (1 hunks)
  • apps/web/lib/api/create-downloadable-export.ts (1 hunks)
  • apps/web/lib/api/links/bulk-delete-links.ts (1 hunks)
  • apps/web/lib/api/links/bulk-update-links.ts (1 hunks)
  • apps/web/lib/api/links/create-link.ts (1 hunks)
  • apps/web/lib/api/links/delete-link.ts (1 hunks)
  • apps/web/lib/api/links/update-link.ts (1 hunks)
  • apps/web/lib/api/partners/create-and-enroll-partner.ts (1 hunks)
  • apps/web/lib/api/workspaces/delete-workspace.ts (2 hunks)
  • apps/web/lib/auth/options.ts (2 hunks)
  • apps/web/lib/integrations/utils.ts (1 hunks)
  • apps/web/lib/storage-v2.ts (0 hunks)
  • apps/web/lib/storage.ts (4 hunks)
  • apps/web/scripts/migrations/migrate-images.ts (1 hunks)
  • apps/web/scripts/partners/delete-program.ts (1 hunks)
  • apps/web/scripts/persist-customer-avatars.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/lib/storage-v2.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/create-downloadable-export.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
PR: dubinc/dub#2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/program-resources/add-program-resource.ts
  • apps/web/lib/actions/partners/update-program.ts
  • apps/web/scripts/partners/delete-program.ts
  • apps/web/lib/actions/partners/update-partner-profile.ts
  • apps/web/lib/actions/partners/create-program.ts
  • apps/web/lib/actions/partners/program-resources/delete-program-resource.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#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/lib/api/links/update-link.ts
  • apps/web/lib/api/links/bulk-delete-links.ts
  • apps/web/app/(ee)/api/cron/domains/delete/route.ts
  • apps/web/lib/api/links/delete-link.ts
  • apps/web/app/api/links/bulk/route.ts
  • apps/web/lib/api/links/bulk-update-links.ts
🧬 Code graph analysis (37)
apps/web/app/api/workspaces/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/program-resources/add-program-resource.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/links/update-link.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/upload-bounty-submission-file.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/update-program.ts (1)
apps/web/lib/storage.ts (2)
  • isStored (253-255)
  • storage (251-251)
apps/web/scripts/partners/delete-program.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/workspaces/[idOrSlug]/upload-url/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/domains/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/update-partner-profile.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/conversions/track-lead.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/auth/options.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/resumes/upload-url/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/create-program.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/oauth/apps/[appId]/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/upload-lander-image.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/integrations/utils.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/(ee)/api/cron/domains/delete/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/links/delete-link.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/user/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/conversions/track-sale.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/(ee)/api/customers/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/workspaces/[idOrSlug]/route.ts (2)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/workspaces/workspace-id.ts (1)
  • prefixWorkspaceId (1-3)
apps/web/app/api/domains/[domain]/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/api/links/bulk/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/(ee)/api/admin/ban/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/links/bulk-update-links.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/upload-email-image.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/workspaces/delete-workspace.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/program-resources/delete-program-resource.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/scripts/persist-customer-avatars.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/actions/partners/update-group-branding.ts (1)
apps/web/lib/storage.ts (2)
  • storage (251-251)
  • isStored (253-255)
apps/web/app/api/oauth/apps/route.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/scripts/migrations/migrate-images.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/app/(ee)/api/customers/[id]/route.ts (1)
apps/web/lib/storage.ts (2)
  • storage (251-251)
  • isStored (253-255)
apps/web/lib/actions/add-edit-integration.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
apps/web/lib/api/links/create-link.ts (1)
apps/web/lib/storage.ts (1)
  • storage (251-251)
🪛 dotenv-linter (4.0.0)
apps/web/.env.example

[warning] 99-99: [UnorderedKey] The STORAGE_PUBLIC_BUCKET key should go before the STORAGE_SECRET_ACCESS_KEY key

(UnorderedKey)


[warning] 100-100: [UnorderedKey] The STORAGE_PRIVATE_BUCKET key should go before the STORAGE_PUBLIC_BUCKET key

(UnorderedKey)

⏰ 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 (38)
apps/web/lib/api/links/delete-link.ts (1)

27-27: LGTM! Storage API migration applied correctly.

The update to storage.delete() correctly migrates from the string-based API to the object-based signature with a key property, maintaining identical functionality.

apps/web/lib/api/links/bulk-update-links.ts (1)

133-140: LGTM! Storage API migration looks correct.

The migration from positional arguments to object parameters is properly done, with all parameters correctly mapped (key, body, opts).

apps/web/lib/api/links/create-link.ts (1)

160-167: LGTM! Clean storage API migration.

The parameter mapping from the positional signature to the object-based signature is correct:

  • keyimages/${response.id}
  • body → image data
  • opts → width/height configuration

This aligns with the broader storage API refactoring across the codebase.

apps/web/lib/api/partners/create-and-enroll-partner.ts (1)

208-211: LGTM! Clean API migration.

The migration from positional arguments to an object-based API improves readability and type safety. The parameter names (key and body) are explicit and self-documenting.

apps/web/lib/actions/partners/create-program.ts (2)

61-64: LGTM! Object-argument pattern improves API consistency.

The refactoring to use object arguments for storage.upload enhances readability and aligns with the broader storage API migration across the codebase.


210-210: LGTM! Consistent storage API migration.

The refactoring to use object arguments for storage.delete is consistent with the broader API migration. The cleanup operation is properly guarded and appropriately runs asynchronously in waitUntil.

apps/web/lib/integrations/utils.ts (1)

17-17: Approved. Storage API migration is complete and consistent.

Verification confirms all 26 storage.delete() calls across the codebase use the new object format { key: ... }, matching the StorageClient.delete method signature. Line 17 aligns with the comprehensive refactoring. No old-style string-argument calls remain.

apps/web/app/(ee)/api/customers/route.ts (1)

169-176: Refactor verified—all storage.upload calls migrated successfully.

The verification confirms that all 23 storage.upload() calls across the codebase have been migrated to the new object-based parameter shape. No old-style positional argument calls remain. The changes at lines 169-176 are part of a complete and consistent refactor with correct parameter mapping.

apps/web/lib/api/links/bulk-delete-links.ts (1)

23-23: LGTM: Clean migration to object-based storage API.

The storage.delete call has been correctly updated to use the new object-based signature. The filtering on line 22 ensures link.image exists before deletion.

apps/web/app/api/domains/[domain]/route.ts (2)

127-130: LGTM: Storage upload correctly migrated to object-based API.

The logo upload now uses the new object-based signature with clear named parameters. The returned URL is properly handled.


198-198: LGTM: Storage delete correctly migrated.

The old logo deletion uses the new object-based signature, maintaining the same deletion logic within the waitUntil block.

apps/web/app/api/oauth/apps/route.ts (1)

121-124: LGTM: Integration logo upload migrated correctly.

The storage.upload call now uses named parameters with the object-based API. The returned URL is properly used to update the integration logo.

apps/web/lib/actions/partners/update-partner-profile.ts (1)

89-92: LGTM: Partner image upload migrated to new storage API.

The storage.upload call correctly uses the object-based signature with named parameters, improving code clarity.

apps/web/lib/api/conversions/track-sale.ts (1)

223-230: LGTM: Customer avatar upload with resizing options migrated correctly.

The storage.upload call properly uses the new object-based API with opts nested for image dimensions. The 128x128 resize parameters are correctly preserved.

apps/web/lib/api/conversions/track-lead.ts (1)

233-240: LGTM: Customer avatar upload migrated consistently.

The storage.upload call mirrors the pattern in track-sale.ts, using the object-based API with nested opts for image resizing. Good consistency across conversion tracking modules.

apps/web/lib/api/workspaces/delete-workspace.ts (2)

54-54: LGTM: Workspace logo deletion migrated to object-based API.

The storage.delete call correctly uses the new signature with the key parameter wrapped in an object.


136-136: LGTM: Admin workspace logo deletion migrated consistently.

The storage.delete call uses the same object-based pattern as the user-facing deletion, maintaining consistency.

apps/web/app/api/links/bulk/route.ts (1)

448-448: LGTM: Proxy image deletion migrated to new storage API.

The storage.delete call correctly uses the object-based signature. The conditional logic on lines 444-446 ensures only matching proxy images are deleted.

apps/web/app/(ee)/api/admin/ban/route.ts (1)

50-53: LGTM; verify bucket selection.

Delete path is correctly guarded with isStored(). Please confirm storage.delete uses the intended bucket by default for user avatars.

apps/web/scripts/partners/delete-program.ts (1)

92-94: LGTM; confirm delete bucket.

Deletion call matches the new API. Please verify it targets the intended bucket (likely public) given the R2_URL origin.

apps/web/lib/actions/partners/upload-lander-image.ts (1)

22-24: LGTM.

Object-arg getSignedUploadUrl usage is correct; response contract unchanged.

apps/web/lib/actions/partners/onboard-partner.ts (1)

35-38: LGTM.

Upload call migration is correct and consistent with the new storage API.

apps/web/app/api/workspaces/[idOrSlug]/upload-url/route.ts (1)

16-18: LGTM! Clean migration to object-based storage API.

The update from getSignedUrl(key) to getSignedUploadUrl({ key }) improves API clarity and aligns with the repository-wide storage API standardization.

apps/web/lib/actions/partners/upload-bounty-submission-file.ts (1)

123-125: LGTM! Consistent with storage API migration.

The object-based call to getSignedUploadUrl({ key }) is correct and maintains the same behavior.

apps/web/lib/actions/add-edit-integration.ts (1)

34-37: LGTM! Storage API updates are correct.

Both the upload and delete operations have been properly migrated to the object-based API pattern.

Also applies to: 58-60

apps/web/app/api/workspaces/route.ts (1)

173-176: LGTM! Object-based upload call is correct.

The migration to storage.upload({ key, body }) is consistent with the broader API standardization effort.

apps/web/scripts/migrations/migrate-images.ts (1)

24-31: LGTM! Object-based API with opts parameter.

The migration properly nests the opts parameter within the object-based call: storage.upload({ key, body, opts: { width, height } }). This maintains the image processing behavior while conforming to the new API shape.

apps/web/app/api/user/route.ts (1)

73-76: LGTM! All storage operations updated correctly.

The avatar upload and cleanup delete operations have been properly migrated to the object-based storage API.

Also applies to: 143-145, 178-178

apps/web/app/api/oauth/apps/[appId]/route.ts (1)

86-89: LGTM! OAuth app logo storage operations updated correctly.

All storage operations (upload for new logo, delete for old logo cleanup) have been properly migrated to the object-based API pattern.

Also applies to: 130-132, 201-203

apps/web/app/api/workspaces/[idOrSlug]/route.ts (1)

103-106: LGTM! Workspace logo storage operations updated correctly.

Both the upload and delete operations for workspace logo management have been properly migrated to the object-based storage API.

Also applies to: 177-179

apps/web/app/(ee)/api/cron/merge-partner-accounts/route.ts (1)

202-204: LGTM! Storage API migration is correct.

The storage.delete calls have been properly migrated to the new object-based API. The key extraction and parameter wrapping are consistent with the repository-wide storage API refactor.

Also applies to: 217-219

apps/web/lib/actions/partners/upload-email-image.ts (1)

21-23: LGTM! Storage API migration is correct.

The migration from storage.getSignedUrl(key) to storage.getSignedUploadUrl({ key }) is correct. The new method name is more explicit about the purpose, and the object-based parameter structure aligns with the broader storage API refactor.

apps/web/lib/api/links/update-link.ts (1)

192-199: LGTM! Storage API migrations are correct.

Both storage.upload and storage.delete calls have been properly migrated to the object-based API. The opts parameter is correctly nested within the upload call, and the key extraction for deletion is appropriate.

Also applies to: 204-204

apps/web/app/(ee)/api/customers/[id]/route.ts (1)

100-107: LGTM! Storage API migrations are correct.

All storage operations (upload and delete) have been properly migrated to the object-based API. The parameter structure is consistent across both the PATCH and DELETE handlers, aligning with the repository-wide storage API refactor.

Also applies to: 110-112, 172-172

apps/web/app/api/resumes/upload-url/route.ts (1)

29-31: LGTM! Storage API migration is correct.

The migration to storage.getSignedUploadUrl({ key }) is correct and consistent with the object-based storage API refactor across the codebase.

apps/web/lib/actions/partners/update-program.ts (1)

54-68: LGTM! Storage API migrations are correct.

All storage operations have been properly migrated to the object-based API:

  • Upload calls for logo and wordmark correctly structured with { key, body }
  • Delete calls for old assets correctly structured with { key }
  • The conditional deletion logic in waitUntil is preserved correctly

Also applies to: 95-103

apps/web/lib/actions/partners/update-group-branding.ts (1)

61-74: LGTM! Storage API migrations are correct.

All storage operations have been properly migrated to the object-based API across multiple functions:

  • Logo and wordmark upload/delete operations in the main action
  • Lander image uploads in the uploadLanderDataImages helper function
  • Consistent parameter structure throughout

Also applies to: 113-121, 246-250

apps/web/app/(ee)/api/cron/domains/delete/route.ts (1)

71-71: LGTM! Storage API migrations are correct.

Both storage.delete calls have been properly migrated to the object-based API. The rate-limited batch deletion logic for link images and the domain logo deletion are both correctly updated while preserving their original behavior.

Also applies to: 131-131

@steven-tey steven-tey merged commit 82ac5d3 into main Oct 29, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the async-links-export branch October 29, 2025 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants