Skip to content

Commit

Permalink
feat: pagination (#1490)
Browse files Browse the repository at this point in the history
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
rodrigopavezi and coderabbitai[bot] authored Nov 26, 2024
1 parent 7643e82 commit d54df7f
Show file tree
Hide file tree
Showing 19 changed files with 372 additions and 40 deletions.
8 changes: 6 additions & 2 deletions packages/data-access/src/combined-data-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess
async getChannelsByTopic(
topic: string,
updatedBetween?: DataAccessTypes.ITimestampBoundaries | undefined,
page?: number,
pageSize?: number,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
return await this.reader.getChannelsByTopic(topic, updatedBetween);
return await this.reader.getChannelsByTopic(topic, updatedBetween, page, pageSize);
}
async getChannelsByMultipleTopics(
topics: string[],
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number | undefined,
pageSize?: number | undefined,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
return await this.reader.getChannelsByMultipleTopics(topics, updatedBetween);
return await this.reader.getChannelsByMultipleTopics(topics, updatedBetween, page, pageSize);
}
async persistTransaction(
transactionData: DataAccessTypes.ITransaction,
Expand Down
53 changes: 51 additions & 2 deletions packages/data-access/src/data-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,26 @@ export class DataAccessRead implements DataAccessTypes.IDataRead {
async getChannelsByTopic(
topic: string,
updatedBetween?: DataAccessTypes.ITimestampBoundaries | undefined,
page?: number | undefined,
pageSize?: number | undefined,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
return this.getChannelsByMultipleTopics([topic], updatedBetween);
return this.getChannelsByMultipleTopics([topic], updatedBetween, page, pageSize);
}

async getChannelsByMultipleTopics(
topics: string[],
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
const result = await this.storage.getTransactionsByTopics(topics);
// Validate pagination parameters
if (page !== undefined && page < 1) {
throw new Error(`Page number must be greater than or equal to 1, but it is ${page}`);
}
if (pageSize !== undefined && pageSize < 1) {
throw new Error(`Page size must be greater than 0, but it is ${pageSize}`);
}

const pending = this.pendingStore?.findByTopics(topics) || [];

const pendingItems = pending.map((item) => ({
Expand All @@ -73,6 +84,33 @@ export class DataAccessRead implements DataAccessTypes.IDataRead {
topics: item.topics || [],
}));

// Calculate adjusted pagination
let adjustedPage = page;
let adjustedPageSize = pageSize;
let pendingItemsOnCurrentPage = 0;
if (page !== undefined && pageSize !== undefined) {
const totalPending = pendingItems.length;
const itemsPerPage = (page - 1) * pageSize;

if (totalPending > itemsPerPage) {
pendingItemsOnCurrentPage = Math.min(totalPending - itemsPerPage, pageSize);
adjustedPageSize = pageSize - pendingItemsOnCurrentPage;
adjustedPage = 1;
if (adjustedPageSize === 0) {
adjustedPageSize = 1;
pendingItemsOnCurrentPage--;
}
} else {
adjustedPage = page - Math.floor(totalPending / pageSize);
}
}

const result = await this.storage.getTransactionsByTopics(
topics,
adjustedPage,
adjustedPageSize,
);

const transactions = result.transactions.concat(...pendingItems);

// list of channels having at least one tx updated during the updatedBetween boundaries
Expand Down Expand Up @@ -106,6 +144,17 @@ export class DataAccessRead implements DataAccessTypes.IDataRead {
},
{} as Record<string, string[]>,
),
pagination:
page && pageSize
? {
total: result.transactions.length + pendingItems.length,
page,
pageSize,
hasMore:
(page - 1) * pageSize + filteredTxs.length - pendingItemsOnCurrentPage <
result.transactions.length,
}
: undefined,
},
result: {
transactions: filteredTxs.reduce((prev, curr) => {
Expand Down
34 changes: 32 additions & 2 deletions packages/data-access/src/in-memory-indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,38 @@ export class InMemoryIndexer implements StorageTypes.IIndexer {
};
}

async getTransactionsByTopics(topics: string[]): Promise<StorageTypes.IGetTransactionsResponse> {
const channelIds = topics.map((topic) => this.#topicToChannelsIndex.get(topic)).flat();
async getTransactionsByTopics(
topics: string[],
page?: number,
pageSize?: number,
): Promise<StorageTypes.IGetTransactionsResponse> {
if (page !== undefined && page < 1) {
throw new Error('Page must be greater than or equal to 1');
}
if (pageSize !== undefined && pageSize <= 0) {
throw new Error('Page size must be greater than 0');
}

// Efficiently get total count without creating intermediate array
const channelIdsSet = new Set(topics.flatMap((topic) => this.#topicToChannelsIndex.get(topic)));
const total = channelIdsSet.size;
let channelIds = Array.from(channelIdsSet);

if (page && pageSize) {
const start = (page - 1) * pageSize;
// Return empty result if page exceeds available data
if (start >= total) {
return {
blockNumber: 0,
transactions: [],
pagination:
page && pageSize
? { total, page, pageSize, hasMore: page * pageSize < total }
: undefined,
};
}
channelIds = channelIds.slice(start, start + pageSize);
}
const locations = channelIds
.map((channel) => this.#channelToLocationsIndex.get(channel))
.flat();
Expand Down
48 changes: 41 additions & 7 deletions packages/request-client.js/src/api/request-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
SignatureProviderTypes,
TransactionTypes,
} from '@requestnetwork/types';
import { deepCopy, supportedIdentities } from '@requestnetwork/utils';
import { deepCopy, supportedIdentities, validatePaginationParams } from '@requestnetwork/utils';
import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency';
import * as Types from '../types';
import ContentDataExtension from './content-data-extension';
Expand Down Expand Up @@ -268,7 +268,12 @@ export default class RequestNetwork {
public async fromIdentity(
identity: IdentityTypes.IIdentity,
updatedBetween?: Types.ITimestampBoundaries,
options?: { disablePaymentDetection?: boolean; disableEvents?: boolean },
options?: {
disablePaymentDetection?: boolean;
disableEvents?: boolean;
page?: number;
pageSize?: number;
},
): Promise<Request[]> {
if (!this.supportedIdentities.includes(identity.type)) {
throw new Error(`${identity.type} is not supported`);
Expand All @@ -286,7 +291,12 @@ export default class RequestNetwork {
public async fromMultipleIdentities(
identities: IdentityTypes.IIdentity[],
updatedBetween?: Types.ITimestampBoundaries,
options?: { disablePaymentDetection?: boolean; disableEvents?: boolean },
options?: {
disablePaymentDetection?: boolean;
disableEvents?: boolean;
page?: number;
pageSize?: number;
},
): Promise<Request[]> {
const identityNotSupported = identities.find(
(identity) => !this.supportedIdentities.includes(identity.type),
Expand All @@ -309,11 +319,23 @@ export default class RequestNetwork {
public async fromTopic(
topic: any,
updatedBetween?: Types.ITimestampBoundaries,
options?: { disablePaymentDetection?: boolean; disableEvents?: boolean },
options?: {
disablePaymentDetection?: boolean;
disableEvents?: boolean;
page?: number;
pageSize?: number;
},
): Promise<Request[]> {
validatePaginationParams(options?.page, options?.pageSize);

// Gets all the requests indexed by the value of the identity
const requestsAndMeta: RequestLogicTypes.IReturnGetRequestsByTopic =
await this.requestLogic.getRequestsByTopic(topic, updatedBetween);
await this.requestLogic.getRequestsByTopic(
topic,
updatedBetween,
options?.page,
options?.pageSize,
);
// From the requests of the request-logic layer creates the request objects and gets the payment networks
const requestPromises = requestsAndMeta.result.requests.map(
async (requestFromLogic: {
Expand Down Expand Up @@ -361,11 +383,23 @@ export default class RequestNetwork {
public async fromMultipleTopics(
topics: any[],
updatedBetween?: Types.ITimestampBoundaries,
options?: { disablePaymentDetection?: boolean; disableEvents?: boolean },
options?: {
disablePaymentDetection?: boolean;
disableEvents?: boolean;
page?: number;
pageSize?: number;
},
): Promise<Request[]> {
validatePaginationParams(options?.page, options?.pageSize);

// Gets all the requests indexed by the value of the identity
const requestsAndMeta: RequestLogicTypes.IReturnGetRequestsByTopic =
await this.requestLogic.getRequestsByMultipleTopics(topics, updatedBetween);
await this.requestLogic.getRequestsByMultipleTopics(
topics,
updatedBetween,
options?.page,
options?.pageSize,
);

// From the requests of the request-logic layer creates the request objects and gets the payment networks
const requestPromises = requestsAndMeta.result.requests.map(
Expand Down
20 changes: 17 additions & 3 deletions packages/request-client.js/src/http-data-access.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClientTypes, DataAccessTypes } from '@requestnetwork/types';
import { EventEmitter } from 'events';
import httpConfigDefaults from './http-config-defaults';
import { normalizeKeccak256Hash, retry } from '@requestnetwork/utils';
import { normalizeKeccak256Hash, retry, validatePaginationParams } from '@requestnetwork/utils';
import { stringify } from 'qs';
import { utils } from 'ethers';

Expand Down Expand Up @@ -175,11 +175,19 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess {
public async getChannelsByTopic(
topic: string,
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
return await this.fetchAndRetry('/getChannelsByTopic', {
validatePaginationParams(page, pageSize);

const params = {
topic,
updatedBetween,
});
...(page !== undefined && { page }),
...(pageSize !== undefined && { pageSize }),
};

return await this.fetchAndRetry('/getChannelsByTopic', params);
}

/**
Expand All @@ -191,10 +199,16 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess {
public async getChannelsByMultipleTopics(
topics: string[],
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<DataAccessTypes.IReturnGetChannelsByTopic> {
validatePaginationParams(page, pageSize);

return await this.fetchAndRetry('/getChannelsByMultipleTopics', {
topics,
updatedBetween,
page,
pageSize,
});
}

Expand Down
34 changes: 28 additions & 6 deletions packages/request-client.js/test/api/request-network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ describe('api/request-network', () => {
it('can get requests with payment network fromIdentity', async () => {
const mockDataAccessWithTxs: DataAccessTypes.IDataAccess = {
...mockDataAccess,
async getChannelsByTopic(topic: string): Promise<any> {
async getChannelsByTopic(
topic: string,
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<any> {
expect(topic).toBe('01f1a21ab419611dbf492b3136ac231c8773dc897ee0eb5167ef2051a39e685e76');
return {
meta: {
Expand Down Expand Up @@ -137,7 +142,14 @@ describe('api/request-network', () => {
};

const requestnetwork = new RequestNetwork({ dataAccess: mockDataAccessWithTxs });
const requests: Request[] = await requestnetwork.fromIdentity(TestData.payee.identity);
const requests: Request[] = await requestnetwork.fromIdentity(
TestData.payee.identity,
undefined,
{
page: 1,
pageSize: 10,
},
);

expect(requests.length).toBe(2);
expect(requests[0].requestId).toBe(TestData.actionRequestId);
Expand Down Expand Up @@ -201,7 +213,12 @@ describe('api/request-network', () => {
it('can get requests with payment network from multiple Identities', async () => {
const mockDataAccessWithTxs: DataAccessTypes.IDataAccess = {
...mockDataAccess,
async getChannelsByMultipleTopics(topics: [string]): Promise<any> {
async getChannelsByMultipleTopics(
topics: [string],
updatedBetween?: DataAccessTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<any> {
expect(topics).toEqual([
'01f1a21ab419611dbf492b3136ac231c8773dc897ee0eb5167ef2051a39e685e76',
]);
Expand Down Expand Up @@ -245,9 +262,14 @@ describe('api/request-network', () => {
};

const requestnetwork = new RequestNetwork({ dataAccess: mockDataAccessWithTxs });
const requests: Request[] = await requestnetwork.fromMultipleIdentities([
TestData.payee.identity,
]);
const requests: Request[] = await requestnetwork.fromMultipleIdentities(
[TestData.payee.identity],
undefined,
{
page: 1,
pageSize: 10,
},
);

expect(requests.length).toBe(2);
expect(requests[0].requestId).toBe(TestData.actionRequestId);
Expand Down
8 changes: 8 additions & 0 deletions packages/request-logic/src/request-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,17 @@ export default class RequestLogic implements RequestLogicTypes.IRequestLogic {
public async getRequestsByTopic(
topic: string,
updatedBetween?: RequestLogicTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<RequestLogicTypes.IReturnGetRequestsByTopic> {
// hash all the topics
const hashedTopic = MultiFormat.serialize(normalizeKeccak256Hash(topic));

const getChannelsResult = await this.transactionManager.getChannelsByTopic(
hashedTopic,
updatedBetween,
page,
pageSize,
);
return this.computeMultipleRequestFromChannels(getChannelsResult);
}
Expand All @@ -365,6 +369,8 @@ export default class RequestLogic implements RequestLogicTypes.IRequestLogic {
public async getRequestsByMultipleTopics(
topics: string[],
updatedBetween?: RequestLogicTypes.ITimestampBoundaries,
page?: number,
pageSize?: number,
): Promise<RequestLogicTypes.IReturnGetRequestsByTopic> {
// hash all the topics
const hashedTopics = topics.map((topic) =>
Expand All @@ -374,6 +380,8 @@ export default class RequestLogic implements RequestLogicTypes.IRequestLogic {
const getChannelsResult = await this.transactionManager.getChannelsByMultipleTopics(
hashedTopics,
updatedBetween,
page,
pageSize,
);
return this.computeMultipleRequestFromChannels(getChannelsResult);
}
Expand Down
Loading

0 comments on commit d54df7f

Please sign in to comment.