From 15e01167f3dc4c0e6a814c1ed4de0ceaf975ca3a Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Mon, 29 Jun 2026 16:45:19 -0300 Subject: [PATCH 1/4] fix duplicate partners across paginated pages --- apps/web/lib/api/pagination.ts | 6 +-- apps/web/lib/api/partners/get-partners.ts | 16 ++++-- apps/web/tests/partners/pagination.test.ts | 61 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 apps/web/tests/partners/pagination.test.ts diff --git a/apps/web/lib/api/pagination.ts b/apps/web/lib/api/pagination.ts index 4ef11ae3f23..f1de3dd714a 100644 --- a/apps/web/lib/api/pagination.ts +++ b/apps/web/lib/api/pagination.ts @@ -78,11 +78,7 @@ export function buildPaginationQuery(filters: Filters): PaginationQuery { } 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, }; diff --git a/apps/web/lib/api/partners/get-partners.ts b/apps/web/lib/api/partners/get-partners.ts index 8764538304a..6eae5573364 100644 --- a/apps/web/lib/api/partners/get-partners.ts +++ b/apps/web/lib/api/partners/get-partners.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma"; import { getPartnersQuerySchemaExtended } from "@/lib/zod/schemas/partners"; import { toCentsNumber } from "@dub/utils"; import * as z from "zod/v4"; +import { buildPaginationQuery } from "../pagination"; import { buildProgramEnrollmentWhereForList } from "./program-enrollment-query"; type PartnerFilters = z.infer & { @@ -24,6 +25,13 @@ export async function getPartners(filters: PartnerFilters) { ...enrollmentRest } = filters; + const { take, skip, orderBy } = buildPaginationQuery({ + page, + pageSize, + sortBy, + sortOrder, + }); + const partners = await prisma.programEnrollment.findMany({ where: buildProgramEnrollmentWhereForList({ ...enrollmentRest, @@ -54,11 +62,9 @@ export async function getPartners(filters: PartnerFilters) { } : {}), }, - take: pageSize, - skip: (page - 1) * pageSize, - orderBy: { - [sortBy]: sortOrder, - }, + take, + skip, + orderBy, }); return partners.map( diff --git a/apps/web/tests/partners/pagination.test.ts b/apps/web/tests/partners/pagination.test.ts new file mode 100644 index 00000000000..8399c2608ea --- /dev/null +++ b/apps/web/tests/partners/pagination.test.ts @@ -0,0 +1,61 @@ +import { EnrolledPartnerProps } from "@/lib/types"; +import { beforeAll, describe, expect, test } from "vitest"; +import { expectNoOverlap } from "../utils/helpers"; +import { IntegrationHarness } from "../utils/integration"; + +describe.concurrent("/partners/** - pagination", async () => { + const h = new IntegrationHarness(); + let http: IntegrationHarness["http"]; + let baseline: EnrolledPartnerProps[]; + let baselineIds: string[]; + + const commonQuery = { + pageSize: "5", + sortBy: "totalSaleAmount", + sortOrder: "desc", + }; + + beforeAll(async () => { + ({ http } = await h.init()); + + const { status, data } = await http.get({ + path: "/partners", + query: { ...commonQuery, pageSize: "100" }, + }); + + expect(status).toEqual(200); + + baseline = data; + baselineIds = baseline.map((p) => p.id); + }); + + test("Offset pagination has no duplicate partners across pages", async () => { + const page1 = await http.get({ + path: "/partners", + query: { ...commonQuery, page: "1" }, + }); + const page2 = await http.get({ + path: "/partners", + query: { ...commonQuery, page: "2" }, + }); + const page3 = await http.get({ + path: "/partners", + query: { ...commonQuery, page: "3" }, + }); + + expect(page1.status).toEqual(200); + expect(page2.status).toEqual(200); + expect(page3.status).toEqual(200); + + expectNoOverlap(page1.data, page2.data); + expectNoOverlap(page2.data, page3.data); + expectNoOverlap(page1.data, page3.data); + + const paginatedIds = [...page1.data, ...page2.data, ...page3.data].map( + (p) => p.id, + ); + + expect(new Set(paginatedIds).size).toBe(paginatedIds.length); + expect(paginatedIds).toEqual(baselineIds.slice(0, paginatedIds.length)); + }); +}); From 2b593b5ece132b35be0cfc027ec91e8df6c61d1d Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Mon, 29 Jun 2026 17:16:06 -0300 Subject: [PATCH 2/4] code improvements --- apps/web/tests/partners/pagination.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/tests/partners/pagination.test.ts b/apps/web/tests/partners/pagination.test.ts index 8399c2608ea..c5e782a67d0 100644 --- a/apps/web/tests/partners/pagination.test.ts +++ b/apps/web/tests/partners/pagination.test.ts @@ -27,6 +27,7 @@ describe.concurrent("/partners/** - pagination", async () => { baseline = data; baselineIds = baseline.map((p) => p.id); + expect(baselineIds.length).toBeGreaterThanOrEqual(15); }); test("Offset pagination has no duplicate partners across pages", async () => { @@ -46,6 +47,9 @@ describe.concurrent("/partners/** - pagination", async () => { expect(page1.status).toEqual(200); expect(page2.status).toEqual(200); expect(page3.status).toEqual(200); + expect(page1.data).toHaveLength(5); + expect(page2.data).toHaveLength(5); + expect(page3.data).toHaveLength(5); expectNoOverlap(page1.data, page2.data); expectNoOverlap(page2.data, page3.data); From 0eb859f9f442337e4d811ecea90ddbf4f34bdc3a Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Wed, 1 Jul 2026 15:45:03 -0300 Subject: [PATCH 3/4] revert buildPaginationQuery --- apps/web/lib/api/pagination.ts | 4 +++- apps/web/lib/api/partners/get-partners.ts | 14 +++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/web/lib/api/pagination.ts b/apps/web/lib/api/pagination.ts index f1de3dd714a..b70f16a6c7b 100644 --- a/apps/web/lib/api/pagination.ts +++ b/apps/web/lib/api/pagination.ts @@ -78,7 +78,9 @@ export function buildPaginationQuery(filters: Filters): PaginationQuery { } return { - orderBy: [{ [sortBy]: sortOrder }, { id: sortOrder }], + orderBy: { + [sortBy]: sortOrder, + }, take: pageSize, skip: (page - 1) * pageSize, }; diff --git a/apps/web/lib/api/partners/get-partners.ts b/apps/web/lib/api/partners/get-partners.ts index 6eae5573364..7b0497005a1 100644 --- a/apps/web/lib/api/partners/get-partners.ts +++ b/apps/web/lib/api/partners/get-partners.ts @@ -2,7 +2,6 @@ import { prisma } from "@/lib/prisma"; import { getPartnersQuerySchemaExtended } from "@/lib/zod/schemas/partners"; import { toCentsNumber } from "@dub/utils"; import * as z from "zod/v4"; -import { buildPaginationQuery } from "../pagination"; import { buildProgramEnrollmentWhereForList } from "./program-enrollment-query"; type PartnerFilters = z.infer & { @@ -25,13 +24,6 @@ export async function getPartners(filters: PartnerFilters) { ...enrollmentRest } = filters; - const { take, skip, orderBy } = buildPaginationQuery({ - page, - pageSize, - sortBy, - sortOrder, - }); - const partners = await prisma.programEnrollment.findMany({ where: buildProgramEnrollmentWhereForList({ ...enrollmentRest, @@ -62,9 +54,9 @@ export async function getPartners(filters: PartnerFilters) { } : {}), }, - take, - skip, - orderBy, + take: pageSize, + skip: (page - 1) * pageSize, + orderBy: [{ [sortBy]: sortOrder }, { id: sortOrder }], }); return partners.map( From 49e420e431e9a5ab6596a703e94333aac85aa773 Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Wed, 1 Jul 2026 17:18:04 -0300 Subject: [PATCH 4/4] revent initial changes --- apps/web/lib/api/pagination.ts | 2 + apps/web/tests/partners/pagination.test.ts | 65 ---------------------- 2 files changed, 2 insertions(+), 65 deletions(-) delete mode 100644 apps/web/tests/partners/pagination.test.ts diff --git a/apps/web/lib/api/pagination.ts b/apps/web/lib/api/pagination.ts index b70f16a6c7b..4ef11ae3f23 100644 --- a/apps/web/lib/api/pagination.ts +++ b/apps/web/lib/api/pagination.ts @@ -78,6 +78,8 @@ export function buildPaginationQuery(filters: Filters): PaginationQuery { } 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, }, diff --git a/apps/web/tests/partners/pagination.test.ts b/apps/web/tests/partners/pagination.test.ts deleted file mode 100644 index c5e782a67d0..00000000000 --- a/apps/web/tests/partners/pagination.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EnrolledPartnerProps } from "@/lib/types"; -import { beforeAll, describe, expect, test } from "vitest"; -import { expectNoOverlap } from "../utils/helpers"; -import { IntegrationHarness } from "../utils/integration"; - -describe.concurrent("/partners/** - pagination", async () => { - const h = new IntegrationHarness(); - let http: IntegrationHarness["http"]; - let baseline: EnrolledPartnerProps[]; - let baselineIds: string[]; - - const commonQuery = { - pageSize: "5", - sortBy: "totalSaleAmount", - sortOrder: "desc", - }; - - beforeAll(async () => { - ({ http } = await h.init()); - - const { status, data } = await http.get({ - path: "/partners", - query: { ...commonQuery, pageSize: "100" }, - }); - - expect(status).toEqual(200); - - baseline = data; - baselineIds = baseline.map((p) => p.id); - expect(baselineIds.length).toBeGreaterThanOrEqual(15); - }); - - test("Offset pagination has no duplicate partners across pages", async () => { - const page1 = await http.get({ - path: "/partners", - query: { ...commonQuery, page: "1" }, - }); - const page2 = await http.get({ - path: "/partners", - query: { ...commonQuery, page: "2" }, - }); - const page3 = await http.get({ - path: "/partners", - query: { ...commonQuery, page: "3" }, - }); - - expect(page1.status).toEqual(200); - expect(page2.status).toEqual(200); - expect(page3.status).toEqual(200); - expect(page1.data).toHaveLength(5); - expect(page2.data).toHaveLength(5); - expect(page3.data).toHaveLength(5); - - expectNoOverlap(page1.data, page2.data); - expectNoOverlap(page2.data, page3.data); - expectNoOverlap(page1.data, page3.data); - - const paginatedIds = [...page1.data, ...page2.data, ...page3.data].map( - (p) => p.id, - ); - - expect(new Set(paginatedIds).size).toBe(paginatedIds.length); - expect(paginatedIds).toEqual(baselineIds.slice(0, paginatedIds.length)); - }); -});