From 6535527f5be5521db3c7727798d71b9565d54304 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:20:49 +0300 Subject: [PATCH 1/2] fix: notif --- src/resolvers/event.js | 23 +++-- src/resolvers/helpers/bulkEvents.js | 20 ++++- test/resolvers/bulk-events-helper.test.ts | 86 +++++++++++++++++++ .../event-bulk-update-assignee.test.ts | 14 +++ 4 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 test/resolvers/bulk-events-helper.test.ts diff --git a/src/resolvers/event.js b/src/resolvers/event.js index 971b80a5..af1d6f70 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -304,6 +304,7 @@ module.exports = { async bulkUpdateAssignee(_obj, { input }, { factories, user, ...context }) { const { projectId, eventIds, assignee } = input; const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds); + let assigneeData = null; if (validEventIds.length === 0) { return { @@ -322,6 +323,8 @@ module.exports = { throw new UserInputError('assignee not found'); } + assigneeData = userExists; + const project = await factories.projectsFactory.findById(projectId); const workspace = await factories.workspacesFactory.findById(project.workspaceId); const assigneeExistsInWorkspace = await workspace.getMemberInfo(assignee); @@ -338,19 +341,13 @@ module.exports = { }; if (assignee && resultWithInvalid.updatedEventIds.length > 0) { - factories.usersFactory.dataLoaders.userById.load(assignee) - .then((assigneeData) => { - fireAndForgetAssigneeNotifications({ - assigneeData, - eventIds: resultWithInvalid.updatedEventIds, - projectId, - assigneeId: assignee, - whoAssignedId: user.id, - }); - }) - .catch((error) => { - console.error('Failed to load assignee data for bulk notifications', error); - }); + fireAndForgetAssigneeNotifications({ + assigneeData, + eventIds: resultWithInvalid.updatedEventIds, + projectId, + assigneeId: assignee, + whoAssignedId: user.id, + }); } return resultWithInvalid; diff --git a/src/resolvers/helpers/bulkEvents.js b/src/resolvers/helpers/bulkEvents.js index 75cb7cc2..4d0238a5 100644 --- a/src/resolvers/helpers/bulkEvents.js +++ b/src/resolvers/helpers/bulkEvents.js @@ -20,6 +20,12 @@ function fireAndForgetAssigneeNotifications({ assigneeId, whoAssignedId, }) { + if (!assigneeData) { + console.error('Failed to enqueue assignee notifications: assignee data is empty'); + + return; + } + Promise.allSettled(eventIds.map(eventId => sendPersonalNotification(assigneeData, { type: 'assignee', payload: { @@ -28,9 +34,17 @@ function fireAndForgetAssigneeNotifications({ whoAssignedId, eventId, }, - }))).catch((error) => { - console.error('Failed to enqueue assignee notifications', error); - }); + }))) + .then((results) => { + const failedResults = results.filter(result => result.status === 'rejected'); + + if (failedResults.length > 0) { + console.error('Failed to enqueue assignee notifications', failedResults); + } + }) + .catch((error) => { + console.error('Failed to enqueue assignee notifications', error); + }); } /** diff --git a/test/resolvers/bulk-events-helper.test.ts b/test/resolvers/bulk-events-helper.test.ts new file mode 100644 index 00000000..a9394f7f --- /dev/null +++ b/test/resolvers/bulk-events-helper.test.ts @@ -0,0 +1,86 @@ +import '../../src/env-test'; + +jest.mock('../../src/utils/personalNotifications', () => ({ + __esModule: true, + default: jest.fn().mockResolvedValue(undefined), +})); + +import sendPersonalNotification from '../../src/utils/personalNotifications'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { fireAndForgetAssigneeNotifications } = require('../../src/resolvers/helpers/bulkEvents') as { + fireAndForgetAssigneeNotifications: (args: { + assigneeData: Record | null; + eventIds: string[]; + projectId: string; + assigneeId: string; + whoAssignedId: string; + }) => void; +}; + +describe('fireAndForgetAssigneeNotifications', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('should enqueue personal notification for each event id', async () => { + fireAndForgetAssigneeNotifications({ + assigneeData: { id: 'assignee-1', email: 'assignee@hawk.so' }, + eventIds: [ 'e-1', 'e-2' ], + projectId: 'p-1', + assigneeId: 'assignee-1', + whoAssignedId: 'u-1', + }); + + await Promise.resolve(); + + expect(sendPersonalNotification).toHaveBeenCalledTimes(2); + expect(sendPersonalNotification).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ id: 'assignee-1' }), + { + type: 'assignee', + payload: { + assigneeId: 'assignee-1', + projectId: 'p-1', + whoAssignedId: 'u-1', + eventId: 'e-1', + }, + } + ); + expect(sendPersonalNotification).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ id: 'assignee-1' }), + { + type: 'assignee', + payload: { + assigneeId: 'assignee-1', + projectId: 'p-1', + whoAssignedId: 'u-1', + eventId: 'e-2', + }, + } + ); + }); + + it('should not call personal notifications when assignee data is empty', () => { + fireAndForgetAssigneeNotifications({ + assigneeData: null, + eventIds: [ 'e-1' ], + projectId: 'p-1', + assigneeId: 'assignee-1', + whoAssignedId: 'u-1', + }); + + expect(sendPersonalNotification).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to enqueue assignee notifications: assignee data is empty' + ); + }); +}); diff --git a/test/resolvers/event-bulk-update-assignee.test.ts b/test/resolvers/event-bulk-update-assignee.test.ts index 7a370c69..3ab7ce76 100644 --- a/test/resolvers/event-bulk-update-assignee.test.ts +++ b/test/resolvers/event-bulk-update-assignee.test.ts @@ -13,6 +13,7 @@ jest.mock('../../src/resolvers/helpers/eventsFactory', () => ({ })); import getEventsFactory from '../../src/resolvers/helpers/eventsFactory'; +import sendPersonalNotification from '../../src/utils/personalNotifications'; // eslint-disable-next-line @typescript-eslint/no-var-requires const eventResolvers = require('../../src/resolvers/event') as { EventsMutations: { @@ -90,6 +91,19 @@ describe('EventsMutations.bulkUpdateAssignee', () => { [ '507f1f77bcf86cd799439011' ], 'assignee-1' ); + expect(sendPersonalNotification).toHaveBeenCalledTimes(1); + expect(sendPersonalNotification).toHaveBeenCalledWith( + expect.objectContaining({ id: 'assignee-1' }), + expect.objectContaining({ + type: 'assignee', + payload: expect.objectContaining({ + assigneeId: 'assignee-1', + projectId: 'p1', + whoAssignedId: 'u1', + eventId: '507f1f77bcf86cd799439011', + }), + }) + ); }); it('should validate ids on resolver level and merge invalid ids into failedEventIds', async () => { From 93fd72839e83b89f79b9d123a3d30ebb8e747a79 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:22:33 +0000 Subject: [PATCH 2/2] Bump version up to 1.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a468592..2eb5eb56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.5.2", + "version": "1.5.3", "main": "index.ts", "license": "BUSL-1.1", "scripts": {