From c27156313246eb50faca52208f5781d2369d391b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:31:17 +0300 Subject: [PATCH 1/5] refactor(events): simplify bulk event operations by removing updatedCount from responses and enhancing error handling --- src/models/eventsFactory.js | 128 ++++++++++++++---- src/resolvers/event.js | 43 +++--- src/resolvers/helpers/bulkEventUtils.ts | 109 +++++++++++++++ src/resolvers/helpers/bulkEvents.js | 116 ---------------- src/typeDefs/event.ts | 15 -- test/models/eventsFactory-bulk-toggle.test.ts | 76 +++-------- ...eventsFactory-bulk-update-assignee.test.ts | 19 +-- test/models/eventsFactory-bulk-visit.test.ts | 10 +- test/resolvers/bulk-events-helper.test.ts | 98 ++++---------- .../resolvers/event-bulk-toggle-marks.test.ts | 9 +- .../event-bulk-update-assignee.test.ts | 7 +- test/resolvers/event-bulk-visit.test.ts | 5 +- 12 files changed, 292 insertions(+), 343 deletions(-) create mode 100644 src/resolvers/helpers/bulkEventUtils.ts delete mode 100644 src/resolvers/helpers/bulkEvents.js diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index e7234566..27b0ca85 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -887,7 +887,7 @@ class EventsFactory extends Factory { * * @param {string[]} eventIds - original event ids * @param {string|ObjectId} userId - id of the user who is visiting events - * @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @returns {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkVisitEvents(eventIds, userId) { const { @@ -902,25 +902,53 @@ class EventsFactory extends Factory { return !visitedBy.some((visitedUserId) => String(visitedUserId) === userIdStr); }); - const updatedEventIds = docsToUpdate.map(doc => doc._id.toString()); if (docsToUpdate.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds, }; } - const updateManyResult = await collection.updateMany( - { _id: { $in: docsToUpdate.map(doc => doc._id) } }, - { $addToSet: { visitedBy: new ObjectId(userId) } } + const userObjectId = new ObjectId(userId); + const settled = await Promise.allSettled( + docsToUpdate.map(async (doc) => { + const eventId = doc._id.toString(); + const updateResult = await collection.updateOne( + { + _id: doc._id, + visitedBy: { $ne: userObjectId }, + }, + { $addToSet: { visitedBy: userObjectId } } + ); + + return { + eventId, + updated: updateResult.modifiedCount > 0, + }; + }) ); + const updatedEventIds = []; + const failedByUpdate = []; + + settled.forEach((result, index) => { + const fallbackEventId = docsToUpdate[index]._id.toString(); + + if (result.status === 'fulfilled') { + if (result.value.updated) { + updatedEventIds.push(result.value.eventId); + } else { + failedByUpdate.push(result.value.eventId); + } + } else { + failedByUpdate.push(fallbackEventId); + } + }); + return { - updatedCount: updateManyResult.modifiedCount, updatedEventIds, - failedEventIds, + failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], }; } @@ -965,7 +993,7 @@ class EventsFactory extends Factory { * * @param {string[]} eventIds - original event ids * @param {string} mark - 'resolved' | 'ignored' | 'starred' - * @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @returns {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkToggleEventMark(eventIds, mark) { const { @@ -978,7 +1006,6 @@ class EventsFactory extends Factory { const markKey = `marks.${mark}`; const allHaveMark = found.length > 0 && found.every(doc => doc.marks && doc.marks[mark]); const ops = []; - const updatedEventIds = []; for (const doc of found) { const hasMark = doc.marks && doc.marks[mark]; @@ -998,23 +1025,50 @@ class EventsFactory extends Factory { update, }, }); - updatedEventIds.push(doc._id.toString()); } if (ops.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds, }; } - const bulkResult = await collection.bulkWrite(ops, { ordered: false }); + const settled = await Promise.allSettled( + ops.map(async ({ updateOne }) => { + const eventId = updateOne.filter._id.toString(); + const updateResult = await collection.updateOne( + updateOne.filter, + updateOne.update + ); + + return { + eventId, + updated: updateResult.modifiedCount > 0, + }; + }) + ); + + const updatedEventIds = []; + const failedByUpdate = []; + + settled.forEach((result, index) => { + const fallbackEventId = ops[index].updateOne.filter._id.toString(); + + if (result.status === 'fulfilled') { + if (result.value.updated) { + updatedEventIds.push(result.value.eventId); + } else { + failedByUpdate.push(result.value.eventId); + } + } else { + failedByUpdate.push(fallbackEventId); + } + }); return { - updatedCount: bulkResult.modifiedCount + bulkResult.upsertedCount, updatedEventIds, - failedEventIds, + failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], }; } @@ -1023,7 +1077,7 @@ class EventsFactory extends Factory { * * @param {string[]} eventIds - original event ids * @param {string|null|undefined} assignee - target assignee id, null/undefined to clear - * @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @returns {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkUpdateAssignee(eventIds, assignee) { const { @@ -1034,25 +1088,51 @@ class EventsFactory extends Factory { const normalizedAssignee = assignee ? String(assignee) : ''; const docsToUpdate = found.filter(doc => String(doc.assignee || '') !== normalizedAssignee); - const updatedEventIds = docsToUpdate.map(doc => doc._id.toString()); - if (docsToUpdate.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds, }; } - const updateManyResult = await collection.updateMany( - { _id: { $in: docsToUpdate.map(doc => doc._id) } }, - { $set: { assignee: normalizedAssignee } } + const settled = await Promise.allSettled( + docsToUpdate.map(async (doc) => { + const eventId = doc._id.toString(); + const updateResult = await collection.updateOne( + { + _id: doc._id, + assignee: { $ne: normalizedAssignee }, + }, + { $set: { assignee: normalizedAssignee } } + ); + + return { + eventId, + updated: updateResult.modifiedCount > 0, + }; + }) ); + const updatedEventIds = []; + const failedByUpdate = []; + + settled.forEach((result, index) => { + const fallbackEventId = docsToUpdate[index]._id.toString(); + + if (result.status === 'fulfilled') { + if (result.value.updated) { + updatedEventIds.push(result.value.eventId); + } else { + failedByUpdate.push(result.value.eventId); + } + } else { + failedByUpdate.push(fallbackEventId); + } + }); + return { - updatedCount: updateManyResult.modifiedCount, updatedEventIds, - failedEventIds, + failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], }; } diff --git a/src/resolvers/event.js b/src/resolvers/event.js index dd6f12b6..a517b562 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -1,9 +1,10 @@ const getEventsFactory = require('./helpers/eventsFactory').default; const { - fireAndForgetAssigneeNotifications, parseBulkEventIds, - mergeFailedEventIds, -} = require('./helpers/bulkEvents'); + withMergedInvalidEventIds, + enqueueAssigneeNotification, + enqueueBulkAssigneeNotifications, +} = require('./helpers/bulkEventUtils'); const { aiService } = require('../services/ai'); const { UserInputError } = require('apollo-server-express'); const { ObjectId } = require('mongodb'); @@ -148,14 +149,13 @@ module.exports = { * @param {string} projectId - project id * @param {string[]} eventIds - original event ids * @param {UserInContext} user - user context - * @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @returns {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkVisitEvents(_obj, { projectId, eventIds }, { user, ...context }) { const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds); if (validEventIds.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds: invalidEventIds, }; @@ -164,10 +164,7 @@ module.exports = { const factory = getEventsFactory(context, projectId); const result = await factory.bulkVisitEvents(validEventIds, user.id); - return { - ...result, - failedEventIds: mergeFailedEventIds(result, invalidEventIds), - }; + return withMergedInvalidEventIds(result, invalidEventIds); }, /** @@ -196,14 +193,13 @@ module.exports = { * @param {string[]} eventIds - original event ids * @param {string} mark - EventMark enum value * @param {object} context - gql context - * @return {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @return {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkToggleEventMarks(_obj, { projectId, eventIds, mark }, context) { const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds); if (validEventIds.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds: invalidEventIds, }; @@ -212,10 +208,7 @@ module.exports = { const factory = getEventsFactory(context, projectId); const result = await factory.bulkToggleEventMark(validEventIds, mark); - return { - ...result, - failedEventIds: mergeFailedEventIds(result, invalidEventIds), - }; + return withMergedInvalidEventIds(result, invalidEventIds); }, /** @@ -261,12 +254,12 @@ module.exports = { const assigneeData = await factories.usersFactory.dataLoaders.userById.load(assignee); - fireAndForgetAssigneeNotifications({ + enqueueAssigneeNotification({ assigneeData, - eventIds: [ eventId ], - projectId, assigneeId: assignee, + projectId, whoAssignedId: user.id, + eventId, }); return { @@ -300,7 +293,7 @@ module.exports = { * @param {ResolverObj} _obj - resolver context * @param {BulkUpdateAssigneeInput} input - object of arguments * @param factories - factories for working with models - * @return {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>} + * @return {Promise<{ updatedEventIds: string[], failedEventIds: string[] }>} */ async bulkUpdateAssignee(_obj, { input }, { factories, user, ...context }) { const { projectId, eventIds, assignee } = input; @@ -309,7 +302,6 @@ module.exports = { if (validEventIds.length === 0) { return { - updatedCount: 0, updatedEventIds: [], failedEventIds: invalidEventIds, }; @@ -340,18 +332,15 @@ module.exports = { } const result = await factory.bulkUpdateAssignee(validEventIds, assignee); - const resultWithInvalid = { - ...result, - failedEventIds: mergeFailedEventIds(result, invalidEventIds), - }; + const resultWithInvalid = withMergedInvalidEventIds(result, invalidEventIds); if (assignee && resultWithInvalid.updatedEventIds.length > 0) { - fireAndForgetAssigneeNotifications({ + enqueueBulkAssigneeNotifications({ assigneeData, - eventIds: resultWithInvalid.updatedEventIds, - projectId, assigneeId: assignee, + projectId, whoAssignedId: user.id, + eventIds: resultWithInvalid.updatedEventIds, }); } diff --git a/src/resolvers/helpers/bulkEventUtils.ts b/src/resolvers/helpers/bulkEventUtils.ts new file mode 100644 index 00000000..99356a22 --- /dev/null +++ b/src/resolvers/helpers/bulkEventUtils.ts @@ -0,0 +1,109 @@ +import { UserInputError } from 'apollo-server-express'; +import { ObjectId } from 'mongodb'; +import sendPersonalNotification from '../../utils/personalNotifications'; +import type { UserDBScheme } from '@hawk.so/types'; +import { SenderWorkerTaskType } from '../../types/userNotifications/task-type'; + +/** + * Validate and normalize bulk event ids from resolver input. + * + * @param eventIds - raw event ids from GraphQL input + */ +export function parseBulkEventIds(eventIds: string[]): { validEventIds: string[]; invalidEventIds: string[] } { + if (!eventIds || !eventIds.length) { + throw new UserInputError('eventIds must contain at least one id'); + } + + const uniqueEventIds = [ ...new Set(eventIds.map(id => String(id))) ]; + const invalidEventIds: string[] = []; + const validEventIds: string[] = []; + + uniqueEventIds.forEach((id) => { + if (ObjectId.isValid(id)) { + validEventIds.push(id); + } else { + invalidEventIds.push(id); + } + }); + + return { + validEventIds, + invalidEventIds, + }; +} + +/** + * Merge failed ids returned by factory with invalid ids from resolver validation. + */ +export function mergeFailedEventIds(result: { failedEventIds?: string[] }, invalidEventIds: string[]): string[] { + return [ ...new Set([ ...(result.failedEventIds || []), ...invalidEventIds ]) ]; +} + +/** + * Merge resolver-level invalid ids into factory-level failed ids. + */ +export function withMergedInvalidEventIds( + result: T, + invalidEventIds: string[] +): T & { failedEventIds: string[] } { + return { + ...result, + failedEventIds: mergeFailedEventIds(result, invalidEventIds), + }; +} + +type AssigneeNotificationParams = { + assigneeData: UserDBScheme | null; + assigneeId: string; + projectId: string; + whoAssignedId: string; + eventId: string; +}; + +/** + * Enqueue one assignee notification without blocking resolver response. + */ +export function enqueueAssigneeNotification({ + assigneeData, + assigneeId, + projectId, + whoAssignedId, + eventId, +}: AssigneeNotificationParams): void { + if (!assigneeData) { + return; + } + + void sendPersonalNotification(assigneeData, { + type: SenderWorkerTaskType.Assignee, + payload: { + assigneeId, + projectId, + whoAssignedId, + eventId, + }, + }).catch((error: unknown) => { + console.error('Failed to enqueue assignee notification', error); + }); +} + +/** + * Enqueue assignee notifications for all updated original events. + */ +export function enqueueBulkAssigneeNotifications({ + assigneeData, + assigneeId, + projectId, + whoAssignedId, + eventIds, +}: Omit & { eventIds: string[] }): void { + eventIds.forEach((eventId) => { + enqueueAssigneeNotification({ + assigneeData, + assigneeId, + projectId, + whoAssignedId, + eventId, + }); + }); +} diff --git a/src/resolvers/helpers/bulkEvents.js b/src/resolvers/helpers/bulkEvents.js deleted file mode 100644 index 50dedb6f..00000000 --- a/src/resolvers/helpers/bulkEvents.js +++ /dev/null @@ -1,116 +0,0 @@ -const sendPersonalNotification = require('../../utils/personalNotifications').default; -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) - * - * @param {object} args - notification args - * @param {object} args.assigneeData - assigned user data - * @param {string[]} args.eventIds - original event ids - * @param {string} args.projectId - project id - * @param {string} args.assigneeId - assignee id - * @param {string} args.whoAssignedId - user id who performed assignment - * @returns {void} - */ -function fireAndForgetAssigneeNotifications({ - assigneeData, - eventIds, - projectId, - assigneeId, - whoAssignedId, -}) { - if (!assigneeData) { - console.error('Failed to enqueue assignee notifications: assignee data is empty'); - - return; - } - - 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) { - 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) => { - console.error('Failed to enqueue assignee notifications', error); - }); -} - -/** - * Validate and normalize bulk event ids from resolver input. - * - * @param {string[]} eventIds - raw event ids from GraphQL input - * @returns {{ validEventIds: string[], invalidEventIds: string[] }} - */ -function parseBulkEventIds(eventIds) { - if (!eventIds || !eventIds.length) { - throw new UserInputError('eventIds must contain at least one id'); - } - - const uniqueEventIds = [ ...new Set(eventIds.map(id => String(id))) ]; - const invalidEventIds = []; - const validEventIds = []; - - uniqueEventIds.forEach((id) => { - if (ObjectId.isValid(id)) { - validEventIds.push(id); - } else { - invalidEventIds.push(id); - } - }); - - return { - validEventIds, - invalidEventIds, - }; -} - -/** - * Merge failed ids returned by factory with invalid ids from resolver validation. - * - * @param {{ failedEventIds?: string[] }} result - factory response - * @param {string[]} invalidEventIds - invalid ids detected on resolver level - * @returns {string[]} - */ -function mergeFailedEventIds(result, invalidEventIds) { - return [ ...new Set([...(result.failedEventIds || []), ...invalidEventIds]) ]; -} - -module.exports = { - fireAndForgetAssigneeNotifications, - parseBulkEventIds, - mergeFailedEventIds, -}; diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 1d792aff..1a3de27c 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -470,11 +470,6 @@ type RemoveAssigneeResponse { } type BulkUpdateAssigneeResponse { - """ - Number of events updated in the database - """ - updatedCount: Int! - """ Original event ids actually updated in this operation """ @@ -490,11 +485,6 @@ type BulkUpdateAssigneeResponse { Result of bulk toggling event marks (resolve / ignore / starred) """ type BulkToggleEventMarksResult { - """ - Number of events updated in the database - """ - updatedCount: Int! - """ Original event ids actually toggled in this operation """ @@ -510,11 +500,6 @@ type BulkToggleEventMarksResult { Result of bulk marking events as viewed """ type BulkVisitEventsResult { - """ - Number of events updated in the database - """ - updatedCount: Int! - """ Original event ids actually updated in this operation """ diff --git a/test/models/eventsFactory-bulk-toggle.test.ts b/test/models/eventsFactory-bulk-toggle.test.ts index 6fa85a1c..0da1a7ca 100644 --- a/test/models/eventsFactory-bulk-toggle.test.ts +++ b/test/models/eventsFactory-bulk-toggle.test.ts @@ -3,7 +3,7 @@ import { ObjectId } from 'mongodb'; const collectionMock = { find: jest.fn(), - bulkWrite: jest.fn(), + updateOne: jest.fn(), }; jest.mock('../../src/redisHelper', () => ({ @@ -40,13 +40,7 @@ describe('EventsFactory.bulkToggleEventMark', () => { beforeEach(() => { jest.clearAllMocks(); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 0, - upsertedCount: 0, - insertedCount: 0, - matchedCount: 0, - deletedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 0 }); }); it('should support starred mark', async () => { @@ -62,19 +56,13 @@ describe('EventsFactory.bulkToggleEventMark', () => { }, ]), }); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 1, - upsertedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkToggleEventMark([ id.toString() ], 'starred'); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ id.toString() ]); - const ops = collectionMock.bulkWrite.mock.calls[0][0]; - - expect(ops).toHaveLength(1); - expect(ops[0].updateOne.update).toEqual( + expect(collectionMock.updateOne).toHaveBeenCalledWith( + { _id: id }, expect.objectContaining({ $set: { 'marks.starred': expect.any(Number) }, }) @@ -94,17 +82,11 @@ describe('EventsFactory.bulkToggleEventMark', () => { }, ]), }); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 1, - upsertedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); await factory.bulkToggleEventMark([ id.toString(), id.toString(), id.toString() ], 'ignored'); - expect(collectionMock.bulkWrite).toHaveBeenCalledTimes(1); - const ops = collectionMock.bulkWrite.mock.calls[0][0]; - - expect(ops).toHaveLength(1); + expect(collectionMock.updateOne).toHaveBeenCalledTimes(1); }); it('should list valid but missing document ids in failedEventIds', async () => { @@ -117,10 +99,9 @@ describe('EventsFactory.bulkToggleEventMark', () => { const result = await factory.bulkToggleEventMark([ missing.toString() ], 'ignored'); - expect(result.updatedCount).toBe(0); expect(result.updatedEventIds).toEqual([]); expect(result.failedEventIds).toContain(missing.toString()); - expect(collectionMock.bulkWrite).not.toHaveBeenCalled(); + expect(collectionMock.updateOne).not.toHaveBeenCalled(); }); it('should set mark only on events that do not have it when selection is mixed', async () => { @@ -135,20 +116,14 @@ describe('EventsFactory.bulkToggleEventMark', () => { { _id: b, marks: {} }, ]), }); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 1, - upsertedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored'); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ b.toString() ]); - const ops = collectionMock.bulkWrite.mock.calls[0][0]; - - expect(ops).toHaveLength(1); - expect(ops[0].updateOne.filter._id).toEqual(b); - expect(ops[0].updateOne.update).toEqual( + expect(collectionMock.updateOne).toHaveBeenCalledTimes(1); + expect(collectionMock.updateOne).toHaveBeenCalledWith( + { _id: b }, expect.objectContaining({ $set: { 'marks.ignored': expect.any(Number) }, }) @@ -167,20 +142,14 @@ describe('EventsFactory.bulkToggleEventMark', () => { { _id: b, marks: { resolved: 2 } }, ]), }); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 2, - upsertedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'resolved'); - expect(result.updatedCount).toBe(2); expect(result.updatedEventIds).toEqual([ a.toString(), b.toString() ]); - const ops = collectionMock.bulkWrite.mock.calls[0][0]; - - expect(ops).toHaveLength(2); - expect(ops[0].updateOne.update).toEqual({ $unset: { 'marks.resolved': '' } }); - expect(ops[1].updateOne.update).toEqual({ $unset: { 'marks.resolved': '' } }); + expect(collectionMock.updateOne).toHaveBeenCalledTimes(2); + expect(collectionMock.updateOne).toHaveBeenNthCalledWith(1, { _id: a }, { $unset: { 'marks.resolved': '' } }); + expect(collectionMock.updateOne).toHaveBeenNthCalledWith(2, { _id: b }, { $unset: { 'marks.resolved': '' } }); }); it('should not remove mark from a subset when only some of the found events have the mark', async () => { @@ -195,19 +164,14 @@ describe('EventsFactory.bulkToggleEventMark', () => { { _id: b, marks: {} }, ]), }); - collectionMock.bulkWrite.mockResolvedValue({ - modifiedCount: 1, - upsertedCount: 0, - }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored'); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ b.toString() ]); - const ops = collectionMock.bulkWrite.mock.calls[0][0]; - - expect(ops).toHaveLength(1); - expect(ops[0].updateOne.update).toEqual( + expect(collectionMock.updateOne).toHaveBeenCalledTimes(1); + expect(collectionMock.updateOne).toHaveBeenCalledWith( + { _id: b }, expect.objectContaining({ $set: { 'marks.ignored': expect.any(Number) } }) ); }); diff --git a/test/models/eventsFactory-bulk-update-assignee.test.ts b/test/models/eventsFactory-bulk-update-assignee.test.ts index cce853bb..5a85f0c5 100644 --- a/test/models/eventsFactory-bulk-update-assignee.test.ts +++ b/test/models/eventsFactory-bulk-update-assignee.test.ts @@ -3,7 +3,7 @@ import { ObjectId } from 'mongodb'; const collectionMock = { find: jest.fn(), - updateMany: jest.fn(), + updateOne: jest.fn(), }; jest.mock('../../src/redisHelper', () => ({ @@ -40,7 +40,7 @@ describe('EventsFactory.bulkUpdateAssignee', () => { beforeEach(() => { jest.clearAllMocks(); - collectionMock.updateMany.mockResolvedValue({ modifiedCount: 0 }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 0 }); }); it('should update only events with changed assignee', async () => { @@ -54,14 +54,13 @@ describe('EventsFactory.bulkUpdateAssignee', () => { { _id: b, assignee: '' }, ]), }); - collectionMock.updateMany.mockResolvedValue({ modifiedCount: 1 }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkUpdateAssignee([ a.toString(), b.toString() ], 'user-1'); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ b.toString() ]); expect(result.failedEventIds).toEqual([]); - expect(collectionMock.updateMany).toHaveBeenCalledTimes(1); + expect(collectionMock.updateOne).toHaveBeenCalledTimes(1); }); it('should clear assignee with null value', async () => { @@ -71,14 +70,16 @@ describe('EventsFactory.bulkUpdateAssignee', () => { collectionMock.find.mockReturnValue({ toArray: () => Promise.resolve([{ _id: a, assignee: 'user-1' }]), }); - collectionMock.updateMany.mockResolvedValue({ modifiedCount: 1 }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkUpdateAssignee([ a.toString() ], null); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ a.toString() ]); - expect(collectionMock.updateMany).toHaveBeenCalledWith( - { _id: { $in: [ a ] } }, + expect(collectionMock.updateOne).toHaveBeenCalledWith( + { + _id: a, + assignee: { $ne: '' }, + }, { $set: { assignee: '' } } ); }); diff --git a/test/models/eventsFactory-bulk-visit.test.ts b/test/models/eventsFactory-bulk-visit.test.ts index 7a1b1627..bb4bb35c 100644 --- a/test/models/eventsFactory-bulk-visit.test.ts +++ b/test/models/eventsFactory-bulk-visit.test.ts @@ -3,7 +3,7 @@ import { ObjectId } from 'mongodb'; const collectionMock = { find: jest.fn(), - updateMany: jest.fn(), + updateOne: jest.fn(), }; jest.mock('../../src/redisHelper', () => ({ @@ -40,7 +40,7 @@ describe('EventsFactory.bulkVisitEvents', () => { beforeEach(() => { jest.clearAllMocks(); - collectionMock.updateMany.mockResolvedValue({ modifiedCount: 0 }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 0 }); }); it('should mark only not-yet-visited events', async () => { @@ -55,11 +55,10 @@ describe('EventsFactory.bulkVisitEvents', () => { { _id: b, visitedBy: [] }, ]), }); - collectionMock.updateMany.mockResolvedValue({ modifiedCount: 1 }); + collectionMock.updateOne.mockResolvedValue({ modifiedCount: 1 }); const result = await factory.bulkVisitEvents([ a.toString(), b.toString() ], userId.toString()); - expect(result.updatedCount).toBe(1); expect(result.updatedEventIds).toEqual([ b.toString() ]); expect(result.failedEventIds).toEqual([]); }); @@ -74,9 +73,8 @@ describe('EventsFactory.bulkVisitEvents', () => { const result = await factory.bulkVisitEvents([ missing.toString() ], new ObjectId().toString()); - expect(result.updatedCount).toBe(0); expect(result.updatedEventIds).toEqual([]); expect(result.failedEventIds).toEqual([ missing.toString() ]); - expect(collectionMock.updateMany).not.toHaveBeenCalled(); + expect(collectionMock.updateOne).not.toHaveBeenCalled(); }); }); diff --git a/test/resolvers/bulk-events-helper.test.ts b/test/resolvers/bulk-events-helper.test.ts index a9394f7f..c29f6a25 100644 --- a/test/resolvers/bulk-events-helper.test.ts +++ b/test/resolvers/bulk-events-helper.test.ts @@ -1,86 +1,36 @@ 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; +const { parseBulkEventIds, mergeFailedEventIds } = require('../../src/resolvers/helpers/bulkEventUtils') as { + parseBulkEventIds: (eventIds: string[]) => { validEventIds: string[]; invalidEventIds: string[] }; + mergeFailedEventIds: ( + result: { failedEventIds?: string[] }, + invalidEventIds: string[] + ) => string[]; }; -describe('fireAndForgetAssigneeNotifications', () => { - let consoleErrorSpy: jest.SpyInstance; - - beforeEach(() => { - jest.clearAllMocks(); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - }); +describe('bulkEvents helper', () => { + it('should split valid and invalid ids and deduplicate them', () => { + const validA = '507f1f77bcf86cd799439011'; + const validB = '507f1f77bcf86cd799439012'; + const invalid = 'bad-id'; + const result = parseBulkEventIds([ validA, validA, invalid, validB ]); - 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', + expect(result).toEqual({ + validEventIds: [ validA, validB ], + invalidEventIds: [ invalid ], }); + }); - 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 merge failed ids from factory and invalid resolver ids', () => { + const result = mergeFailedEventIds( + { failedEventIds: [ '507f1f77bcf86cd799439011' ] }, + [ 'bad-id', '507f1f77bcf86cd799439011' ] ); - }); - 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(result).toEqual([ '507f1f77bcf86cd799439011', 'bad-id' ]); + }); - expect(sendPersonalNotification).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to enqueue assignee notifications: assignee data is empty' - ); + it('should throw when eventIds is empty', () => { + expect(() => parseBulkEventIds([])).toThrow('eventIds must contain at least one id'); }); }); diff --git a/test/resolvers/event-bulk-toggle-marks.test.ts b/test/resolvers/event-bulk-toggle-marks.test.ts index 1b66daff..1e568339 100644 --- a/test/resolvers/event-bulk-toggle-marks.test.ts +++ b/test/resolvers/event-bulk-toggle-marks.test.ts @@ -13,7 +13,7 @@ const eventResolvers = require('../../src/resolvers/event') as { o: unknown, args: { projectId: string; eventIds: string[]; mark: string }, ctx: unknown - ) => Promise<{ updatedCount: number; updatedEventIds: string[]; failedEventIds: string[] }>; + ) => Promise<{ updatedEventIds: string[]; failedEventIds: string[] }>; }; }; @@ -42,7 +42,7 @@ describe('Mutation.bulkToggleEventMarks', () => { }); it('should call factory with original event ids and return its result', async () => { - const payload = { updatedCount: 2, updatedEventIds: [ 'a', 'b' ], failedEventIds: [ 'x' ] }; + const payload = { updatedEventIds: [ 'a', 'b' ], failedEventIds: [ 'x' ] }; bulkToggleEventMark.mockResolvedValue(payload); @@ -65,7 +65,7 @@ describe('Mutation.bulkToggleEventMarks', () => { }); it('should allow starred mark for bulk toggle', async () => { - const payload = { updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [] }; + const payload = { updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [] }; bulkToggleEventMark.mockResolvedValue(payload); @@ -88,7 +88,6 @@ describe('Mutation.bulkToggleEventMarks', () => { it('should validate ids on resolver level and merge invalid ids into failedEventIds', async () => { bulkToggleEventMark.mockResolvedValue({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [ '507f1f77bcf86cd799439099' ], }); @@ -108,7 +107,6 @@ describe('Mutation.bulkToggleEventMarks', () => { 'ignored' ); expect(result).toEqual({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [ '507f1f77bcf86cd799439099', 'invalid-id' ], }); @@ -127,7 +125,6 @@ describe('Mutation.bulkToggleEventMarks', () => { expect(bulkToggleEventMark).not.toHaveBeenCalled(); expect(result).toEqual({ - updatedCount: 0, updatedEventIds: [], failedEventIds: [ 'bad-1', 'bad-2' ], }); diff --git a/test/resolvers/event-bulk-update-assignee.test.ts b/test/resolvers/event-bulk-update-assignee.test.ts index 9fcc093d..a00aa388 100644 --- a/test/resolvers/event-bulk-update-assignee.test.ts +++ b/test/resolvers/event-bulk-update-assignee.test.ts @@ -21,7 +21,7 @@ const eventResolvers = require('../../src/resolvers/event') as { o: unknown, args: { input: { projectId: string; eventIds: string[]; assignee?: string | null } }, ctx: any - ) => Promise<{ updatedCount: number; updatedEventIds: string[]; failedEventIds: string[] }>; + ) => Promise<{ updatedEventIds: string[]; failedEventIds: string[] }>; }; }; @@ -57,7 +57,6 @@ describe('EventsMutations.bulkUpdateAssignee', () => { bulkUpdateAssignee, }); bulkUpdateAssignee.mockResolvedValue({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [], }); @@ -105,7 +104,6 @@ describe('EventsMutations.bulkUpdateAssignee', () => { ctx ); - expect(result.updatedCount).toBe(1); expect(bulkUpdateAssignee).toHaveBeenCalledWith( [ '507f1f77bcf86cd799439011' ], ASSIGNEE_ID @@ -127,7 +125,6 @@ describe('EventsMutations.bulkUpdateAssignee', () => { it('should validate ids on resolver level and merge invalid ids into failedEventIds', async () => { bulkUpdateAssignee.mockResolvedValue({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [ '507f1f77bcf86cd799439099' ], }); @@ -149,7 +146,6 @@ describe('EventsMutations.bulkUpdateAssignee', () => { ASSIGNEE_ID ); expect(result).toEqual({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [ '507f1f77bcf86cd799439099', 'invalid-id' ], }); @@ -170,7 +166,6 @@ describe('EventsMutations.bulkUpdateAssignee', () => { expect(bulkUpdateAssignee).not.toHaveBeenCalled(); expect(result).toEqual({ - updatedCount: 0, updatedEventIds: [], failedEventIds: [ 'bad-1', 'bad-2' ], }); diff --git a/test/resolvers/event-bulk-visit.test.ts b/test/resolvers/event-bulk-visit.test.ts index e99ce988..dade08b6 100644 --- a/test/resolvers/event-bulk-visit.test.ts +++ b/test/resolvers/event-bulk-visit.test.ts @@ -13,7 +13,7 @@ const eventResolvers = require('../../src/resolvers/event') as { o: unknown, args: { projectId: string; eventIds: string[] }, ctx: any - ) => Promise<{ updatedCount: number; updatedEventIds: string[]; failedEventIds: string[] }>; + ) => Promise<{ updatedEventIds: string[]; failedEventIds: string[] }>; }; }; @@ -31,7 +31,6 @@ describe('Mutation.bulkVisitEvents', () => { it('should call factory with valid ids only and merge invalid ids', async () => { bulkVisitEvents.mockResolvedValue({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439012' ], failedEventIds: [ '507f1f77bcf86cd799439099' ], }); @@ -47,7 +46,6 @@ describe('Mutation.bulkVisitEvents', () => { '507f1f77bcf86cd799439011' ); expect(result).toEqual({ - updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439012' ], failedEventIds: [ '507f1f77bcf86cd799439099', 'bad-id' ], }); @@ -62,7 +60,6 @@ describe('Mutation.bulkVisitEvents', () => { expect(bulkVisitEvents).not.toHaveBeenCalled(); expect(result).toEqual({ - updatedCount: 0, updatedEventIds: [], failedEventIds: [ 'bad-1', 'bad-2' ], }); From 57582bdb3988d34f8216de6876c51293c14625cd Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:33:49 +0300 Subject: [PATCH 2/5] refactor(events): rename response types for bulk event operations to improve clarity --- src/typeDefs/event.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 1a3de27c..e4e63f07 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -482,9 +482,9 @@ type BulkUpdateAssigneeResponse { } """ -Result of bulk toggling event marks (resolve / ignore / starred) +Response of bulk toggling event marks (resolve / ignore / starred) """ -type BulkToggleEventMarksResult { +type BulkToggleEventMarksResponse { """ Original event ids actually toggled in this operation """ @@ -497,9 +497,9 @@ type BulkToggleEventMarksResult { } """ -Result of bulk marking events as viewed +Response of bulk marking events as viewed """ -type BulkVisitEventsResult { +type BulkVisitEventsResponse { """ Original event ids actually updated in this operation """ @@ -563,7 +563,7 @@ extend type Mutation { Original event ids """ eventIds: [ID!]! - ): BulkVisitEventsResult! @requireUserInWorkspace + ): BulkVisitEventsResponse! @requireUserInWorkspace """ Mutation sets or unsets passed mark to event @@ -606,7 +606,7 @@ extend type Mutation { otherwise set it on every selected event that does not have it yet. """ mark: EventMark! - ): BulkToggleEventMarksResult! @requireUserInWorkspace + ): BulkToggleEventMarksResponse! @requireUserInWorkspace """ Namespace that contains only mutations related to the events From 1e13972419db9f27cd43449cfa2667d4da89ebed Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:48:31 +0300 Subject: [PATCH 3/5] fix: lint --- src/models/eventsFactory.js | 26 +++++++++++-- src/resolvers/event.js | 33 ++++++++++------ src/resolvers/helpers/bulkEventUtils.ts | 46 ++--------------------- test/resolvers/bulk-events-helper.test.ts | 15 +------- 4 files changed, 48 insertions(+), 72 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 27b0ca85..fdf37211 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -95,7 +95,7 @@ class EventsFactory extends Factory { /** * Creates Event instance - * @param {ObjectId} projectId + * @param {ObjectId} projectId - project id */ constructor(projectId) { super(); @@ -948,7 +948,7 @@ class EventsFactory extends Factory { return { updatedEventIds, - failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], + failedEventIds: this._mergeFailedEventIds(failedEventIds, failedByUpdate), }; } @@ -1068,7 +1068,7 @@ class EventsFactory extends Factory { return { updatedEventIds, - failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], + failedEventIds: this._mergeFailedEventIds(failedEventIds, failedByUpdate), }; } @@ -1088,6 +1088,7 @@ class EventsFactory extends Factory { const normalizedAssignee = assignee ? String(assignee) : ''; const docsToUpdate = found.filter(doc => String(doc.assignee || '') !== normalizedAssignee); + if (docsToUpdate.length === 0) { return { updatedEventIds: [], @@ -1132,7 +1133,7 @@ class EventsFactory extends Factory { return { updatedEventIds, - failedEventIds: [ ...new Set([ ...failedEventIds, ...failedByUpdate ]) ], + failedEventIds: this._mergeFailedEventIds(failedEventIds, failedByUpdate), }; } @@ -1207,6 +1208,23 @@ class EventsFactory extends Factory { }; } + /** + * Merge two failed ids collections preserving uniqueness. + * + * @param {string[]} baseFailedEventIds - existing failed ids + * @param {string[]} extraFailedEventIds - failed ids collected from update results + * @returns {string[]} + */ + _mergeFailedEventIds(baseFailedEventIds, extraFailedEventIds) { + const mergedFailedEventIds = new Set(baseFailedEventIds); + + extraFailedEventIds.forEach((eventId) => { + mergedFailedEventIds.add(eventId); + }); + + return Array.from(mergedFailedEventIds); + } + /** * Compose event with repetition * diff --git a/src/resolvers/event.js b/src/resolvers/event.js index a517b562..fa11c888 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -1,9 +1,7 @@ const getEventsFactory = require('./helpers/eventsFactory').default; const { parseBulkEventIds, - withMergedInvalidEventIds, enqueueAssigneeNotification, - enqueueBulkAssigneeNotifications, } = require('./helpers/bulkEventUtils'); const { aiService } = require('../services/ai'); const { UserInputError } = require('apollo-server-express'); @@ -163,8 +161,12 @@ module.exports = { const factory = getEventsFactory(context, projectId); const result = await factory.bulkVisitEvents(validEventIds, user.id); + const failedEventIds = Array.from(new Set([...(result.failedEventIds || []), ...invalidEventIds])); - return withMergedInvalidEventIds(result, invalidEventIds); + return { + ...result, + failedEventIds, + }; }, /** @@ -207,8 +209,12 @@ module.exports = { const factory = getEventsFactory(context, projectId); const result = await factory.bulkToggleEventMark(validEventIds, mark); + const failedEventIds = Array.from(new Set([...(result.failedEventIds || []), ...invalidEventIds])); - return withMergedInvalidEventIds(result, invalidEventIds); + return { + ...result, + failedEventIds, + }; }, /** @@ -332,15 +338,20 @@ module.exports = { } const result = await factory.bulkUpdateAssignee(validEventIds, assignee); - const resultWithInvalid = withMergedInvalidEventIds(result, invalidEventIds); + const resultWithInvalid = { + ...result, + failedEventIds: Array.from(new Set([...(result.failedEventIds || []), ...invalidEventIds])), + }; if (assignee && resultWithInvalid.updatedEventIds.length > 0) { - enqueueBulkAssigneeNotifications({ - assigneeData, - assigneeId: assignee, - projectId, - whoAssignedId: user.id, - eventIds: resultWithInvalid.updatedEventIds, + resultWithInvalid.updatedEventIds.forEach((eventId) => { + enqueueAssigneeNotification({ + assigneeData, + assigneeId: assignee, + projectId, + whoAssignedId: user.id, + eventId, + }); }); } diff --git a/src/resolvers/helpers/bulkEventUtils.ts b/src/resolvers/helpers/bulkEventUtils.ts index 99356a22..b3e34c8a 100644 --- a/src/resolvers/helpers/bulkEventUtils.ts +++ b/src/resolvers/helpers/bulkEventUtils.ts @@ -7,7 +7,8 @@ import { SenderWorkerTaskType } from '../../types/userNotifications/task-type'; /** * Validate and normalize bulk event ids from resolver input. * - * @param eventIds - raw event ids from GraphQL input + * @param {string[]} eventIds - raw event ids from GraphQL input + * @returns {object} normalized ids grouped by validity */ export function parseBulkEventIds(eventIds: string[]): { validEventIds: string[]; invalidEventIds: string[] } { if (!eventIds || !eventIds.length) { @@ -32,26 +33,6 @@ export function parseBulkEventIds(eventIds: string[]): { validEventIds: string[] }; } -/** - * Merge failed ids returned by factory with invalid ids from resolver validation. - */ -export function mergeFailedEventIds(result: { failedEventIds?: string[] }, invalidEventIds: string[]): string[] { - return [ ...new Set([ ...(result.failedEventIds || []), ...invalidEventIds ]) ]; -} - -/** - * Merge resolver-level invalid ids into factory-level failed ids. - */ -export function withMergedInvalidEventIds( - result: T, - invalidEventIds: string[] -): T & { failedEventIds: string[] } { - return { - ...result, - failedEventIds: mergeFailedEventIds(result, invalidEventIds), - }; -} - type AssigneeNotificationParams = { assigneeData: UserDBScheme | null; assigneeId: string; @@ -74,7 +55,7 @@ export function enqueueAssigneeNotification({ return; } - void sendPersonalNotification(assigneeData, { + sendPersonalNotification(assigneeData, { type: SenderWorkerTaskType.Assignee, payload: { assigneeId, @@ -86,24 +67,3 @@ export function enqueueAssigneeNotification({ console.error('Failed to enqueue assignee notification', error); }); } - -/** - * Enqueue assignee notifications for all updated original events. - */ -export function enqueueBulkAssigneeNotifications({ - assigneeData, - assigneeId, - projectId, - whoAssignedId, - eventIds, -}: Omit & { eventIds: string[] }): void { - eventIds.forEach((eventId) => { - enqueueAssigneeNotification({ - assigneeData, - assigneeId, - projectId, - whoAssignedId, - eventId, - }); - }); -} diff --git a/test/resolvers/bulk-events-helper.test.ts b/test/resolvers/bulk-events-helper.test.ts index c29f6a25..5ca4dcc9 100644 --- a/test/resolvers/bulk-events-helper.test.ts +++ b/test/resolvers/bulk-events-helper.test.ts @@ -1,11 +1,7 @@ import '../../src/env-test'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const { parseBulkEventIds, mergeFailedEventIds } = require('../../src/resolvers/helpers/bulkEventUtils') as { +const { parseBulkEventIds } = require('../../src/resolvers/helpers/bulkEventUtils') as { parseBulkEventIds: (eventIds: string[]) => { validEventIds: string[]; invalidEventIds: string[] }; - mergeFailedEventIds: ( - result: { failedEventIds?: string[] }, - invalidEventIds: string[] - ) => string[]; }; describe('bulkEvents helper', () => { @@ -21,15 +17,6 @@ describe('bulkEvents helper', () => { }); }); - it('should merge failed ids from factory and invalid resolver ids', () => { - const result = mergeFailedEventIds( - { failedEventIds: [ '507f1f77bcf86cd799439011' ] }, - [ 'bad-id', '507f1f77bcf86cd799439011' ] - ); - - expect(result).toEqual([ '507f1f77bcf86cd799439011', 'bad-id' ]); - }); - it('should throw when eventIds is empty', () => { expect(() => parseBulkEventIds([])).toThrow('eventIds must contain at least one id'); }); From 2e9fba0d61ff028257bb019911e48b2c7fa6db10 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:51:22 +0300 Subject: [PATCH 4/5] fix --- src/models/eventsFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index fdf37211..2bfb585b 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -95,7 +95,7 @@ class EventsFactory extends Factory { /** * Creates Event instance - * @param {ObjectId} projectId - project id + * @param {ObjectId} projectId */ constructor(projectId) { super(); From 0ee4684dbf4a3e3c8836212c316a090239609571 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:53:20 +0000 Subject: [PATCH 5/5] Bump version up to 1.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9aaa8e8..d484a908 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.5.0", + "version": "1.5.1", "main": "index.ts", "license": "BUSL-1.1", "scripts": {