Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #80 from ar-io/PE-5073-paginate-interactions
Browse files Browse the repository at this point in the history
feat(pagination): add pagination to `/interactions` endpoints
  • Loading branch information
dtfiedler authored Dec 14, 2023
2 parents f53c1e0 + e34bc07 commit 192438d
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 94 deletions.
63 changes: 47 additions & 16 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ components:
description: Evaluate the contract at up to a specific sort key. Only applicable if blockHeight is not provided.
schema:
type: string
page:
name: page
in: query
required: false
description: The page of results to return. 1 indexed. Minimum of 1, maximum of MAX_SAFE_INTEGER.
schema:
type: number
maximum: MAX_SAFE_INTEGER
minimum: 1
pageSize:
name: pageSize
in: query
required: false
description: The number of results per page. Minimum of 1, maximum of 1000.
schema:
type: number
maximum: 1000
minimum: 1
responses:
BadRequest:
description: Invalid request.
Expand Down Expand Up @@ -112,7 +130,6 @@ components:
type: integer
throwOnInternalWriteError:
type: boolean

ContractInteraction:
type: array
description: The interactions for a contract, including their validity
Expand All @@ -130,6 +147,26 @@ components:
'id': '2wszuZi_rwoOFjowdH7GLbgdeIZBaGbMLXiOuIV-6_0',
},
]
PagesContext:
type: object
description: The context for paginated results
properties:
page:
type: integer
description: The current page of results - 1 indexed
example: 1
pageSize:
type: integer
description: The number of results per page
example: 10
totalItems:
type: integer
description: The total number of results
example: 100
totalPages:
type: integer
description: The total number of pages
example: 10
paths:
/contract/{contractTxId}:
get:
Expand Down Expand Up @@ -450,6 +487,8 @@ paths:
- $ref: '#/components/parameters/contractTxId'
- $ref: '#/components/parameters/blockHeight'
- $ref: '#/components/parameters/sortKey'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/pageSize'
responses:
'200':
description: OK
Expand Down Expand Up @@ -483,6 +522,8 @@ paths:
- $ref: '#/components/parameters/blockHeight'
- $ref: '#/components/parameters/sortKey'
- $ref: '#/components/parameters/walletAddress'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/pageSize'
responses:
'200':
description: OK
Expand Down Expand Up @@ -571,25 +612,15 @@ paths:
'503':
$ref: '#/components/responses/InternalServerError'

/wallet/{address}/contract/{contractTxId}:
/wallet/{walletAddress}/contract/{contractTxId}:
get:
summary: Returns the interactions on a given contract for a given wallet address
description: Returns the interactions on a given contract for a given wallet address
parameters:
- in: path
name: address
required: true
description: Public Arweave wallet address.
schema:
type: string
example: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ'
- in: path
name: contractTxId
required: true
description: Transaction ID of the contract.
schema:
type: string
example: 'bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U'
- $ref: '#/components/parameters/contractTxId'
- $ref: '#/components/parameters/walletAddress'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/pageSize'
responses:
'200':
description: OK
Expand Down
30 changes: 27 additions & 3 deletions src/middleware/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ import logger from '../logger';
import { BadRequestError } from '../errors';

const WARP_SORT_KEY_REGEX = /^[0-9]{12},[0-9]{13},[0-9a-f]{64}$/;
const MAX_PAGE_LIMIT = 1000;
export const queryMiddleware = async (ctx: KoaContext, next: Next) => {
const { blockHeight, sortKey } = ctx.query;
const { blockHeight, sortKey, page, pageSize } = ctx.query;

logger.debug('Query params provided', {
...ctx.query,
});

if (blockHeight && sortKey) {
throw new BadRequestError(
Expand All @@ -36,7 +41,6 @@ export const queryMiddleware = async (ctx: KoaContext, next: Next) => {
'Invalid block height, must be a single integer',
);
}
logger.info('Block height provided via query param', { blockHeight });
ctx.state.blockHeight = +blockHeight;
}

Expand All @@ -47,9 +51,29 @@ export const queryMiddleware = async (ctx: KoaContext, next: Next) => {
`Invalid sort key, must be a single string and match ${WARP_SORT_KEY_REGEX}`,
);
}
logger.info('Sort key provided via query param', { sortKey });
ctx.state.sortKey = sortKey;
}

if (page) {
// for improved UX, page is 1 based
if (isNaN(+page) || +page > Number.MAX_SAFE_INTEGER || +page < 1) {
logger.debug('Invalid page provided', { page });
throw new BadRequestError(
`Invalid page, must be a single positive integer and less than ${Number.MAX_SAFE_INTEGER}`,
);
}
ctx.state.page = +page;
}

if (pageSize) {
if (isNaN(+pageSize) || +pageSize > MAX_PAGE_LIMIT || +pageSize < 1) {
logger.debug('Invalid pageSize provided', { pageSize });
throw new BadRequestError(
`Invalid pageSize, must be a single positive integer and less than ${MAX_PAGE_LIMIT}`,
);
}
ctx.state.pageSize = +pageSize;
}

return next();
};
49 changes: 47 additions & 2 deletions src/routes/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
warp,
sortKey: requestedSortKey,
blockHeight: requestedBlockHeight,
page: requestedPage,
pageSize: requestedPageSize = 100,
} = ctx.state;
const { contractTxId, address } = ctx.params;

Expand All @@ -75,6 +77,10 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
address,
});

/**
* TODO: add a read through promise cache here that uses the following logic as the resulting promise. The cache key should contain the contractTxId, sortKey, blockHeight, address, page, and pageSize.
*/

const [
{ validity, errorMessages, evaluationOptions, sortKey: evaluatedSortKey },
{ interactions },
Expand Down Expand Up @@ -161,6 +167,7 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
interactionSortKey === requestedSortKey,
);
mappedInteractions = mappedInteractions.slice(0, sortKeyIndex + 1);

logger.debug('Done filtering up to sort key', {
contractTxId,
sortKey: requestedSortKey,
Expand All @@ -170,12 +177,50 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
});
}

// sort them in descending order
mappedInteractions = mappedInteractions.reverse();
const totalInteractions = mappedInteractions.length;

if (requestedPage !== undefined) {
logger.debug('Paginating interactions', {
contractTxId,
sortKey: requestedSortKey,
blockHeight: requestedBlockHeight,
address,
page: requestedPage,
pageSize: requestedPageSize,
});
// this logic is 1 based
const pageStartIndex = requestedPage - 1 * requestedPageSize;
const pageEndIndex = requestedPage * requestedPageSize;
mappedInteractions = mappedInteractions.slice(pageStartIndex, pageEndIndex);
logger.debug('Done paginating interactions', {
contractTxId,
sortKey: requestedSortKey,
blockHeight: requestedBlockHeight,
address,
page: requestedPage,
pageSize: requestedPageSize,
totalCount: mappedInteractions.length,
});
}

ctx.body = {
contractTxId,
// return them in descending order
interactions: mappedInteractions.reverse(),
address,
sortKey: evaluatedSortKey,
interactions: mappedInteractions,
// only include page information if params were provided
...(requestedPage !== undefined && {
pages: {
page: requestedPage,
pageSize: requestedPageSize,
totalPages: Math.ceil(totalInteractions / requestedPageSize),
totalItems: totalInteractions,
hasNextPage:
requestedPage < Math.ceil(totalInteractions / requestedPageSize),
},
}),
evaluationOptions,
};
}
Expand Down
12 changes: 10 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ import {
import { allowedContractTypes } from './constants';

// Koa types
export type KoaState = {
export type QueryState = {
blockHeight?: number;
sortKey?: string;
page?: number;
pageSize?: number;
};

export type ArnsState = {
logger: winston.Logger;
warp: Warp;
arweave: Arweave;
} & DefaultState;
};

export type KoaState = ArnsState & QueryState & DefaultState;
export type KoaContext = ParameterizedContext<KoaState>;

// ArNS types
Expand Down
Loading

0 comments on commit 192438d

Please sign in to comment.