-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Async links export #3025
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
Async links export #3025
Conversation
…ion and improve code readability
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughCentralizes 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Suggested reviewers
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 |
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: 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 providedWhen 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 datasetsAdd
mode: "insensitive"tocontainspredicates 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: ClarifywithTagsinteraction with specific tag filters
withTagsaddstags: { some: {} }, but it gets overridden iftagIds/tagNamesare present because the latter spread comes later. If that’s intended, add a note; otherwise, combine conditions under a singletagskey.- ...(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
createdAtandupdatedAt.Also applies to: 237-242
218-222: Revenue units: confirm cents vs. dollars
saleAmountis documented as cents in LinkSchema; parsing withNumberwill 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-DispositionImprove 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
generateExportFilenameimport if you adopt this.apps/web/app/(ee)/api/cron/commissions/export/route.ts (1)
68-82: Memory footprint for very large exports
allCommissionsaccumulates 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 setsAs 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 inputcolumnsarrayYou 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.findby precomputing a lookup mapPerf/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 confirmingArray.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
searchParamsintofiltersusinglinksExportQuerySchema.parse(), but line 48 sends the raw unparsedsearchParamsto the background job. While the background job re-validates them using the same schema (line 21 inapps/web/app/api/cron/links/export/route.ts), this results in redundant parsing and validation. Pass the already-validatedfiltersobject 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
📒 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
useSessionfrom 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
isSubmittingfrom 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
isSubmittingfor 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"ignoresdomainfilterWhen
groupBy === "domain", the domain filter is intentionally dropped. Verify that callers expect grouped counts across all domains rather than a single domain bucket whendomainis passed.apps/web/app/(ee)/api/partners/export/route.ts (1)
20-20: LGTM on destructuring columns/filtersImmutable 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 objectCentralizing validation with
validateLinksQueryFiltersand passingfolderIdsintogetLinksCountsimplifies the route and enforces access checks.Please confirm
booleanQuerySchemacoerces strings to booleans soshowArchived/withTagsare 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 tocreateDownloadableExportThe batching + single upload path is clean. One concern:
createDownloadableExportreferencesexpiresIninternally (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 tocreateDownloadableExportConsistent 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 fortransformLinkEnsure
./utilsre-exportstransformLink(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
useUnknownInCatchVariablesdisabled 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 parametererroris currently typed asany, makingerror.messagetype-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 callinggetLinksForWorkspace. 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
validateLinksQueryFiltershelper and properly derives the folder context. The type cast tonumberon line 38 is safe since nogroupByparameter 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
folderIdsfrom the centralized validation- Limits pageSize to MAX_LINKS_TO_EXPORT (safe since count was verified ≤ 1000)
|
@coderabbitai full review |
✅ 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/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?.emailis 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:
The variable
parseFnon line 50 contains the full column definition object, not just the parse function. Consider renaming tocolumnDeffor clarity.The
elsebranch (lines 54-55) is unreachable sincecolumnsis filtered at lines 26-28 to only include valid columns fromexportLinksColumns. TheparseFnwill 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
allLinksin 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-Dispositionheader 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
📒 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
isSubmittingwhich is the proper React Hook Form property for tracking submission state.
245-245: LGTM!Correctly updated to use
isSubmittingto 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?.emailuses 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
statuschecking fromuseSession()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
instanceofbefore callingtoISOString()- Array handling checks
Array.isArray()before join- The
archivedparsing 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
GetLinksCountParamsinterface 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
validateLinksQueryFiltersto 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
lettoconstcorrectly 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
createDownloadableExportconsolidates 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
createDownloadableExporthelper 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. ThegetStartEndDates()utility always returns bothstartDateandendDate(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
programoptional and addingworkspacewith 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-Dispositionfor 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
fileKeyanddownloadUrlgives 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
validateLinksQueryFilterscentralizes domain/folder validation and eliminates duplication. TheselectedFolder.typedrivingsearchModeis 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
workspaceIdanduserIdfor 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/endOfDayboundaries.
113-143: LGTM!The export upload, email notification, and error handling are correctly implemented. The
workspaceprop 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
searchParamsparameter is already a plain object (Record<string, string>) returned bygetSearchParams(), 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.
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
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 throwsTypeError: Cannot read properties of undefined (reading 'toString')before we ever reach R2. Please guard the Content-Length derivation and fall back toBuffer.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: ConsiderContent-Disposition: inlinefor images.For logos meant to render in browsers,
inlineusually beatsattachment. Keepattachmentfor generic file downloads.- "Content-Disposition": "attachment", + "Content-Disposition": "inline",
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.tsapps/web/lib/actions/partners/update-program.tsapps/web/scripts/partners/delete-program.tsapps/web/lib/actions/partners/update-partner-profile.tsapps/web/lib/actions/partners/create-program.tsapps/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.tsapps/web/lib/api/links/bulk-delete-links.tsapps/web/app/(ee)/api/cron/domains/delete/route.tsapps/web/lib/api/links/delete-link.tsapps/web/app/api/links/bulk/route.tsapps/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 akeyproperty, 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:
key→images/${response.id}body→ image dataopts→ width/height configurationThis 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 (
keyandbody) 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.uploadenhances 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.deleteis consistent with the broader API migration. The cleanup operation is properly guarded and appropriately runs asynchronously inwaitUntil.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 theStorageClient.deletemethod 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
getSignedUploadUrlusage 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)togetSignedUploadUrl({ 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
optsparameter 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.deletecalls 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)tostorage.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.uploadandstorage.deletecalls have been properly migrated to the object-based API. Theoptsparameter 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 (
uploadanddelete) 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
waitUntilis preserved correctlyAlso 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
uploadLanderDataImageshelper 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.deletecalls 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
Summary by CodeRabbit
New Features
Improvements
Chores