From 3a6cf3a1319272224dbafa435bd9c4301c584039 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:56:06 +0300 Subject: [PATCH 1/2] feat(events): enhance assignee filtering with sentinel values for unassigned and any assignee --- src/models/eventsFactory.js | 42 +++++++++++++++++-- src/typeDefs/project.ts | 2 +- .../project-daily-events-portion.test.ts | 32 ++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 1a3df725..916fb9bb 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -340,7 +340,7 @@ class EventsFactory extends Factory { ? Object.fromEntries( Object .entries(filters) - .filter(([mark]) => markFilters.includes(mark)) + .filter(([ mark ]) => markFilters.includes(mark)) .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) ) : {}; @@ -364,9 +364,43 @@ class EventsFactory extends Factory { } : {}; - const assigneeFilter = assignee - ? { 'event.assignee': String(assignee) } - : {}; + /** + * Sentinel values from garage assignee filter (not user ids) + */ + const FILTER_UNASSIGNED = '__filter_unassigned__'; + const FILTER_ANY_ASSIGNEE = '__filter_any_assignee__'; + + const assigneeFilter = (() => { + if (!assignee) { + return {}; + } + if (assignee === FILTER_UNASSIGNED) { + /** + * Use $and so this does not collide with searchFilter’s top-level $or in $match spread + */ + return { + $and: [ + { + $or: [ + { 'event.assignee': { $exists: false } }, + { 'event.assignee': null }, + { 'event.assignee': '' }, + ], + }, + ], + }; + } + if (assignee === FILTER_ANY_ASSIGNEE) { + return { + 'event.assignee': { + $exists: true, + $nin: [null, ''], + }, + }; + } + + return { 'event.assignee': String(assignee) }; + })(); pipeline.push( /** diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index 494cca39..1e7135e9 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -353,7 +353,7 @@ type Project { release: String """ - User id to filter events by assignee + Filter by assignee: workspace user id, or sentinels __filter_unassigned__ (no assignee) / __filter_any_assignee__ (has assignee) """ assignee: ID ): DailyEventsPortion diff --git a/test/resolvers/project-daily-events-portion.test.ts b/test/resolvers/project-daily-events-portion.test.ts index 93c2a96b..23e2d071 100644 --- a/test/resolvers/project-daily-events-portion.test.ts +++ b/test/resolvers/project-daily-events-portion.test.ts @@ -55,6 +55,38 @@ describe('Project resolver dailyEventsPortion', () => { ); }); + it('should pass assignee sentinel for unassigned filter to factory', async () => { + const findDailyEventsPortion = jest.fn().mockResolvedValue({ + nextCursor: null, + dailyEvents: [], + }); + (getEventsFactory as unknown as jest.Mock).mockReturnValue({ + findDailyEventsPortion, + }); + + const project = { _id: 'project-1' }; + const args = { + limit: 50, + nextCursor: null, + sort: 'BY_DATE', + filters: {}, + search: '', + assignee: '__filter_unassigned__', + }; + + await projectResolver.Project.dailyEventsPortion(project, args, {}); + + expect(findDailyEventsPortion).toHaveBeenCalledWith( + 50, + null, + 'BY_DATE', + {}, + '', + undefined, + '__filter_unassigned__' + ); + }); + it('should keep old call shape when assignee is not provided', async () => { const findDailyEventsPortion = jest.fn().mockResolvedValue({ nextCursor: null, From 7dcbc3e12db1de3d6a93012abdb3668555d15b47 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:00:06 +0000 Subject: [PATCH 2/2] Bump version up to 1.4.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 126150a4..00cd7e34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.4.10", + "version": "1.4.11", "main": "index.ts", "license": "BUSL-1.1", "scripts": {