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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.5.3",
"version": "1.5.0",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/models/eventsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ class EventsFactory extends Factory {
* @param {string|ObjectId} userId - id of the user who is visiting events
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkVisitEvent(eventIds, userId) {
async bulkVisitEvents(eventIds, userId) {
const {
collection,
found,
Expand Down
7 changes: 6 additions & 1 deletion src/resolvers/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
} = require('./helpers/bulkEvents');
const { aiService } = require('../services/ai');
const { UserInputError } = require('apollo-server-express');
const { ObjectId } = require('mongodb');

/**
* See all types and fields here {@see ../typeDefs/event.graphql}
Expand Down Expand Up @@ -161,7 +162,7 @@ module.exports = {
}

const factory = getEventsFactory(context, projectId);
const result = await factory.bulkVisitEvent(validEventIds, user.id);
const result = await factory.bulkVisitEvents(validEventIds, user.id);

return {
...result,
Expand Down Expand Up @@ -317,6 +318,10 @@ module.exports = {
const factory = getEventsFactory(context, projectId);

if (assignee) {
if (!ObjectId.isValid(String(assignee))) {
throw new UserInputError('assignee must be a valid id or null');
}

const userExists = await factories.usersFactory.findById(assignee);

if (!userExists) {
Expand Down
46 changes: 34 additions & 12 deletions src/resolvers/helpers/bulkEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const sendPersonalNotification = require('../../utils/personalNotifications').de
const { UserInputError } = require('apollo-server-express');
const { ObjectId } = require('mongodb');

const ASSIGNEE_NOTIFICATIONS_CHUNK_SIZE = 25;

/**
* Enqueue assignee notifications in background (do not block resolver response)
*
Expand All @@ -26,20 +28,40 @@ function fireAndForgetAssigneeNotifications({
return;
}

Promise.allSettled(eventIds.map(eventId => sendPersonalNotification(assigneeData, {
type: 'assignee',
payload: {
assigneeId,
projectId,
whoAssignedId,
eventId,
},
})))
.then((results) => {
const failedResults = results.filter(result => result.status === 'rejected');
Promise.resolve()
.then(async () => {
const failedResults = [];

for (let i = 0; i < eventIds.length; i += ASSIGNEE_NOTIFICATIONS_CHUNK_SIZE) {
const chunk = eventIds.slice(i, i + ASSIGNEE_NOTIFICATIONS_CHUNK_SIZE);
const results = await Promise.allSettled(chunk.map(eventId => sendPersonalNotification(assigneeData, {
type: 'assignee',
payload: {
assigneeId,
projectId,
whoAssignedId,
eventId,
},
})));

failedResults.push(...results.filter(result => result.status === 'rejected'));
}

if (failedResults.length > 0) {
console.error('Failed to enqueue assignee notifications', failedResults);
const failedMessages = failedResults.map((result) => {
const reason = result && result.reason;

if (reason && typeof reason.message === 'string') {
return reason.message;
}

return String(reason || 'Unknown error');
});

console.error('Failed to enqueue assignee notifications', {
failedCount: failedResults.length,
errors: failedMessages,
});
}
})
.catch((error) => {
Expand Down
5 changes: 3 additions & 2 deletions src/typeDefs/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ type BulkUpdateAssigneeResponse {
}

"""
Result of bulk toggling event marks (resolve / ignore)
Result of bulk toggling event marks (resolve / ignore / starred)
"""
type BulkToggleEventMarksResult {
"""
Expand Down Expand Up @@ -602,7 +602,8 @@ extend type Mutation {

"""
Toggle the same mark on many original events at once (resolved, ignored or starred).
Same toggle semantics as toggleEventMark per event.
Uses bulk semantics: if every selected event already has the mark, clear it for all;
otherwise set it on each selected event that does not have it yet.
"""
bulkToggleEventMarks(
"""
Expand Down
6 changes: 3 additions & 3 deletions test/models/eventsFactory-bulk-visit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jest.mock('../../src/mongo', () => ({
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-explicit-any -- CJS class
const EventsFactory = require('../../src/models/eventsFactory') as any;

describe('EventsFactory.bulkVisitEvent', () => {
describe('EventsFactory.bulkVisitEvents', () => {
const projectId = '507f1f77bcf86cd799439011';

beforeEach(() => {
Expand All @@ -57,7 +57,7 @@ describe('EventsFactory.bulkVisitEvent', () => {
});
collectionMock.updateMany.mockResolvedValue({ modifiedCount: 1 });

const result = await factory.bulkVisitEvent([ a.toString(), b.toString() ], userId.toString());
const result = await factory.bulkVisitEvents([ a.toString(), b.toString() ], userId.toString());

expect(result.updatedCount).toBe(1);
expect(result.updatedEventIds).toEqual([ b.toString() ]);
Expand All @@ -72,7 +72,7 @@ describe('EventsFactory.bulkVisitEvent', () => {
toArray: () => Promise.resolve([]),
});

const result = await factory.bulkVisitEvent([ missing.toString() ], new ObjectId().toString());
const result = await factory.bulkVisitEvents([ missing.toString() ], new ObjectId().toString());

expect(result.updatedCount).toBe(0);
expect(result.updatedEventIds).toEqual([]);
Expand Down
41 changes: 30 additions & 11 deletions test/resolvers/event-bulk-update-assignee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ const eventResolvers = require('../../src/resolvers/event') as {
};

const bulkUpdateAssignee = jest.fn();
const ASSIGNEE_ID = '507f1f77bcf86cd799439012';

describe('EventsMutations.bulkUpdateAssignee', () => {
const ctx = {
user: { id: 'u1' },
factories: {
usersFactory: {
findById: jest.fn().mockResolvedValue({ id: 'assignee-1' }),
findById: jest.fn().mockResolvedValue({ id: ASSIGNEE_ID }),
dataLoaders: {
userById: {
load: jest.fn().mockResolvedValue({ id: 'assignee-1', email: 'a@a.a' }),
load: jest.fn().mockResolvedValue({ id: ASSIGNEE_ID, email: 'a@a.a' }),
},
},
},
Expand All @@ -44,7 +45,7 @@ describe('EventsMutations.bulkUpdateAssignee', () => {
},
workspacesFactory: {
findById: jest.fn().mockResolvedValue({
getMemberInfo: jest.fn().mockResolvedValue({ userId: 'assignee-1' }),
getMemberInfo: jest.fn().mockResolvedValue({ userId: ASSIGNEE_ID }),
}),
},
},
Expand All @@ -66,21 +67,39 @@ describe('EventsMutations.bulkUpdateAssignee', () => {
await expect(
eventResolvers.EventsMutations.bulkUpdateAssignee(
{},
{ input: { projectId: 'p1', eventIds: [], assignee: 'assignee-1' } },
{ input: { projectId: 'p1', eventIds: [], assignee: ASSIGNEE_ID } },
ctx
)
).rejects.toThrow(UserInputError);
expect(bulkUpdateAssignee).not.toHaveBeenCalled();
});

it('should throw when assignee id is invalid', async () => {
await expect(
eventResolvers.EventsMutations.bulkUpdateAssignee(
{},
{
input: {
projectId: 'p1',
eventIds: [ '507f1f77bcf86cd799439011' ],
assignee: 'not-an-object-id',
},
},
ctx
)
).rejects.toThrow(UserInputError);

expect(bulkUpdateAssignee).not.toHaveBeenCalled();
});

it('should call factory for bulk assign', async () => {
const result = await eventResolvers.EventsMutations.bulkUpdateAssignee(
{},
{
input: {
projectId: 'p1',
eventIds: [ '507f1f77bcf86cd799439011' ],
assignee: 'assignee-1',
assignee: ASSIGNEE_ID,
},
},
ctx
Expand All @@ -89,15 +108,15 @@ describe('EventsMutations.bulkUpdateAssignee', () => {
expect(result.updatedCount).toBe(1);
expect(bulkUpdateAssignee).toHaveBeenCalledWith(
[ '507f1f77bcf86cd799439011' ],
'assignee-1'
ASSIGNEE_ID
);
expect(sendPersonalNotification).toHaveBeenCalledTimes(1);
expect(sendPersonalNotification).toHaveBeenCalledWith(
expect.objectContaining({ id: 'assignee-1' }),
expect.objectContaining({ id: ASSIGNEE_ID }),
expect.objectContaining({
type: 'assignee',
payload: expect.objectContaining({
assigneeId: 'assignee-1',
assigneeId: ASSIGNEE_ID,
projectId: 'p1',
whoAssignedId: 'u1',
eventId: '507f1f77bcf86cd799439011',
Expand All @@ -119,15 +138,15 @@ describe('EventsMutations.bulkUpdateAssignee', () => {
input: {
projectId: 'p1',
eventIds: [ '507f1f77bcf86cd799439011', 'invalid-id' ],
assignee: 'assignee-1',
assignee: ASSIGNEE_ID,
},
},
ctx
);

expect(bulkUpdateAssignee).toHaveBeenCalledWith(
[ '507f1f77bcf86cd799439011' ],
'assignee-1'
ASSIGNEE_ID
);
expect(result).toEqual({
updatedCount: 1,
Expand All @@ -143,7 +162,7 @@ describe('EventsMutations.bulkUpdateAssignee', () => {
input: {
projectId: 'p1',
eventIds: [ 'bad-1', 'bad-2' ],
assignee: 'assignee-1',
assignee: ASSIGNEE_ID,
},
},
ctx
Expand Down
10 changes: 5 additions & 5 deletions test/resolvers/event-bulk-visit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const eventResolvers = require('../../src/resolvers/event') as {
};
};

const bulkVisitEvent = jest.fn();
const bulkVisitEvents = jest.fn();

describe('Mutation.bulkVisitEvents', () => {
const ctx = {
Expand All @@ -26,11 +26,11 @@ describe('Mutation.bulkVisitEvents', () => {

beforeEach(() => {
jest.clearAllMocks();
(getEventsFactory as unknown as jest.Mock).mockReturnValue({ bulkVisitEvent });
(getEventsFactory as unknown as jest.Mock).mockReturnValue({ bulkVisitEvents });
});

it('should call factory with valid ids only and merge invalid ids', async () => {
bulkVisitEvent.mockResolvedValue({
bulkVisitEvents.mockResolvedValue({
updatedCount: 1,
updatedEventIds: [ '507f1f77bcf86cd799439012' ],
failedEventIds: [ '507f1f77bcf86cd799439099' ],
Expand All @@ -42,7 +42,7 @@ describe('Mutation.bulkVisitEvents', () => {
ctx
);

expect(bulkVisitEvent).toHaveBeenCalledWith(
expect(bulkVisitEvents).toHaveBeenCalledWith(
[ '507f1f77bcf86cd799439012' ],
'507f1f77bcf86cd799439011'
);
Expand All @@ -60,7 +60,7 @@ describe('Mutation.bulkVisitEvents', () => {
ctx
);

expect(bulkVisitEvent).not.toHaveBeenCalled();
expect(bulkVisitEvents).not.toHaveBeenCalled();
expect(result).toEqual({
updatedCount: 0,
updatedEventIds: [],
Expand Down
Loading