Skip to content

Commit 5de9ba1

Browse files
committed
Normalize daily event payload titles
Ensure daily events always have a non-empty payload.title by adding normalizeDailyEventsPayloadTitle in src/resolvers/project.js; when missing it sets a fallback title 'Unknown' and logs a warning with identifiers. Integrates the normalizer into Project.dailyEventsPortion. Adds unit tests to verify fallback behavior and that valid titles are preserved. Also bumps @hawk.so/nodejs to 3.3.2 in package.json.
1 parent 03761f1 commit 5de9ba1

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@graphql-tools/merge": "^8.3.1",
4242
"@graphql-tools/schema": "^8.5.1",
4343
"@graphql-tools/utils": "^8.9.0",
44-
"@hawk.so/nodejs": "^3.3.1",
44+
"@hawk.so/nodejs": "^3.3.2",
4545
"@hawk.so/types": "^0.5.9",
4646
"@n1ru4l/json-patch-plus": "^0.2.0",
4747
"@node-saml/node-saml": "^5.0.1",

src/resolvers/project.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,55 @@ const GROUPING_TIMESTAMP_INDEX_NAME = 'groupingTimestamp';
2020
const GROUPING_TIMESTAMP_AND_LAST_REPETITION_TIME_AND_ID_INDEX_NAME = 'groupingTimestampAndLastRepetitionTimeAndId';
2121
const GROUPING_TIMESTAMP_AND_GROUP_HASH_INDEX_NAME = 'groupingTimestampAndGroupHash';
2222
const MAX_SEARCH_QUERY_LENGTH = 50;
23+
const FALLBACK_EVENT_TITLE = 'Unknown';
24+
25+
/**
26+
* Ensures each daily event has non-empty payload title
27+
* and writes warning log with identifiers when fallback is used.
28+
*
29+
* @param {object} dailyEventsPortion - portion returned by events factory
30+
* @param {string|ObjectId} projectId - project id for logs
31+
* @returns {object}
32+
*/
33+
function normalizeDailyEventsPayloadTitle(dailyEventsPortion, projectId) {
34+
if (!dailyEventsPortion || !Array.isArray(dailyEventsPortion.dailyEvents)) {
35+
return dailyEventsPortion;
36+
}
37+
38+
dailyEventsPortion.dailyEvents = dailyEventsPortion.dailyEvents.map((dailyEvent) => {
39+
const event = dailyEvent && dailyEvent.event ? dailyEvent.event : null;
40+
const payload = event && event.payload ? event.payload : null;
41+
const hasValidTitle = payload
42+
&& typeof payload.title === 'string'
43+
&& payload.title.trim().length > 0;
44+
45+
if (hasValidTitle) {
46+
return dailyEvent;
47+
}
48+
49+
console.warn('🔴🔴🔴 [ProjectResolver.dailyEventsPortion] Missing event payload title. Fallback title applied.', {
50+
projectId: projectId ? projectId.toString() : null,
51+
dailyEventId: dailyEvent && dailyEvent.id ? dailyEvent.id.toString() : null,
52+
dailyEventGroupHash: dailyEvent && dailyEvent.groupHash ? dailyEvent.groupHash.toString() : null,
53+
eventOriginalId: event && event.originalEventId ? event.originalEventId.toString() : null,
54+
eventId: event && event._id ? event._id.toString() : null,
55+
});
56+
57+
return {
58+
...dailyEvent,
59+
event: {
60+
...(event || {}),
61+
payload: {
62+
...(payload || {}),
63+
title: FALLBACK_EVENT_TITLE,
64+
},
65+
},
66+
};
67+
});
68+
69+
return dailyEventsPortion;
70+
}
71+
2372

2473
/**
2574
* See all types and fields here {@see ../typeDefs/project.graphql}
@@ -604,6 +653,8 @@ module.exports = {
604653
assignee
605654
);
606655

656+
normalizeDailyEventsPayloadTitle(dailyEventsPortion, project._id);
657+
607658
return dailyEventsPortion;
608659
},
609660

test/resolvers/project-daily-events-portion.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,96 @@ describe('Project resolver dailyEventsPortion', () => {
118118
undefined
119119
);
120120
});
121+
122+
it('should apply fallback title when payload title is missing and log warning', async () => {
123+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
124+
const findDailyEventsPortion = jest.fn().mockResolvedValue({
125+
nextCursor: null,
126+
dailyEvents: [
127+
{
128+
id: 'daily-1',
129+
groupHash: 'group-1',
130+
event: {
131+
_id: 'repetition-1',
132+
originalEventId: 'event-1',
133+
payload: {
134+
title: null,
135+
},
136+
},
137+
},
138+
],
139+
});
140+
(getEventsFactory as unknown as jest.Mock).mockReturnValue({
141+
findDailyEventsPortion,
142+
});
143+
144+
const project = { _id: 'project-1' };
145+
const args = {
146+
limit: 10,
147+
nextCursor: null,
148+
sort: 'BY_DATE',
149+
filters: {},
150+
search: '',
151+
};
152+
153+
const result = await projectResolver.Project.dailyEventsPortion(project, args, {}) as {
154+
dailyEvents: Array<{ event: { payload: { title: string } } }>;
155+
};
156+
157+
expect(result.dailyEvents[0].event.payload.title).toBe('Unknown');
158+
expect(warnSpy).toHaveBeenCalledTimes(1);
159+
expect(warnSpy).toHaveBeenCalledWith(
160+
'[ProjectResolver.dailyEventsPortion] Missing event payload title. Fallback title applied.',
161+
expect.objectContaining({
162+
projectId: 'project-1',
163+
dailyEventId: 'daily-1',
164+
dailyEventGroupHash: 'group-1',
165+
eventOriginalId: 'event-1',
166+
eventId: 'repetition-1',
167+
})
168+
);
169+
170+
warnSpy.mockRestore();
171+
});
172+
173+
it('should keep payload title when it is valid', async () => {
174+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
175+
const findDailyEventsPortion = jest.fn().mockResolvedValue({
176+
nextCursor: null,
177+
dailyEvents: [
178+
{
179+
id: 'daily-1',
180+
groupHash: 'group-1',
181+
event: {
182+
_id: 'repetition-1',
183+
originalEventId: 'event-1',
184+
payload: {
185+
title: 'TypeError',
186+
},
187+
},
188+
},
189+
],
190+
});
191+
(getEventsFactory as unknown as jest.Mock).mockReturnValue({
192+
findDailyEventsPortion,
193+
});
194+
195+
const project = { _id: 'project-1' };
196+
const args = {
197+
limit: 10,
198+
nextCursor: null,
199+
sort: 'BY_DATE',
200+
filters: {},
201+
search: '',
202+
};
203+
204+
const result = await projectResolver.Project.dailyEventsPortion(project, args, {}) as {
205+
dailyEvents: Array<{ event: { payload: { title: string } } }>;
206+
};
207+
208+
expect(result.dailyEvents[0].event.payload.title).toBe('TypeError');
209+
expect(warnSpy).not.toHaveBeenCalled();
210+
211+
warnSpy.mockRestore();
212+
});
121213
});

0 commit comments

Comments
 (0)