Skip to content

Commit

Permalink
feat(api-service,dashboard): Implement before after pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg committed Jan 22, 2025
1 parent c614d44 commit 6670ead
Show file tree
Hide file tree
Showing 11 changed files with 430 additions and 156 deletions.
287 changes: 238 additions & 49 deletions apps/api/src/app/subscribers-v2/subscriber.controller.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,261 @@ const v2Prefix = '/v2';
let session: UserSession;

describe('List Subscriber Permutations', () => {
it('should not return subscribers if not matching query', async () => {
it('should not return subscribers if not matching search params', async () => {
await createSubscriberAndValidate('XYZ');
await createSubscriberAndValidate('XYZ2');
const subscribers = await getAllAndValidate({
searchQuery: 'ABC',
searchParams: { email: '[email protected]' },
expectedTotalResults: 0,
expectedArraySize: 0,
});
expect(subscribers).to.be.empty;
});

it('should not return subscribers if offset is bigger than available subscribers', async () => {
it('should return all results within range', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);
await getAllAndValidate({
searchQuery: uuid,
offset: 11,
limit: 15,
expectedTotalResults: 10,
expectedArraySize: 0,
expectedArraySize: 10,
});
});

it('should return all results within range', async () => {
it('should return results without any search params', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);
await getAllAndValidate({
searchQuery: uuid,
offset: 0,
limit: 15,
expectedTotalResults: 10,
expectedArraySize: 10,
});
});

it('should return results without query', async () => {
it('should page subscribers without overlap using cursors', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);
await getAllAndValidate({
searchQuery: uuid,
offset: 0,
limit: 15,
expectedTotalResults: 10,
expectedArraySize: 10,

const firstPage = await getListSubscribers({
limit: 5,
});

const secondPage = await getListSubscribers({
after: firstPage.next,
limit: 5,
});

const idsDeduplicated = buildIdSet(firstPage.subscribers, secondPage.subscribers);
expect(idsDeduplicated.size).to.be.equal(10);
});
});

describe('List Subscriber Search Filters', () => {
it('should find subscriber by email', async () => {
const uuid = generateUUID();
await createSubscriberAndValidate(uuid);

const subscribers = await getAllAndValidate({
searchParams: { email: `test-${uuid}@subscriber` },
expectedTotalResults: 1,
expectedArraySize: 1,
});

expect(subscribers[0].email).to.contain(uuid);
});

it('should find subscriber by phone', async () => {
const uuid = generateUUID();
await createSubscriberAndValidate(uuid);

const subscribers = await getAllAndValidate({
searchParams: { phone: '1234567' },
expectedTotalResults: 1,
expectedArraySize: 1,
});

expect(subscribers[0].phone).to.equal('+1234567890');
});

it('should find subscriber by full name', async () => {
const uuid = generateUUID();
await createSubscriberAndValidate(uuid);

const subscribers = await getAllAndValidate({
searchParams: { name: `Test ${uuid} Subscriber` },
expectedTotalResults: 1,
expectedArraySize: 1,
});

expect(subscribers[0].firstName).to.equal(`Test ${uuid}`);
expect(subscribers[0].lastName).to.equal('Subscriber');
});

it('should find subscriber by subscriberId', async () => {
const uuid = generateUUID();
await createSubscriberAndValidate(uuid);

const subscribers = await getAllAndValidate({
searchParams: { subscriberId: `test-subscriber-${uuid}` },
expectedTotalResults: 1,
expectedArraySize: 1,
});

expect(subscribers[0].subscriberId).to.equal(`test-subscriber-${uuid}`);
});
});

it('should page subscribers without overlap', async () => {
describe('List Subscriber Cursor Pagination', () => {
it('should paginate forward using after cursor', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);
const listResponse1 = await getAllAndValidate({
searchQuery: uuid,
offset: 0,

const firstPage = await getListSubscribers({
limit: 5,
expectedTotalResults: 10,
expectedArraySize: 5,
});
const listResponse2 = await getAllAndValidate({
searchQuery: uuid,
offset: 5,

const secondPage = await getListSubscribers({
after: firstPage.next,
limit: 5,
expectedTotalResults: 10,
expectedArraySize: 5,
});
const idsDeduplicated = buildIdSet(listResponse1, listResponse2);
expect(idsDeduplicated.size).to.be.equal(10);

expect(firstPage.subscribers).to.have.lengthOf(5);
expect(secondPage.subscribers).to.have.lengthOf(5);
expect(firstPage.next).to.exist;
expect(secondPage.previous).to.exist;

const idsDeduplicated = buildIdSet(firstPage.subscribers, secondPage.subscribers);
expect(idsDeduplicated.size).to.equal(10);
});

it('should paginate backward using before cursor', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const firstPage = await getListSubscribers({
limit: 5,
});

const secondPage = await getListSubscribers({
after: firstPage.next,
limit: 5,
});

const previousPage = await getListSubscribers({
before: secondPage.previous,
limit: 5,
});

expect(previousPage.subscribers).to.have.lengthOf(5);
expect(previousPage.next).to.exist;
expect(previousPage.subscribers).to.deep.equal(firstPage.subscribers);
});

it('should handle pagination with limit=1', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const firstPage = await getListSubscribers({
limit: 1,
});

expect(firstPage.subscribers).to.have.lengthOf(1);
expect(firstPage.next).to.exist;
expect(firstPage.previous).to.not.exist;
});

it('should return empty array when no more results after cursor', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const allResults = await getListSubscribers({
limit: 10,
});

const nextPage = await getListSubscribers({
after: allResults.next,
limit: 5,
});

expect(nextPage.subscribers).to.have.lengthOf(0);
expect(nextPage.next).to.not.exist;
expect(nextPage.previous).to.exist;
});
});

describe('List Subscriber Sorting', () => {
it('should sort subscribers by createdAt in ascending order', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const response = await getListSubscribers({
sortBy: 'createdAt',
sortDirection: 'asc',
limit: 10,
});

const timestamps = response.subscribers.map((sub) => new Date(sub.createdAt).getTime());
const sortedTimestamps = [...timestamps].sort((a, b) => a - b);
expect(timestamps).to.deep.equal(sortedTimestamps);
});

it('should sort subscribers by createdAt in descending order', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const response = await getListSubscribers({
sortBy: 'createdAt',
sortDirection: 'desc',
limit: 10,
});

const timestamps = response.subscribers.map((sub) => new Date(sub.createdAt).getTime());
const sortedTimestamps = [...timestamps].sort((a, b) => b - a);
expect(timestamps).to.deep.equal(sortedTimestamps);
});

it('should sort subscribers by subscriberId', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const response = await getListSubscribers({
sortBy: 'subscriberId',
sortDirection: 'asc',
limit: 10,
});

const ids = response.subscribers.map((sub) => sub.subscriberId);
const sortedIds = [...ids].sort();
expect(ids).to.deep.equal(sortedIds);
});

it('should maintain sort order across pages', async () => {
const uuid = generateUUID();
await create10Subscribers(uuid);

const firstPage = await getListSubscribers({
sortBy: 'createdAt',
sortDirection: 'asc',
limit: 5,
});

const secondPage = await getListSubscribers({
sortBy: 'createdAt',
sortDirection: 'asc',
after: firstPage.next,
limit: 5,
});

const allTimestamps = [
...firstPage.subscribers.map((sub) => new Date(sub.createdAt).getTime()),
...secondPage.subscribers.map((sub) => new Date(sub.createdAt).getTime()),
];

const sortedTimestamps = [...allTimestamps].sort((a, b) => a - b);
expect(allTimestamps).to.deep.equal(sortedTimestamps);
});
});

// Helper functions
async function createSubscriberAndValidate(nameSuffix: string = '') {
const createSubscriberDto = {
subscriberId: `test-subscriber-${nameSuffix}`,
Expand All @@ -100,41 +285,48 @@ async function create10Subscribers(uuid: string) {
}
}

async function getListSubscribers(query: string, offset: number, limit: number) {
const res = await session.testAgent.get(`${v2Prefix}/subscribers`).query({
query,
page: Math.floor(offset / limit) + 1,
limit,
});
interface IListSubscribersQuery {
email?: string;
phone?: string;
name?: string;
subscriberId?: string;
after?: string;
before?: string;
limit?: number;
sortBy?: string;
sortDirection?: 'asc' | 'desc';
}

async function getListSubscribers(params: IListSubscribersQuery = {}) {
const res = await session.testAgent.get(`${v2Prefix}/subscribers`).query(params);
expect(res.status).to.equal(200);

return res.body.data;
}

interface IAllAndValidate {
msgPrefix?: string;
searchQuery: string;
offset?: number;
searchParams?: IListSubscribersQuery;
limit?: number;
expectedTotalResults: number;
expectedArraySize: number;
}

async function getAllAndValidate({
msgPrefix = '',
searchQuery = '',
offset = 0,
limit = 50,
searchParams = {},
limit = 15,
expectedTotalResults,
expectedArraySize,
}: IAllAndValidate) {
const listResponse = await getListSubscribers(searchQuery, offset, limit);
const listResponse = await getListSubscribers({
...searchParams,
limit,
});
const summary = buildLogMsg(
{
msgPrefix,
searchQuery,
offset,
limit,
searchParams,
expectedTotalResults,
expectedArraySize,
},
Expand All @@ -143,16 +335,13 @@ async function getAllAndValidate({

expect(listResponse.subscribers).to.be.an('array', summary);
expect(listResponse.subscribers).lengthOf(expectedArraySize, `subscribers length ${summary}`);
expect(listResponse.totalCount).to.be.equal(expectedTotalResults, `total Results don't match ${summary}`);

return listResponse.subscribers;
}

function buildLogMsg(params: IAllAndValidate, listResponse: any): string {
return `Log - msgPrefix: ${params.msgPrefix},
searchQuery: ${params.searchQuery},
offset: ${params.offset},
limit: ${params.limit},
searchParams: ${JSON.stringify(params.searchParams || 'Not specified', null, 2)},
expectedTotalResults: ${params.expectedTotalResults ?? 'Not specified'},
expectedArraySize: ${params.expectedArraySize ?? 'Not specified'}
response:
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/app/subscribers-v2/subscriber.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class SubscriberController {
ListSubscribersCommand.create({
user,
limit: Number(query.limit || '10'),
cursor: query.cursor,
after: query.after,
before: query.before,
orderDirection: query.orderDirection || DirectionEnum.DESC,
orderBy: query.orderBy || 'createdAt',
email: query.email,
Expand Down
Loading

0 comments on commit 6670ead

Please sign in to comment.