Add cursor based pagination to Links, Customers, and Commissions#3184
Add cursor based pagination to Links, Customers, and Commissions#3184steven-tey merged 54 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a cursor+offset pagination utility and integrates it into commissions, customers, and links APIs with cursor validation; updates Zod schemas and OpenAPI metadata; adds deterministic Prisma composite indexes; introduces pagination integration tests and helpers; and defaults many page params to 1 across routes. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant API as "API Handler"
participant Validator as "Cursor Validator"
participant Pager as "buildPaginationQuery"
participant DB as Database
Client->>API: GET /resource?startingAfter=cursor_id&pageSize=10
API->>Validator: validate cursor (startingAfter / endingBefore)
Validator->>DB: SELECT ... WHERE id = cursor_id
DB-->>Validator: record / null
alt cursor exists
Validator-->>API: valid
API->>Pager: buildPaginationQuery(filters)
Pager-->>API: { cursor?, orderBy, take, skip }
API->>DB: Prisma findMany with pagination params
DB-->>API: paginated rows
API-->>Client: 200 results + nextCursor
else cursor missing
Validator-->>API: invalid
API-->>Client: 422 DubApiError (unprocessable_entity)
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 7
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/zod/schemas/misc.ts (1)
54-65:⚠️ Potential issue | 🟡 MinorMark
pageSizeas deprecated too.When
deprecated: true, onlypageis annotated as deprecated right now.pageSizestill shows up as current in the shared schema metadata, so the public API surface for links/customers/commissions is only half-deprecated.Suggested tweak
pageSize: z.coerce .number({ error: "Page size must be a number." }) .positive({ message: "Page size must be greater than 0." }) .max(pageSize, { message: `Max page size is ${pageSize}.`, }) .optional() .default(pageSize) - .describe("The number of items per page.") + .describe( + deprecated + ? "DEPRECATED. Use cursor-based pagination instead." + : "The number of items per page.", + ) .meta({ example: 50, + deprecated, }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/zod/schemas/misc.ts` around lines 54 - 65, The pageSize schema in apps/web/lib/zod/schemas/misc.ts is not marked deprecated like page is; update the pageSize definition (the symbol pageSize) to include the deprecation metadata by adding deprecated: true to its .meta({ ... }) call (merge with existing keys such as example if present) so the shared schema metadata marks pageSize as deprecated as well.
♻️ Duplicate comments (1)
apps/web/tests/links/list-links.test.ts (1)
50-76:⚠️ Potential issue | 🟠 MajorMake the
describe.concurrentcallback synchronous.
beforeAllalready does the async setup, so the async suite callback is just extra risk here and still trips Vitest's invalid-describe-callback rules/patterns.Suggested fix
-describe.concurrent("/links/** - pagination", async () => { +describe.concurrent("/links/** - pagination", () => {Does Vitest v4 allow an async callback passed directly to describe() or describe.concurrent(), or must suite callbacks be synchronous?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/links/list-links.test.ts` around lines 50 - 76, The suite callback passed to describe.concurrent is async but should be synchronous; remove the async keyword from the describe.concurrent callback declaration so the suite is a normal synchronous function and keep the existing async beforeAll setup (which performs the async initialization using IntegrationHarness, http, baseline, baselineIds and expectSortedByCreatedAt). Ensure you only change the describe.concurrent declaration (not beforeAll, IntegrationHarness usage, or assertions) so Vitest's describe callback rules are satisfied.
🧹 Nitpick comments (2)
apps/web/tests/customers/pagination.test.ts (1)
1-2: Avoid hardcoding the max-page constant in the assertion.This will drift if
CUSTOMERS_MAX_PAGE_SIZEchanges. Import the schema constant and build the expected message from it so the test stays aligned with validation.Suggested tweak
import { Customer } from "@/lib/types"; +import { CUSTOMERS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/customers"; import { beforeAll, describe, expect, test } from "vitest"; @@ code: "unprocessable_entity", message: - "Page is too big (cannot be more than 100), recommend using cursor-based pagination instead.", + `Page is too big (cannot be more than ${CUSTOMERS_MAX_PAGE_SIZE}), recommend using cursor-based pagination instead.`, doc_url: "https://dub.co/docs/api-reference/errors#unprocessable-entity",Also applies to: 103-119
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/customers/pagination.test.ts` around lines 1 - 2, The test currently hardcodes the max-page value in its assertion; import the schema constant CUSTOMERS_MAX_PAGE_SIZE (or the exported name where it's defined) into apps/web/tests/customers/pagination.test.ts and construct the expected validation message using that constant instead of a literal number; update both occurrences (around the initial assertion and lines ~103-119) to reference CUSTOMERS_MAX_PAGE_SIZE so the test stays in sync with the schema validation.apps/web/tests/commissions/pagination.test.ts (1)
202-250: Add an ascending cursor case.This block only validates offset pagination for
sortOrder: "asc". Cursor pagination changes its comparison direction in ascending mode, so that branch can regress without any test failing here.Example of an asc cursor assertion to add nearby
test("Offset pagination with sort order asc works correctly", async () => { // Get baseline in ascending order const { status: baselineStatus, data: ascBaseline } = await http.get< CommissionResponse[] >({ path: "/commissions", query: { pageSize: "25", sortBy: "createdAt", sortOrder: "asc", }, }); expect(baselineStatus).toEqual(200); expectSortedByCreatedAtAsc(ascBaseline); const ascBaselineIds = ascBaseline.map((c) => c.id); // Test offset pagination with asc const page1 = await http.get<CommissionResponse[]>({ path: "/commissions", query: { page: "1", pageSize: "5", sortBy: "createdAt", sortOrder: "asc", }, }); const page2 = await http.get<CommissionResponse[]>({ path: "/commissions", query: { page: "2", pageSize: "5", sortBy: "createdAt", sortOrder: "asc", }, }); expect(page1.status).toEqual(200); expect(page2.status).toEqual(200); expect(page1.data.map((c) => c.id)).toEqual(ascBaselineIds.slice(0, 5)); expect(page2.data.map((c) => c.id)).toEqual(ascBaselineIds.slice(5, 10)); expectSortedByCreatedAtAsc(page1.data); expectSortedByCreatedAtAsc(page2.data); expectNoOverlap(page1.data, page2.data); + + const ascCursorPage = await http.get<CommissionResponse[]>({ + path: "/commissions", + query: { + pageSize: "5", + sortBy: "createdAt", + sortOrder: "asc", + startingAfter: ascBaselineIds[4], + }, + }); + + expect(ascCursorPage.status).toEqual(200); + expect(ascCursorPage.data.map((c) => c.id)).toEqual( + ascBaselineIds.slice(5, 10), + ); + expectSortedByCreatedAtAsc(ascCursorPage.data); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/commissions/pagination.test.ts` around lines 202 - 250, Add a cursor-pagination assertion for ascending order next to the existing offset test: after the offset checks in the "Offset pagination with sort order asc works correctly" test, call the same endpoint via http.get with sortBy: "createdAt", sortOrder: "asc" and pageSize (e.g., "5") using cursor-based params (first fetch without cursor, then fetch using the returned cursor from the last item of page1), then assert responses are 200, that page1.data and page2.data IDs equal ascBaselineIds.slice(0,5) and ascBaselineIds.slice(5,10) respectively, and reuse expectSortedByCreatedAtAsc and expectNoOverlap to validate ordering and no overlap; reference the existing http.get calls, CommissionResponse typing, and helper functions expectSortedByCreatedAtAsc / expectNoOverlap to implement the new assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/lib/api/commissions/get-commissions.ts`:
- Around line 35-54: The cursor validation currently uses
prisma.commission.findUnique to check global existence of cursorId (from
startingAfter/endingBefore); instead, build and reuse the same filtered "where"
object used by the main query (including program, status, user, etc.), then
validate the cursor by querying with that filter plus the id (e.g.,
prisma.commission.findFirst or findUnique with an ANDed where) so the cursor
must belong to the current filtered result set; if no record is found, throw the
same DubApiError for an invalid cursor. Ensure you reference cursorId,
startingAfter/endingBefore, the shared where object, and
prisma.commission.findFirst/findUnique in the change.
In `@apps/web/lib/api/links/get-links-for-workspace.ts`:
- Around line 50-69: The cursor validation currently only checks global
existence (cursorId via prisma.link.findUnique) but must ensure the cursor
record matches the same filters used for the main list query; change the
validation to query using the same where-clause as the main findMany (include
workspaceId, folder/tag/search/domain/date/other filters) and include id:
cursorId (use prisma.link.findFirst or prisma.link.findMany with id plus the
existing where) so it only succeeds when the cursor belongs to the filtered
result set; if not found, throw the same DubApiError as before. Ensure you
reference filters.startingAfter and filters.endingBefore for cursorId and reuse
the exact where object used by the main findMany to avoid drift.
In `@apps/web/lib/api/pagination.ts`:
- Around line 55-85: The cursor and offset branches must use a stable two-column
ordering (createdAt then id) to preserve requested sort and avoid duplicates;
update the useCursorPagination branch (where useCursorPagination, startingAfter,
endingBefore, sortOrder are used) to set orderBy to include createdAt: sortOrder
and id: sortOrder (as a tiebreaker) and adapt the cursor payload to use a
composite cursor (createdAt + id) instead of relying on id alone, and update the
offset branch (where sortBy, sortOrder, take, skip are returned) to return
orderBy: { createdAt: sortOrder, id: sortOrder } rather than only id so offset
pagination uses the same stable ordering.
In `@apps/web/lib/customers/api/get-customers.ts`:
- Around line 29-48: The cursor validation currently checks global existence;
change the prisma.customer.findUnique call that looks up cursorRecord to include
the same filtered WHERE clause used for the main query (i.e. combine the
constructed filter object used later for listing customers with the id check,
e.g. where: { ...finalWhere, id: cursorId }) so the cursor must belong to the
filtered customer set before allowing pagination; keep the DubApiError throw on
failure.
In `@apps/web/lib/zod/schemas/misc.ts`:
- Around line 40-53: Remove the .default(1) from the page zod schema (the `page`
field in the Zod schema in misc.ts) so omitted vs explicit page=1 can be
distinguished; then apply the default only in the offset-pagination code path by
changing the pagination helper to use a nullish fallback (e.g., replace direct
use of filters.page with `const page = filters.page ?? 1` inside the
offset/limit branch where offset-pagination is selected) so validation in the
useCursorPagination check (`if (useCursorPagination && page > 1)`) correctly
rejects mixed pagination modes.
In `@apps/web/tests/commissions/pagination.test.ts`:
- Around line 57-82: The tests "Cursor forward (startingAfter)" and "Cursor
backward (endingBefore)" only check length and monotonic ID order; tighten them
to assert the exact cursor window by comparing returned IDs to the expected
slice from baseline using the same sort parameters. For the forward test
(variables firstPage, lastId) compute the expected IDs =
baseline.slice(0,5).map(b => b.id) and assert data.map(d => d.id) equals that
array; for the backward test (beforeId) compute expected IDs =
baseline.slice(1,6).map(b => b.id) (or the equivalent slice used by the API) and
assert equality instead of only checking length and expectSortedById; keep the
http.get call and CommissionResponse type and ensure the sort param matches the
baseline when forming the query.
In `@apps/web/tests/links/list-links.test.ts`:
- Around line 97-121: The cursor tests ("Cursor forward (startingAfter)" and
"Cursor backward (endingBefore)") currently only assert local id ordering;
update each test to request the same sort params used to build the baseline
(e.g., include sortBy: "createdAt" and sortDir: "desc" in the http.get query)
and assert the returned ids exactly match the expected window from baselineIds
(compute expectedIds via baselineIds.slice(...) for the forward/backward window
and compare to data.map(d => d.id)). This ensures pagination is validated
against the createdAt-ordered baseline rather than just id ordering.
---
Outside diff comments:
In `@apps/web/lib/zod/schemas/misc.ts`:
- Around line 54-65: The pageSize schema in apps/web/lib/zod/schemas/misc.ts is
not marked deprecated like page is; update the pageSize definition (the symbol
pageSize) to include the deprecation metadata by adding deprecated: true to its
.meta({ ... }) call (merge with existing keys such as example if present) so the
shared schema metadata marks pageSize as deprecated as well.
---
Duplicate comments:
In `@apps/web/tests/links/list-links.test.ts`:
- Around line 50-76: The suite callback passed to describe.concurrent is async
but should be synchronous; remove the async keyword from the describe.concurrent
callback declaration so the suite is a normal synchronous function and keep the
existing async beforeAll setup (which performs the async initialization using
IntegrationHarness, http, baseline, baselineIds and expectSortedByCreatedAt).
Ensure you only change the describe.concurrent declaration (not beforeAll,
IntegrationHarness usage, or assertions) so Vitest's describe callback rules are
satisfied.
---
Nitpick comments:
In `@apps/web/tests/commissions/pagination.test.ts`:
- Around line 202-250: Add a cursor-pagination assertion for ascending order
next to the existing offset test: after the offset checks in the "Offset
pagination with sort order asc works correctly" test, call the same endpoint via
http.get with sortBy: "createdAt", sortOrder: "asc" and pageSize (e.g., "5")
using cursor-based params (first fetch without cursor, then fetch using the
returned cursor from the last item of page1), then assert responses are 200,
that page1.data and page2.data IDs equal ascBaselineIds.slice(0,5) and
ascBaselineIds.slice(5,10) respectively, and reuse expectSortedByCreatedAtAsc
and expectNoOverlap to validate ordering and no overlap; reference the existing
http.get calls, CommissionResponse typing, and helper functions
expectSortedByCreatedAtAsc / expectNoOverlap to implement the new assertions.
In `@apps/web/tests/customers/pagination.test.ts`:
- Around line 1-2: The test currently hardcodes the max-page value in its
assertion; import the schema constant CUSTOMERS_MAX_PAGE_SIZE (or the exported
name where it's defined) into apps/web/tests/customers/pagination.test.ts and
construct the expected validation message using that constant instead of a
literal number; update both occurrences (around the initial assertion and lines
~103-119) to reference CUSTOMERS_MAX_PAGE_SIZE so the test stays in sync with
the schema validation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9a89b269-351d-4bc4-b1ee-52d595f6318f
📒 Files selected for processing (17)
apps/web/lib/api/commissions/get-commissions.tsapps/web/lib/api/links/get-links-for-workspace.tsapps/web/lib/api/pagination.tsapps/web/lib/customers/api/get-customers.tsapps/web/lib/openapi/commissions/list-commissions.tsapps/web/lib/openapi/customers/get-customers.tsapps/web/lib/zod/schemas/commissions.tsapps/web/lib/zod/schemas/customers.tsapps/web/lib/zod/schemas/links.tsapps/web/lib/zod/schemas/misc.tsapps/web/tests/commissions/pagination.test.tsapps/web/tests/customers/pagination.test.tsapps/web/tests/links/list-links.test.tsapps/web/tests/utils/helpers.tspackages/prisma/schema/commission.prismapackages/prisma/schema/customer.prismapackages/prisma/schema/link.prisma
…ema and buildPaginationQuery
|
@coderabbitai full review please |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
apps/web/tests/links/list-links.test.ts (2)
49-75:⚠️ Potential issue | 🟠 MajorKeep the suite callback synchronous.
describe.concurrentshould not be wrapped in an async callback here. All async setup is already inbeforeAll, so the async suite wrapper is just invalid/fragile Vitest usage.🔧 Minimal fix
-describe.concurrent("/links/** - pagination", async () => { +describe.concurrent("/links/** - pagination", () => {Does Vitest allow an async callback for `describe` / `describe.concurrent`, or should async suite setup live in `beforeAll` with a synchronous describe callback?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/links/list-links.test.ts` around lines 49 - 75, The test suite callback passed to describe.concurrent is declared async which is incorrect; make the describe.concurrent callback synchronous and move all asynchronous work into beforeAll (which already contains the http init and baseline setup). Locate the describe.concurrent(...) function wrapper and change its callback to a normal synchronous function, leaving beforeAll(async () => { ... }) unchanged so all async initialization stays in beforeAll and tests run correctly.
96-120:⚠️ Potential issue | 🟡 MinorAssert the exact cursor window.
These two tests still pass if
/linksskips or duplicates rows, because they only checklengthand descendingids. Sincebaselineis built fromcommonQuery, use the same query shape for the cursor requests and compare the returned ids to the expected slice.🔧 Suggested tightening
test("Cursor forward (startingAfter)", async () => { const firstPage = baseline.slice(0, 5); const lastId = firstPage[4].id; const { status, data } = await http.get<Link[]>({ path: "/links", - query: { pageSize: "5", startingAfter: lastId }, + query: { ...commonQuery, startingAfter: lastId }, }); expect(status).toEqual(200); - expect(data).toHaveLength(5); - expectSortedById(data, "desc"); + expect(data.map((l) => l.id)).toEqual(baselineIds.slice(5, 10)); }); test("Cursor backward (endingBefore)", async () => { const beforeId = baseline[5].id; const { status, data } = await http.get<Link[]>({ path: "/links", - query: { pageSize: "5", endingBefore: beforeId }, + query: { ...commonQuery, endingBefore: beforeId }, }); expect(status).toEqual(200); - expect(data).toHaveLength(5); - expectSortedById(data, "desc"); + expect(data.map((l) => l.id)).toEqual(baselineIds.slice(0, 5)); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/links/list-links.test.ts` around lines 96 - 120, Update the two cursor tests ("Cursor forward (startingAfter)" and "Cursor backward (endingBefore)") to assert the exact window of ids returned instead of only length and sort: build the expected id slice from baseline using the same query shape (as used when creating baseline via commonQuery), call http.get<Link[]> with the identical query parameters (pageSize and startingAfter/endingBefore), then compare the array of returned ids to the expected slice of baseline.ids to ensure no rows are skipped or duplicated; reference the test names, baseline array, and the http.get call to locate and change the assertions.apps/web/lib/customers/api/get-customers.ts (1)
27-49:⚠️ Potential issue | 🟠 MajorValidate the cursor against the full filtered query.
A customer from the same workspace but outside the current
programId/partnerId/externalId/country/linkId/customerIdsfilter still passes this check. That makes the cursor boundary inconsistent with the result set the client is actually paging through. Build thewhereobject once and reuse it for both the cursor lookup andfindMany(withfindFirsthere, sincefindUniquecan't combine the extra filters).🔧 Suggested shape
- const paginationQuery = buildPaginationQuery(filters); + const where = { + // reuse the existing filters currently passed to findMany + }; + const paginationQuery = buildPaginationQuery(filters); if (cursorId) { - const customer = await prisma.customer.findUnique({ - where: { - id: cursorId, - }, - select: { - id: true, - projectId: true, - }, + const customer = await prisma.customer.findFirst({ + where: { + AND: [where, { id: cursorId }], + }, + select: { + id: true, + }, }); - if (!customer || customer.projectId !== workspaceId) { + if (!customer) { throw new DubApiError({ code: "unprocessable_entity", - message: "Invalid cursor: the provided ID does not exist.", + message: "Invalid cursor for the current query.", }); } } return await prisma.customer.findMany({ - where: { - // existing filters - }, + where, ...paginationQuery,Does Prisma Client `findUnique` support combining a unique field with arbitrary non-unique filters in `where`, or should query-scoped existence checks use `findFirst`/`findMany` with combined filters?Based on learnings, the codebase prefers enforcing invariants directly in Prisma
whereclauses.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/customers/api/get-customers.ts` around lines 27 - 49, The cursor validation currently looks up the ID with prisma.customer.findUnique using only projectId and thus can pass for a customer outside the active filters; instead build the full Prisma where object once (the same filters used to create paginationQuery) and reuse it for both the cursor check and the final findMany, and use prisma.customer.findFirst (not findUnique) to validate that a record with id === cursorId exists within that combined where (including workspace/projectId plus programId, partnerId, email, externalId, country, linkId, customerIds, etc.); update the validation logic around cursorId/startingAfter/endingBefore to call findFirst with that shared where and throw the existing DubApiError if no matching row is found.
🧹 Nitpick comments (2)
apps/web/tests/utils/helpers.ts (1)
75-93: Assert theidtie-breaker in thecreatedAthelpers.These helpers only verify monotonic timestamps. If two rows share the same
createdAt, the pagination tests can still pass even when the secondaryidordering regresses and pages start duplicating or skipping records. That's the exact edge case this PR is trying to stabilize.♻️ Possible tightening
-export function expectSortedByCreatedAt<T extends { createdAt: string | Date }>( +export function expectSortedByCreatedAt< + T extends { id: string; createdAt: string | Date }, +>( items: T[], ) { for (let i = 0; i < items.length - 1; i++) { const a = new Date(items[i].createdAt).getTime(); const b = new Date(items[i + 1].createdAt).getTime(); - expect(a).toBeGreaterThanOrEqual(b); + if (a === b) { + expect(items[i].id.localeCompare(items[i + 1].id)).toBeGreaterThanOrEqual(0); + } else { + expect(a).toBeGreaterThanOrEqual(b); + } } } export function expectSortedByCreatedAtAsc< - T extends { createdAt: string | Date }, + T extends { id: string; createdAt: string | Date }, >(items: T[]) { for (let i = 0; i < items.length - 1; i++) { const a = new Date(items[i].createdAt).getTime(); const b = new Date(items[i + 1].createdAt).getTime(); - expect(a).toBeLessThanOrEqual(b); + if (a === b) { + expect(items[i].id.localeCompare(items[i + 1].id)).toBeLessThanOrEqual(0); + } else { + expect(a).toBeLessThanOrEqual(b); + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/utils/helpers.ts` around lines 75 - 93, Update the two helpers expectSortedByCreatedAt and expectSortedByCreatedAtAsc to assert a deterministic secondary ordering by id when createdAt timestamps are equal: after computing a and b, if a === b compare items[i].id and items[i + 1].id (coercing to the same type if needed) and assert the id ordering matches the overall comparator (for expectSortedByCreatedAt use id >= next id, for expectSortedByCreatedAtAsc use id <= next id); keep the timestamp checks as-is and only perform the id assertion when timestamps are equal so tied createdAt values cannot regress across pages.apps/web/lib/api/pagination.ts (1)
80-88: Consider addingidas a tiebreaker for offset pagination.For offset pagination, when multiple records share the same
sortByvalue (e.g., samecreatedAt), the order is non-deterministic. Addingidas a secondary sort ensures stable pagination across pages.♻️ Suggested improvement
return { - // Order by id only for better query performance on large datasets (single-column PK index). - // Trade-off: ordering is by id rather than createdAt, so order may not strictly match creation time. - orderBy: { - [sortBy]: sortOrder, - }, + orderBy: [{ [sortBy]: sortOrder }, { id: sortOrder }], take: pageSize, skip: (page - 1) * pageSize, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/api/pagination.ts` around lines 80 - 88, The current offset pagination returns orderBy: { [sortBy]: sortOrder } which is non-deterministic when multiple rows share the same sortBy value; update the return to include id as a secondary tiebreaker so ordering is stable—e.g. set orderBy to include both the primary key and id (for Prisma: [{ [sortBy]: sortOrder }, { id: sortOrder }], or if using an object form add id: sortOrder) while keeping take: pageSize and skip: (page - 1) * pageSize unchanged; use the existing identifiers sortBy, sortOrder, orderBy, pageSize, and page when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/lib/zod/schemas/commissions.ts`:
- Around line 124-132: The derived schemas (via the .extend that spreads
getCursorPaginationQuerySchema and getPaginationQuerySchema) are currently
adding cursor fields (startingAfter, endingBefore) into endpoints that shouldn't
paginate; update the derivations used to build getCommissionsCountQuerySchema
and commissionsExportQuerySchema so they omit cursor params: remove or avoid
spreading getCursorPaginationQuerySchema into those extends (or explicitly pick
only page/pageSize from getPaginationQuerySchema and exclude
startingAfter/endingBefore) so that getCommissionsCountQuerySchema and
commissionsExportQuerySchema do not include startingAfter/endingBefore; look for
the .extend call that combines getCursorPaginationQuerySchema and
getPaginationQuerySchema and change it for the count/export schema constructions
accordingly.
In `@apps/web/tests/commissions/pagination.test.ts`:
- Around line 10-35: The test suite callback passed to describe.concurrent is
marked async but should be synchronous; remove the async keyword from the
describe.concurrent callback (leaving the body and hooks like beforeAll, the
IntegrationHarness initialization, and variables http, baseline, baselineIds
untouched) so the suite definition is registered synchronously while keeping
async setup inside beforeAll.
In `@apps/web/tests/customers/pagination.test.ts`:
- Around line 57-81: The cursor tests ("Cursor forward (startingAfter)" and
"Cursor backward (endingBefore)") only check ordering; update each http.get call
to include the shared commonQuery and then assert the returned ids exactly match
the expected window from baselineIds (e.g., for startingAfter use
baselineIds.slice(5,10) or for endingBefore use the corresponding slice) instead
of only checking length/order; locate the tests and modify the requests that
call http.get<Customer[]> and the assertions that use expectSortedById to
compare the returned id array to the exact expected slice from baselineIds.
- Around line 10-36: The test suite callback passed to describe.concurrent is
marked async but Vitest expects a synchronous suite body; remove the async
keyword from the describe.concurrent callback (the anonymous function that
contains the IntegrationHarness setup, http, baseline variables and the
beforeAll block) so the suite callback is synchronous—leave the existing async
work inside beforeAll as-is (where http is initialized and baseline/baselineIds
are assigned) and ensure only the callback signature for describe.concurrent is
changed.
---
Duplicate comments:
In `@apps/web/lib/customers/api/get-customers.ts`:
- Around line 27-49: The cursor validation currently looks up the ID with
prisma.customer.findUnique using only projectId and thus can pass for a customer
outside the active filters; instead build the full Prisma where object once (the
same filters used to create paginationQuery) and reuse it for both the cursor
check and the final findMany, and use prisma.customer.findFirst (not findUnique)
to validate that a record with id === cursorId exists within that combined where
(including workspace/projectId plus programId, partnerId, email, externalId,
country, linkId, customerIds, etc.); update the validation logic around
cursorId/startingAfter/endingBefore to call findFirst with that shared where and
throw the existing DubApiError if no matching row is found.
In `@apps/web/tests/links/list-links.test.ts`:
- Around line 49-75: The test suite callback passed to describe.concurrent is
declared async which is incorrect; make the describe.concurrent callback
synchronous and move all asynchronous work into beforeAll (which already
contains the http init and baseline setup). Locate the describe.concurrent(...)
function wrapper and change its callback to a normal synchronous function,
leaving beforeAll(async () => { ... }) unchanged so all async initialization
stays in beforeAll and tests run correctly.
- Around line 96-120: Update the two cursor tests ("Cursor forward
(startingAfter)" and "Cursor backward (endingBefore)") to assert the exact
window of ids returned instead of only length and sort: build the expected id
slice from baseline using the same query shape (as used when creating baseline
via commonQuery), call http.get<Link[]> with the identical query parameters
(pageSize and startingAfter/endingBefore), then compare the array of returned
ids to the expected slice of baseline.ids to ensure no rows are skipped or
duplicated; reference the test names, baseline array, and the http.get call to
locate and change the assertions.
---
Nitpick comments:
In `@apps/web/lib/api/pagination.ts`:
- Around line 80-88: The current offset pagination returns orderBy: { [sortBy]:
sortOrder } which is non-deterministic when multiple rows share the same sortBy
value; update the return to include id as a secondary tiebreaker so ordering is
stable—e.g. set orderBy to include both the primary key and id (for Prisma: [{
[sortBy]: sortOrder }, { id: sortOrder }], or if using an object form add id:
sortOrder) while keeping take: pageSize and skip: (page - 1) * pageSize
unchanged; use the existing identifiers sortBy, sortOrder, orderBy, pageSize,
and page when applying the change.
In `@apps/web/tests/utils/helpers.ts`:
- Around line 75-93: Update the two helpers expectSortedByCreatedAt and
expectSortedByCreatedAtAsc to assert a deterministic secondary ordering by id
when createdAt timestamps are equal: after computing a and b, if a === b compare
items[i].id and items[i + 1].id (coercing to the same type if needed) and assert
the id ordering matches the overall comparator (for expectSortedByCreatedAt use
id >= next id, for expectSortedByCreatedAtAsc use id <= next id); keep the
timestamp checks as-is and only perform the id assertion when timestamps are
equal so tied createdAt values cannot regress across pages.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e9a56197-3196-4617-bbee-2802e3705f52
📒 Files selected for processing (35)
apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.tsapps/web/app/(ee)/api/campaigns/route.tsapps/web/app/(ee)/api/fraud/events/route.tsapps/web/app/(ee)/api/fraud/groups/route.tsapps/web/app/(ee)/api/network/programs/route.tsapps/web/app/(ee)/api/partner-profile/payouts/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/customers/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/referrals/route.tsapps/web/app/(ee)/api/payouts/route.tsapps/web/app/(ee)/api/programs/[programId]/referrals/route.tsapps/web/app/api/domains/route.tsapps/web/app/api/tags/route.tsapps/web/lib/api/campaigns/get-campaign-events.tsapps/web/lib/api/commissions/get-commissions.tsapps/web/lib/api/groups/get-groups.tsapps/web/lib/api/links/get-links-for-workspace.tsapps/web/lib/api/network/calculate-partner-ranking.tsapps/web/lib/api/pagination.tsapps/web/lib/api/partners/get-partners.tsapps/web/lib/api/payouts/get-eligible-payouts.tsapps/web/lib/customers/api/get-customers.tsapps/web/lib/openapi/commissions/list-commissions.tsapps/web/lib/openapi/customers/get-customers.tsapps/web/lib/zod/schemas/commissions.tsapps/web/lib/zod/schemas/customers.tsapps/web/lib/zod/schemas/links.tsapps/web/lib/zod/schemas/misc.tsapps/web/tests/commissions/pagination.test.tsapps/web/tests/customers/pagination.test.tsapps/web/tests/links/list-links.test.tsapps/web/tests/utils/helpers.tspackages/prisma/schema/commission.prismapackages/prisma/schema/customer.prismapackages/prisma/schema/link.prisma
|
@coderabbitai full review, any breaking changes? |
🧩 Analysis chainLet me dig into the codebase to assess the breaking changes properly. 🏁 Script executed: Length of output: 12284
Breaking Changes1. 🔴 Hard Break — Offset Pagination Cap (
|
| Schema | Change |
|---|---|
link.prisma |
Added id to composite index: [projectId, folderId, archived, createdAt(Desc), id] |
customer.prisma |
Added id to composite index: [projectId, createdAt, id] |
commission.prisma |
New index added: [programId, createdAt, id] |
These are purely additive index changes (no column drops, no uniqueness constraint changes), but the migrations must be applied to avoid query failures when the new pagination cursor logic runs.
3. 🟡 Soft Break — Deprecated page / pageSize Params
For Links, Customers, and Commissions, the page and pageSize query parameters are now marked deprecated: true in the OpenAPI schema. They still function, but SDK-generated clients (e.g., Speakeasy) will surface deprecation warnings to consumers.
Non-Breaking Notes
getLinksForWorkspacesignature change: Internally refactored fromgetLinksForWorkspace({ ... }: Props)togetLinksForWorkspace(filters: Props). All 3 call sites (links/route.ts,links/export/route.ts,fetch-links-batch.ts) use object-spread syntax and are unaffected.- Cursor
orderByusesid, notcreatedAt: Even whensortBy: "createdAt"is specified with cursor mode, the actual DB ordering isorderBy: { id: sortOrder }. The code comment acknowledges this: "ordering is by id rather than createdAt, so order may not strictly match creation time." This is only relevant to new cursor pagination consumers, not existing ones. - New 422 validations for cursor misuse (both cursors simultaneously, cursor + page, cursor + non-
createdAtsort) are all gated behind the newstartingAfter/endingBeforeparams, so they can't affect existing consumers.
TL;DR: The main risk is the page > 1000 cap for existing deep-paginating consumers and the database migrations that must be run on deploy. Everything else is additive or soft-deprecated.
✅ Actions performed
Full review triggered.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
apps/web/tests/commissions/pagination.test.ts (1)
3-7:⚠️ Potential issue | 🟠 MajorThese cursor expectations still bake in
idordering.The
expectedIdscalculation re-filters and re-sortsbaselinebyid, so this suite still passes if/commissionspaginates byidinstead of the requestedcreatedAtorder. Compare against the exact window frombaselineIdsand keep the assertions aligned withcreatedAt; otherwise the test codifies the current bug.♻️ Tighten the expectations to the baseline window
import { expectNoOverlap, expectSortedByCreatedAt, - expectSortedById, } from "../utils/helpers"; @@ test("Cursor forward (startingAfter)", async () => { const firstPage = baseline.slice(0, 5); const lastId = firstPage[4].id; - const expectedIds = baseline - .filter((b) => b.id < lastId) - .sort((a, b) => b.id.localeCompare(a.id)) - .slice(0, 5) - .map((b) => b.id); + const expectedIds = baselineIds.slice(5, 10); const { status, data } = await http.get<CommissionResponse[]>({ path: "/commissions", query: { ...commonQuery, @@ expect(status).toEqual(200); expect(data.map((d) => d.id)).toEqual(expectedIds); - expectSortedById(data, "desc"); + expectSortedByCreatedAt(data); }); test("Cursor backward (endingBefore)", async () => { const beforeId = baseline[5].id; - const expectedIds = baseline - .filter((b) => b.id > beforeId) - .sort((a, b) => b.id.localeCompare(a.id)) - .slice(0, 5) - .map((b) => b.id); + const expectedIds = baselineIds.slice(0, 5); const { status, data } = await http.get<CommissionResponse[]>({ path: "/commissions", query: { ...commonQuery, @@ expect(status).toEqual(200); expect(data.map((d) => d.id)).toEqual(expectedIds); - expectSortedById(data, "desc"); + expectSortedByCreatedAt(data); });Also applies to: 56-99
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/commissions/pagination.test.ts` around lines 3 - 7, The test currently recomputes expectedIds by re-filtering and re-sorting baseline data by id which hides a bug where the API might be paginating by id instead of createdAt; update the expectedIds calculation in the pagination tests (the logic around baseline, baselineIds and expectedIds used by expectSortedByCreatedAt / expectNoOverlap) to take the exact window slice from baselineIds (which is already sorted by createdAt) rather than re-sorting by id, and ensure the assertions call expectSortedByCreatedAt against that baseline window; apply the same change to the other occurrences in the file (the block around lines 56-99) so all cursor-window expectations are aligned to createdAt ordering.apps/web/lib/zod/schemas/customers.ts (1)
365-370:⚠️ Potential issue | 🟡 MinorOmit cursor params from the export schema too.
customersExportQuerySchemastill inheritsstartingAfter/endingBeforefromgetCustomersQuerySchema, even though exports are not paginated. That leaks dead query params into the public schema, andcustomersExportCronInputSchemapicks them up as well.♻️ Minimal fix
export const customersExportQuerySchema = getCustomersQuerySchema .omit({ page: true, pageSize: true, includeExpandedFields: true, + startingAfter: true, + endingBefore: true, }) .extend({🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/zod/schemas/customers.ts` around lines 365 - 370, customersExportQuerySchema currently inherits cursor params from getCustomersQuerySchema; update the omit call on customersExportQuerySchema to also remove the cursor fields (startingAfter and endingBefore) so exports don't expose pagination params, and verify customersExportCronInputSchema (which derives from customersExportQuerySchema) no longer includes those cursor keys.apps/web/tests/links/list-links.test.ts (1)
4-8:⚠️ Potential issue | 🟠 MajorAssert the
createdAtcursor window, not just localidorder.Both cursor tests still pass if the handler paginates by
idinstead of thecreatedAt-ordered baseline. Reuse...commonQuery, compare against the exactbaselineIds.slice(...)window, and assertcreatedAtordering so the suite actually covers the cursor boundary.♻️ Tighten the cursor assertions
import { expectNoOverlap, expectSortedByCreatedAt, - expectSortedById, } from "../utils/helpers"; @@ test("Cursor forward (startingAfter)", async () => { const firstPage = baseline.slice(0, 5); const lastId = firstPage[4].id; const { status, data } = await http.get<Link[]>({ path: "/links", - query: { pageSize: "5", startingAfter: lastId }, + query: { ...commonQuery, startingAfter: lastId }, }); expect(status).toEqual(200); expect(data).toHaveLength(5); - expectSortedById(data, "desc"); + expect(data.map((l) => l.id)).toEqual(baselineIds.slice(5, 10)); + expectSortedByCreatedAt(data); }); test("Cursor backward (endingBefore)", async () => { const beforeId = baseline[5].id; const { status, data } = await http.get<Link[]>({ path: "/links", - query: { pageSize: "5", endingBefore: beforeId }, + query: { ...commonQuery, endingBefore: beforeId }, }); expect(status).toEqual(200); expect(data).toHaveLength(5); - expectSortedById(data, "desc"); + expect(data.map((l) => l.id)).toEqual(baselineIds.slice(0, 5)); + expectSortedByCreatedAt(data); });Also applies to: 96-121
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/tests/links/list-links.test.ts` around lines 4 - 8, Update the cursor tests in list-links.test.ts to validate the createdAt-based cursor window rather than only local id order: reuse the existing commonQuery for requests, compute the expected window from baselineIds.slice(...) and assert the handler's returned ids exactly match that slice, and replace or augment expectSortedById with expectSortedByCreatedAt to ensure results are ordered by createdAt; also keep expectNoOverlap to validate pagination boundaries. Locate the test cases that call commonQuery and reference baselineIds, then change the assertions to compare the full returned id array to baselineIds.slice(start, end) and call expectSortedByCreatedAt(response) instead of expectSortedById where appropriate.
🧹 Nitpick comments (1)
apps/web/lib/api/links/get-links-for-workspace.ts (1)
43-46: Mutating thefiltersparameter may cause unexpected side effects.The reassignment
filters = { ...filters, sortBy: sort }creates a new object, but the function signature impliesfiltersshould be treated as input-only. While this works, consider using a local variable for clarity.♻️ Suggested refinement
// Support legacy sort param if (sort && sort !== "createdAt") { - filters = { ...filters, sortBy: sort }; + const effectiveFilters = { ...filters, sortBy: sort }; + // use effectiveFilters for buildPaginationQuery } + const paginationQuery = buildPaginationQuery( + sort && sort !== "createdAt" ? { ...filters, sortBy: sort } : filters + );Or inline the override:
- // Support legacy sort param - if (sort && sort !== "createdAt") { - filters = { ...filters, sortBy: sort }; - } - - const paginationQuery = buildPaginationQuery(filters); + // Support legacy sort param + const paginationQuery = buildPaginationQuery( + sort && sort !== "createdAt" ? { ...filters, sortBy: sort } : filters + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/api/links/get-links-for-workspace.ts` around lines 43 - 46, The code mutates the input parameter `filters` by reassigning it when handling the legacy `sort` param; instead create a new local variable (e.g. `effectiveFilters` or `updatedFilters`) and assign `{ ...filters, sortBy: sort }` to that, then use that local variable in downstream calls (or inline the override where `filters` is consumed) so the original `filters` parameter remains untouched; update references to use the new local variable rather than reassigning `filters`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/lib/customers/api/get-customers.ts`:
- Line 27: The cursor pagination is using id-only ordering which can diverge
from createdAt sorting; update the pagination logic so when sortBy ===
"createdAt" (used by this endpoint) the cursor branch produces a deterministic
composite ordering and cursor position: orderBy should be [{ createdAt: dir }, {
id: dir }] and the cursor value must include both createdAt and id (not just id)
so that the object passed into findMany via paginationQuery uses createdAt+id
consistently; modify buildPaginationQuery (or override paginationQuery in
get-customers.ts) to emit this composite cursor and ordering when sortBy is
"createdAt".
In `@apps/web/lib/zod/schemas/misc.ts`:
- Around line 33-52: getPaginationQuerySchema currently allows any positive page
number while runtime enforces a hard cap; add a maxPage numeric parameter (e.g.,
getPaginationQuerySchema({ pageSize, deprecated = false, maxPage })) and apply
it to the page schema using z.coerce.number().positive().max(maxPage, { message:
`Page must be <= ${maxPage}.` }) so the OpenAPI/docs and runtime bounds match;
update the page .meta() or .describe() to include the max value and ensure
callers (places that call getPaginationQuerySchema) pass the agreed max (the
same constant used by buildPaginationQuery()).
In `@packages/prisma/schema/customer.prisma`:
- Line 40: The existing index @@index([projectId, createdAt, id]) doesn’t match
the cursor branch used by getCustomers() (built via buildPaginationQuery())
because that path orders by id only; add a composite index that has projectId
followed immediately by id (for example add @@index([projectId, id]) on the
Customer model) so MySQL can use the index for WHERE projectId = ? ORDER BY id;
update the Prisma schema (Customer model) accordingly and run prisma migrate to
apply the change.
In `@packages/prisma/schema/link.prisma`:
- Line 99: The composite index in link.prisma currently has createdAt before id
which prevents MySQL from using it for the cursor branch that orders by id
alone; update the schema to add or replace an index that places id before
createdAt so the cursor path can be satisfied — e.g., add an @@index([...]) for
the same filter prefix but with id (then createdAt(sort: Desc) if still needed)
such as [projectId, folderId, archived, id, createdAt(sort: Desc)] (or add a
separate index [projectId, folderId, archived, id]) to cover
getLinksForWorkspace() and buildPaginationQuery() cursor ordering.
---
Duplicate comments:
In `@apps/web/lib/zod/schemas/customers.ts`:
- Around line 365-370: customersExportQuerySchema currently inherits cursor
params from getCustomersQuerySchema; update the omit call on
customersExportQuerySchema to also remove the cursor fields (startingAfter and
endingBefore) so exports don't expose pagination params, and verify
customersExportCronInputSchema (which derives from customersExportQuerySchema)
no longer includes those cursor keys.
In `@apps/web/tests/commissions/pagination.test.ts`:
- Around line 3-7: The test currently recomputes expectedIds by re-filtering and
re-sorting baseline data by id which hides a bug where the API might be
paginating by id instead of createdAt; update the expectedIds calculation in the
pagination tests (the logic around baseline, baselineIds and expectedIds used by
expectSortedByCreatedAt / expectNoOverlap) to take the exact window slice from
baselineIds (which is already sorted by createdAt) rather than re-sorting by id,
and ensure the assertions call expectSortedByCreatedAt against that baseline
window; apply the same change to the other occurrences in the file (the block
around lines 56-99) so all cursor-window expectations are aligned to createdAt
ordering.
In `@apps/web/tests/links/list-links.test.ts`:
- Around line 4-8: Update the cursor tests in list-links.test.ts to validate the
createdAt-based cursor window rather than only local id order: reuse the
existing commonQuery for requests, compute the expected window from
baselineIds.slice(...) and assert the handler's returned ids exactly match that
slice, and replace or augment expectSortedById with expectSortedByCreatedAt to
ensure results are ordered by createdAt; also keep expectNoOverlap to validate
pagination boundaries. Locate the test cases that call commonQuery and reference
baselineIds, then change the assertions to compare the full returned id array to
baselineIds.slice(start, end) and call expectSortedByCreatedAt(response) instead
of expectSortedById where appropriate.
---
Nitpick comments:
In `@apps/web/lib/api/links/get-links-for-workspace.ts`:
- Around line 43-46: The code mutates the input parameter `filters` by
reassigning it when handling the legacy `sort` param; instead create a new local
variable (e.g. `effectiveFilters` or `updatedFilters`) and assign `{ ...filters,
sortBy: sort }` to that, then use that local variable in downstream calls (or
inline the override where `filters` is consumed) so the original `filters`
parameter remains untouched; update references to use the new local variable
rather than reassigning `filters`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cee50609-09e5-4c8e-8205-9336bec10c00
📒 Files selected for processing (39)
apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.tsapps/web/app/(ee)/api/campaigns/route.tsapps/web/app/(ee)/api/fraud/events/route.tsapps/web/app/(ee)/api/fraud/groups/route.tsapps/web/app/(ee)/api/network/programs/route.tsapps/web/app/(ee)/api/partner-profile/payouts/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/customers/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/earnings/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/referrals/route.tsapps/web/app/(ee)/api/payouts/route.tsapps/web/app/(ee)/api/programs/[programId]/referrals/route.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/(index)/page-client.tsxapps/web/app/api/domains/route.tsapps/web/app/api/tags/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commissions-table.tsxapps/web/lib/api/campaigns/get-campaign-events.tsapps/web/lib/api/commissions/get-commissions.tsapps/web/lib/api/groups/get-groups.tsapps/web/lib/api/links/get-links-for-workspace.tsapps/web/lib/api/network/calculate-partner-ranking.tsapps/web/lib/api/pagination.tsapps/web/lib/api/partners/get-partners.tsapps/web/lib/api/payouts/get-eligible-payouts.tsapps/web/lib/customers/api/get-customers.tsapps/web/lib/openapi/commissions/list-commissions.tsapps/web/lib/openapi/customers/get-customers.tsapps/web/lib/zod/schemas/commissions.tsapps/web/lib/zod/schemas/customers.tsapps/web/lib/zod/schemas/links.tsapps/web/lib/zod/schemas/misc.tsapps/web/tests/commissions/pagination.test.tsapps/web/tests/customers/pagination.test.tsapps/web/tests/links/list-links.test.tsapps/web/tests/utils/helpers.tsapps/web/ui/customers/customers-table/customers-table.tsxapps/web/ui/links/links-container.tsxpackages/prisma/schema/commission.prismapackages/prisma/schema/customer.prismapackages/prisma/schema/link.prisma
Summary by CodeRabbit
New Features
Bug Fixes
Performance
Tests
Documentation