Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/thick-ads-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@namehash/ens-referrals": minor
"@ensnode/ensnode-sdk": minor
"ensapi": minor
---

Renamed `itemsPerPage` to `recordsPerPage` and `paginationContext` to `pageContext` in referrer leaderboard APIs to align with registrar actions terminology.
34 changes: 17 additions & 17 deletions apps/ensapi/src/handlers/ensanalytics-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,23 @@ describe("/ensanalytics", () => {

// Arrange: create the test client from the app instance
const client = testClient(app);
const itemsPerPage = 10;
const recordsPerPage = 10;

// Act: send test request to fetch 1st page
const responsePage1 = await client.referrers
.$get({ query: { itemsPerPage: `${itemsPerPage}`, page: "1" } }, {})
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "1" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);

// Act: send test request to fetch 2nd page
const responsePage2 = await client.referrers
.$get({ query: { itemsPerPage: `${itemsPerPage}`, page: "2" } }, {})
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "2" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);

// Act: send test request to fetch 3rd page
const responsePage3 = await client.referrers
.$get({ query: { itemsPerPage: `${itemsPerPage}`, page: "3" } }, {})
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "3" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);

Expand All @@ -87,17 +87,17 @@ describe("/ensanalytics", () => {
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
data: {
...populatedReferrerLeaderboard,
paginationContext: {
pageContext: {
endIndex: 9,
hasNext: true,
hasPrev: false,
itemsPerPage: 10,
recordsPerPage: 10,
page: 1,
startIndex: 0,
totalPages: 3,
totalRecords: 29,
},
referrers: allPossibleReferrersIterator.take(itemsPerPage).toArray(),
referrers: allPossibleReferrersIterator.take(recordsPerPage).toArray(),
},
} satisfies ReferrerLeaderboardPageResponseOk;

Expand All @@ -108,17 +108,17 @@ describe("/ensanalytics", () => {
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
data: {
...populatedReferrerLeaderboard,
paginationContext: {
pageContext: {
endIndex: 19,
hasNext: true,
hasPrev: true,
itemsPerPage: 10,
recordsPerPage: 10,
page: 2,
startIndex: 10,
totalPages: 3,
totalRecords: 29,
},
referrers: allPossibleReferrersIterator.take(itemsPerPage).toArray(),
referrers: allPossibleReferrersIterator.take(recordsPerPage).toArray(),
},
} satisfies ReferrerLeaderboardPageResponseOk;
expect(responsePage2).toMatchObject(expectedResponsePage2);
Expand All @@ -128,17 +128,17 @@ describe("/ensanalytics", () => {
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
data: {
...populatedReferrerLeaderboard,
paginationContext: {
pageContext: {
endIndex: 28,
hasNext: false,
hasPrev: true,
itemsPerPage: 10,
recordsPerPage: 10,
page: 3,
startIndex: 20,
totalPages: 3,
totalRecords: 29,
},
referrers: allPossibleReferrersIterator.take(itemsPerPage).toArray(),
referrers: allPossibleReferrersIterator.take(recordsPerPage).toArray(),
},
} satisfies ReferrerLeaderboardPageResponseOk;
expect(responsePage3).toMatchObject(expectedResponsePage3);
Expand All @@ -155,11 +155,11 @@ describe("/ensanalytics", () => {

// Arrange: create the test client from the app instance
const client = testClient(app);
const itemsPerPage = 10;
const recordsPerPage = 10;

// Act: send test request to fetch 1st page
const response = await client.referrers
.$get({ query: { itemsPerPage: `${itemsPerPage}`, page: "1" } }, {})
.$get({ query: { recordsPerPage: `${recordsPerPage}`, page: "1" } }, {})
.then((r) => r.json())
.then(deserializeReferrerLeaderboardPageResponse);

Expand All @@ -168,10 +168,10 @@ describe("/ensanalytics", () => {
responseCode: ReferrerLeaderboardPageResponseCodes.Ok,
data: {
...emptyReferralLeaderboard,
paginationContext: {
pageContext: {
hasNext: false,
hasPrev: false,
itemsPerPage: 10,
recordsPerPage: 10,
page: 1,
totalPages: 1,
totalRecords: 0,
Expand Down
10 changes: 5 additions & 5 deletions apps/ensapi/src/handlers/ensanalytics-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ const logger = makeLogger("ensanalytics-api");
// Pagination query parameters schema (mirrors ReferrerLeaderboardPageRequest)
const paginationQuerySchema = z.object({
page: z.optional(z.coerce.number().int().min(1, "Page must be a positive integer")),
itemsPerPage: z.optional(
recordsPerPage: z.optional(
z.coerce
.number()
.int()
.min(1, "Items per page must be at least 1")
.min(1, "Records per page must be at least 1")
.max(
REFERRERS_PER_LEADERBOARD_PAGE_MAX,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we replace these constants for the referrer leaderboard page with the same constants we used for other related APIs? In other words, can we make it so that all our APIs that have pagination use the same constants for pagination rules?

`Items per page must not exceed ${REFERRERS_PER_LEADERBOARD_PAGE_MAX}`,
`Records per page must not exceed ${REFERRERS_PER_LEADERBOARD_PAGE_MAX}`,
),
),
}) satisfies z.ZodType<ReferrerLeaderboardPageRequest>;
Expand Down Expand Up @@ -65,9 +65,9 @@ const app = factory
);
}

const { page, itemsPerPage } = c.req.valid("query");
const { page, recordsPerPage } = c.req.valid("query");
const leaderboardPage = getReferrerLeaderboardPage(
{ page, itemsPerPage },
{ page, recordsPerPage },
referrerLeaderboard.value,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,9 +990,9 @@ export const referrerLeaderboardPageResponseOk: ReferrerLeaderboardPageResponseO
grandTotalQualifiedReferrersFinalScore: 16.55216891669386,
minFinalScoreToQualify: 0,
},
paginationContext: {
pageContext: {
page: 1,
itemsPerPage: 100,
recordsPerPage: 100,
totalRecords: 29,
totalPages: 1,
hasNext: false,
Expand Down
18 changes: 9 additions & 9 deletions packages/ens-referrals/src/leaderboard-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
import type { AwardedReferrerMetrics } from "./referrer-metrics.ts";

describe("buildReferrerLeaderboardPageContext", () => {
const paginationParams: ReferrerLeaderboardPageParams = {
const pageParams: ReferrerLeaderboardPageParams = {
page: 1,
itemsPerPage: 3,
recordsPerPage: 3,
};

it("correctly evaluates `hasNext` when `leaderboard.referrers.size` and `itemsPerPage` are equal", () => {
it("correctly evaluates `hasNext` when `leaderboard.referrers.size` and `recordsPerPage` are equal", () => {
const leaderboard: ReferrerLeaderboard = {
rules: {
totalAwardPoolValue: 10000,
Expand Down Expand Up @@ -84,15 +84,15 @@ describe("buildReferrerLeaderboardPageContext", () => {
};

const buildReferrerLeaderboardPageContextSpy = vi.fn(buildReferrerLeaderboardPageContext);
const result = buildReferrerLeaderboardPageContextSpy(paginationParams, leaderboard);
const result = buildReferrerLeaderboardPageContextSpy(pageParams, leaderboard);

expect(
buildReferrerLeaderboardPageContextSpy,
"buildReferrerLeaderboardPageContext should successfully complete for itemsPerPage=3, leaderboard.referrers.size=3",
"buildReferrerLeaderboardPageContext should successfully complete for recordsPerPage=3, leaderboard.referrers.size=3",
).toHaveReturned();
expect(
result.hasNext,
`Leaderboard should only have one page for itemsPerPage=3, leaderboard.referrers.size=3 (expected hasNext to be false, is ${result.hasNext})`,
`Leaderboard should only have one page for recordsPerPage=3, leaderboard.referrers.size=3 (expected hasNext to be false, is ${result.hasNext})`,
).toStrictEqual(false);
});

Expand Down Expand Up @@ -125,16 +125,16 @@ describe("buildReferrerLeaderboardPageContext", () => {
hasPrev: false,
startIndex: undefined,
endIndex: undefined,
itemsPerPage: 3,
recordsPerPage: 3,
page: 1,
};

const buildReferrerLeaderboardPageContextSpy = vi.fn(buildReferrerLeaderboardPageContext);
const result = buildReferrerLeaderboardPageContextSpy(paginationParams, leaderboard);
const result = buildReferrerLeaderboardPageContextSpy(pageParams, leaderboard);

expect(
buildReferrerLeaderboardPageContextSpy,
"buildReferrerLeaderboardPageContext should successfully complete for itemsPerPage=3, leaderboard.referrers.size=0",
"buildReferrerLeaderboardPageContext should successfully complete for recordsPerPage=3, leaderboard.referrers.size=0",
).toHaveReturned();

expect(
Expand Down
52 changes: 26 additions & 26 deletions packages/ens-referrals/src/leaderboard-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ReferrerLeaderboardPageParams {
* @invariant Must be a positive integer (>= 1) and less than or equal to {@link REFERRERS_PER_LEADERBOARD_PAGE_MAX}
* @default {@link REFERRERS_PER_LEADERBOARD_PAGE_DEFAULT}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be nice if we could standardize the use of this constant too. I think no need for a distinct constant for the referrer's leaderboard page.

*/
itemsPerPage?: number;
recordsPerPage?: number;
}

const validateReferrerLeaderboardPageParams = (params: ReferrerLeaderboardPageParams): void => {
Expand All @@ -41,17 +41,17 @@ const validateReferrerLeaderboardPageParams = (params: ReferrerLeaderboardPagePa
`Invalid ReferrerLeaderboardPageParams: ${params.page}. page must be a positive integer.`,
);
}
if (params.itemsPerPage !== undefined && !isPositiveInteger(params.itemsPerPage)) {
if (params.recordsPerPage !== undefined && !isPositiveInteger(params.recordsPerPage)) {
throw new Error(
`Invalid ReferrerLeaderboardPageParams: ${params.itemsPerPage}. itemsPerPage must be a positive integer.`,
`Invalid ReferrerLeaderboardPageParams: ${params.recordsPerPage}. recordsPerPage must be a positive integer.`,
);
}
if (
params.itemsPerPage !== undefined &&
params.itemsPerPage > REFERRERS_PER_LEADERBOARD_PAGE_MAX
params.recordsPerPage !== undefined &&
params.recordsPerPage > REFERRERS_PER_LEADERBOARD_PAGE_MAX
) {
throw new Error(
`Invalid ReferrerLeaderboardPageParams: ${params.itemsPerPage}. itemsPerPage must be less than or equal to ${REFERRERS_PER_LEADERBOARD_PAGE_MAX}.`,
`Invalid ReferrerLeaderboardPageParams: ${params.recordsPerPage}. recordsPerPage must be less than or equal to ${REFERRERS_PER_LEADERBOARD_PAGE_MAX}.`,
);
}
};
Expand All @@ -61,7 +61,7 @@ export const buildReferrerLeaderboardPageParams = (
): Required<ReferrerLeaderboardPageParams> => {
const result = {
page: params.page ?? 1,
itemsPerPage: params.itemsPerPage ?? REFERRERS_PER_LEADERBOARD_PAGE_DEFAULT,
recordsPerPage: params.recordsPerPage ?? REFERRERS_PER_LEADERBOARD_PAGE_DEFAULT,
} satisfies Required<ReferrerLeaderboardPageParams>;
validateReferrerLeaderboardPageParams(result);
return result;
Expand All @@ -82,7 +82,7 @@ export interface ReferrerLeaderboardPageContext extends Required<ReferrerLeaderb

/**
* Indicates if there is a next page available
* @invariant true if and only if (`page` * `itemsPerPage` < `total`)
* @invariant true if and only if (`page` * `recordsPerPage` < `total`)
*/
hasNext: boolean;

Expand Down Expand Up @@ -123,14 +123,14 @@ export const validateReferrerLeaderboardPageContext = (
`Invalid ReferrerLeaderboardPageContext: total must be a non-negative integer but is ${context.totalRecords}.`,
);
}
const startIndex = (context.page - 1) * context.itemsPerPage;
const endIndex = startIndex + context.itemsPerPage;
const startIndex = (context.page - 1) * context.recordsPerPage;
const endIndex = startIndex + context.recordsPerPage;

if (!context.hasNext && endIndex < context.totalRecords) {
throw new Error(
`Invalid ReferrerLeaderboardPageContext: if hasNext is false, endIndex (${endIndex}) must be greater than or equal to total (${context.totalRecords}).`,
);
} else if (context.hasNext && context.page * context.itemsPerPage >= context.totalRecords) {
} else if (context.hasNext && context.page * context.recordsPerPage >= context.totalRecords) {
throw new Error(
`Invalid ReferrerLeaderboardPageContext: if hasNext is true, endIndex (${endIndex}) must be less than total (${context.totalRecords}).`,
);
Expand All @@ -154,7 +154,7 @@ export const buildReferrerLeaderboardPageContext = (

const totalRecords = leaderboard.referrers.size;

const totalPages = Math.max(1, Math.ceil(totalRecords / materializedParams.itemsPerPage));
const totalPages = Math.max(1, Math.ceil(totalRecords / materializedParams.recordsPerPage));

if (materializedParams.page > totalPages) {
throw new Error(
Expand All @@ -174,8 +174,8 @@ export const buildReferrerLeaderboardPageContext = (
} satisfies ReferrerLeaderboardPageContext;
}

const startIndex = (materializedParams.page - 1) * materializedParams.itemsPerPage;
const maxTheoreticalIndexOnPage = startIndex + (materializedParams.itemsPerPage - 1);
const startIndex = (materializedParams.page - 1) * materializedParams.recordsPerPage;
const maxTheoreticalIndexOnPage = startIndex + (materializedParams.recordsPerPage - 1);
const endIndex = Math.min(maxTheoreticalIndexOnPage, totalRecords - 1);
const hasNext = maxTheoreticalIndexOnPage < totalRecords - 1;
const hasPrev = materializedParams.page > 1;
Expand Down Expand Up @@ -205,9 +205,9 @@ export interface ReferrerLeaderboardPage {

/**
* Ordered list of {@link AwardedReferrerMetrics} for the {@link ReferrerLeaderboardPage}
* described by `paginationContext` within the related {@link ReferrerLeaderboard}.
* described by `pageContext` within the related {@link ReferrerLeaderboard}.
*
* @invariant Array will be empty if `paginationContext.totalRecords` is 0.
* @invariant Array will be empty if `pageContext.totalRecords` is 0.
* @invariant Array entries are ordered by `rank` (descending).
*/
referrers: AwardedReferrerMetrics[];
Expand All @@ -221,7 +221,7 @@ export interface ReferrerLeaderboardPage {
* The {@link ReferrerLeaderboardPageContext} of this {@link ReferrerLeaderboardPage} relative to the overall
* {@link ReferrerLeaderboard}.
*/
paginationContext: ReferrerLeaderboardPageContext;
pageContext: ReferrerLeaderboardPageContext;

/**
* The {@link UnixTimestamp} of when the data used to build the {@link ReferrerLeaderboardPage} was accurate as of.
Expand All @@ -230,22 +230,22 @@ export interface ReferrerLeaderboardPage {
}

export const getReferrerLeaderboardPage = (
paginationParams: ReferrerLeaderboardPageParams,
pageParams: ReferrerLeaderboardPageParams,
leaderboard: ReferrerLeaderboard,
): ReferrerLeaderboardPage => {
const paginationContext = buildReferrerLeaderboardPageContext(paginationParams, leaderboard);
const pageContext = buildReferrerLeaderboardPageContext(pageParams, leaderboard);

let referrers: AwardedReferrerMetrics[];

if (
paginationContext.totalRecords > 0 &&
typeof paginationContext.startIndex !== "undefined" &&
typeof paginationContext.endIndex !== "undefined"
pageContext.totalRecords > 0 &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there an opportunity for us to minimize duplicated logic / types / etc.. for pageContext logic across all our APIs?

It seems we can standardize both the pagination params in requests and the pagination context in responses. What do you think?

typeof pageContext.startIndex !== "undefined" &&
typeof pageContext.endIndex !== "undefined"
) {
// extract the referrers from the leaderboard in the range specified by `paginationContext`.
// extract the referrers from the leaderboard in the range specified by `pageContext`.
referrers = Array.from(leaderboard.referrers.values()).slice(
paginationContext.startIndex,
paginationContext.endIndex + 1, // For `slice`, this is exclusive of the element at the index 'end'. We need it to be inclusive, hence plus one.
pageContext.startIndex,
pageContext.endIndex + 1, // For `slice`, this is exclusive of the element at the index 'end'. We need it to be inclusive, hence plus one.
);
} else {
referrers = [];
Expand All @@ -255,7 +255,7 @@ export const getReferrerLeaderboardPage = (
rules: leaderboard.rules,
referrers,
aggregatedMetrics: leaderboard.aggregatedMetrics,
paginationContext,
pageContext,
accurateAsOf: leaderboard.accurateAsOf,
};
};
Loading