Skip to content

Commit

Permalink
chore: add null responses when using cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardogarim committed Nov 18, 2024
1 parent bc1bbbf commit f8e40b5
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 54 deletions.
64 changes: 54 additions & 10 deletions apps/meteor/server/publications/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function mountCursorQuery({ next, previous, count }: { next?: string; pre
} {
const options: FindOptions<IMessage> = {
sort: { _updatedAt: 1 },
...(next || previous ? { limit: count } : {}),
...(next || previous ? { limit: count + 1 } : {}),
};

if (next) {
Expand All @@ -78,20 +78,60 @@ export function mountCursorFromMessage(message: IMessage & { _deletedAt?: Date }
throw new Meteor.Error('error-cursor-not-found', 'Cursor not found', { method: 'messages/get' });
}

export function mountNextCursor(messages: IMessage[], type: CursorPaginationType, next?: string, previous?: string): string | null {
if (messages.length > 0) {
return mountCursorFromMessage(messages[messages.length - 1], type);
export function mountNextCursor(
messages: IMessage[],
count: number,
type: CursorPaginationType,
next?: string,
previous?: string,
): string | null {
if (messages.length === 0) {
return null;
}

if (previous) {
return mountCursorFromMessage(messages[0], type);
}

if (messages.length <= count && next) {
return null;
}

return next ?? previous ?? null;
if (messages.length > count && next) {
return mountCursorFromMessage(messages[messages.length - 2], type);
}

return mountCursorFromMessage(messages[messages.length - 1], type);
}

export function mountPreviousCursor(messages: IMessage[], type: CursorPaginationType, next?: string, previous?: string): string | null {
if (messages.length > 0) {
export function mountPreviousCursor(
messages: IMessage[],
count: number,
type: CursorPaginationType,
next?: string,
previous?: string,
): string | null {
if (messages.length === 0) {
return null;
}

if (messages.length <= count && next) {
return mountCursorFromMessage(messages[0], type);
}

if (messages.length > count && next) {
return mountCursorFromMessage(messages[0], type);
}

return previous ?? next ?? null;
if (messages.length <= count && previous) {
return null;
}

if (messages.length > count && previous) {
return mountCursorFromMessage(messages[messages.length - 2], type);
}

return mountCursorFromMessage(messages[0], type);
}

export async function handleWithoutPagination(rid: IRoom['_id'], lastUpdate: Date) {
Expand Down Expand Up @@ -124,10 +164,14 @@ export async function handleCursorPagination(
: ((await Messages.trashFind({ rid, _deletedAt: query }, { projection: { _id: 1, _deletedAt: 1 }, ...options })!.toArray()) ?? []);

const cursor = {
next: mountNextCursor(response, type, next, previous),
previous: mountPreviousCursor(response, type, next, previous),
next: mountNextCursor(response, count, type, next, previous),
previous: mountPreviousCursor(response, count, type, next, previous),
};

if (response.length > count) {
response.pop();
}

return {
[type.toLowerCase()]: response,
cursor,
Expand Down
12 changes: 4 additions & 8 deletions apps/meteor/tests/end-to-end/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3179,9 +3179,7 @@ describe('[Chat]', () => {

expect(responseWithNext.body.result.updated).to.have.lengthOf(1);
expect(responseWithNext.body.result.updated[0]._id).to.be.equal(thirdMessage.body.message._id);
expect(responseWithNext.body.result.cursor)
.to.have.property('next')
.and.to.equal(new Date(thirdMessage.body.message._updatedAt).getTime().toString());
expect(responseWithNext.body.result.cursor).to.have.property('next').and.to.be.null;
expect(responseWithNext.body.result.cursor)
.to.have.property('previous')
.and.to.equal(new Date(thirdMessage.body.message._updatedAt).getTime().toString());
Expand All @@ -3206,8 +3204,8 @@ describe('[Chat]', () => {
.query({ roomId: newChannel._id, next: lastUpdate.getTime().toString(), type: 'DELETED', count: 2 });

expect(response.body.result.deleted).to.have.lengthOf(0);
expect(response.body.result.cursor).to.have.property('next').and.to.equal(lastUpdate.getTime().toString());
expect(response.body.result.cursor).to.have.property('previous').and.to.equal(lastUpdate.getTime().toString());
expect(response.body.result.cursor).to.have.property('next').and.to.be.null;
expect(response.body.result.cursor).to.have.property('previous').and.to.be.null;

const firstDeletedMessage = (await deleteMessage({ roomId: newChannel._id, msgId: firstMessage._id })).body.message;
const secondDeletedMessage = (await deleteMessage({ roomId: newChannel._id, msgId: secondMessage._id })).body.message;
Expand Down Expand Up @@ -3239,9 +3237,7 @@ describe('[Chat]', () => {

expect(responseAfterDeleteWithPrevious.body.result.deleted).to.have.lengthOf(1);
expect(responseAfterDeleteWithPrevious.body.result.deleted[0]._id).to.be.equal(thirdDeletedMessage._id);
expect(responseAfterDeleteWithPrevious.body.result.cursor)
.to.have.property('next')
.and.to.equal(new Date(responseAfterDeleteWithPrevious.body.result.deleted[0]._deletedAt).getTime().toString());
expect(responseAfterDeleteWithPrevious.body.result.cursor).to.have.property('next').and.to.be.null;
expect(responseAfterDeleteWithPrevious.body.result.cursor)
.to.have.property('previous')
.and.to.equal(new Date(responseAfterDeleteWithPrevious.body.result.deleted[0]._deletedAt).getTime().toString());
Expand Down
81 changes: 45 additions & 36 deletions apps/meteor/tests/unit/server/publications/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ describe('mountCursorQuery', () => {
it('should return a query with $gt and ascending sort when next is provided', () => {
const result = mountCursorQuery({ next: mockDate.toString(), count: 10 });
expect(result.query).to.deep.equal({ $gt: new Date(mockDate) });
expect(result.options).to.deep.equal({ sort: { _updatedAt: 1 }, limit: 10 });
expect(result.options).to.deep.equal({ sort: { _updatedAt: 1 }, limit: 10 + 1 });
});

it('should return a query with $gt and descending sort when previous is provided', () => {
const result = mountCursorQuery({ previous: mockDate.toString(), count: 10 });
expect(result.query).to.deep.equal({ $lt: new Date(mockDate) });
expect(result.options).to.deep.equal({ sort: { _updatedAt: -1 }, limit: 10 });
expect(result.options).to.deep.equal({ sort: { _updatedAt: -1 }, limit: 10 + 1 });
});
});

Expand Down Expand Up @@ -119,62 +119,71 @@ describe('mountCursorFromMessage', () => {
});

describe('mountNextCursor', () => {
it('should return a cursor for the most recent message when messages are present', () => {
// Messages are already sorted by ascending order
const messages = [{ _updatedAt: new Date('2024-10-01T09:00:00Z') }, { _updatedAt: new Date('2024-10-01T10:00:00Z') }];
const type = 'UPDATED';
const result = mountNextCursor(messages, type);
expect(result).to.equal(`${messages[1]._updatedAt.getTime()}`);
const mockMessage = (timestamp: number): Pick<IMessage, '_id' | 'ts' | '_updatedAt'> => ({
_id: '1',
ts: new Date(timestamp),
_updatedAt: new Date(timestamp),
});

it('should return null if no messages and no previous cursor', () => {
const messages: IMessage[] = [];
const type = 'UPDATED';
const result = mountNextCursor(messages, type);
expect(result).to.equal(null);
it('should return null if messages array is empty', () => {
expect(mountNextCursor([], 10, 'UPDATED')).to.be.null;
});

it('should return previous cursor in case of no messages and previous cursor is provided', () => {
const messages: IMessage[] = [];
const type = 'UPDATED';
const previous = '1000';
const result = mountNextCursor(messages, type, undefined, previous);
expect(result).to.equal(previous);
it('should return the first message cursor if previous is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000)];
expect(mountNextCursor(messages, 10, 'UPDATED', undefined, 'prev')).to.equal('1000');
});
});

describe('mountPreviousCursor', () => {
afterEach(() => {
messagesMock.findForUpdates.reset();
messagesMock.trashFindDeletedAfter.reset();
it('should return null if messages length is less than or equal to count and next is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000)];
expect(mountNextCursor(messages, 2, 'UPDATED', 'next')).to.be.null;
});

it('should return the second last message cursor if messages length is greater than count and next is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000), mockMessage(3000)];
expect(mountNextCursor(messages, 2, 'UPDATED', 'next')).to.equal('2000');
});

it('should return the last message cursor if no next or previous is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000)];
expect(mountNextCursor(messages, 10, 'UPDATED')).to.equal('2000');
});
});

describe('mountPreviousCursor', () => {
const mockMessage = (timestamp: number): Pick<IMessage, '_id' | 'ts' | '_updatedAt'> => ({
_id: '1',
ts: new Date(timestamp),
_updatedAt: new Date(timestamp),
});

it('should return null if count, next and previous are not provided', () => {
const result = mountPreviousCursor([], 'UPDATED', null);
expect(result).to.be.null;
it('should return null if messages array is empty', () => {
expect(mountPreviousCursor([], 10, 'UPDATED')).to.be.null;
});

it('should return next if messages length is 0 and next is provided', () => {
const result = mountPreviousCursor([], 'UPDATED', 10, '1000');
expect(result).to.equal('1000');
it('should return the first message cursor if messages length is less than or equal to count and next is provided', () => {
const messages = [mockMessage(1000)];
expect(mountPreviousCursor(messages, 1, 'UPDATED', 'nextCursor')).to.equal('1000');
});

it('should return next if messages length is equal to count and next is provided', () => {
it('should return the first message cursor if messages length is greater than count and next is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000)];
const result = mountPreviousCursor(messages, 'UPDATED', 2, '1000');
expect(result).to.equal('1000');
expect(mountPreviousCursor(messages, 1, 'UPDATED', 'nextCursor')).to.equal('1000');
});

it('should return null if messages length is less than or equal to count and previous is provided', () => {
const messages = [mockMessage(1000)];
expect(mountPreviousCursor(messages, 1, 'UPDATED', undefined, 'previousCursor')).to.be.null;
});

it('should return the second last message cursor if messages length is greater than count and previous is provided', () => {
const messages = [mockMessage(1000), mockMessage(2000), mockMessage(3000)];
expect(mountPreviousCursor(messages, 2, 'UPDATED', undefined, 'previousCursor')).to.equal('2000');
});

it('should return null if messages length is less or equal to count', () => {
it('should return the first message cursor if no next or previous is provided', () => {
const messages = [mockMessage(1000)];
const result = mountPreviousCursor(messages, 'UPDATED', 2);
expect(result).to.equal('1000');
expect(mountPreviousCursor(messages, 1, 'UPDATED')).to.equal('1000');
});
});

Expand Down

0 comments on commit f8e40b5

Please sign in to comment.