Skip to content

Commit afaf103

Browse files
chore(event): normalize event payload titles (#638)
* 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. * Bump version up to 1.4.12 * cover empty and blank title with test * lint --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 03761f1 commit afaf103

File tree

3 files changed

+154
-2
lines changed

3 files changed

+154
-2
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.4.11",
3+
"version": "1.4.12",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,54 @@ 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+
}
2371

2472
/**
2573
* See all types and fields here {@see ../typeDefs/project.graphql}
@@ -604,6 +652,8 @@ module.exports = {
604652
assignee
605653
);
606654

655+
normalizeDailyEventsPayloadTitle(dailyEventsPortion, project._id);
656+
607657
return dailyEventsPortion;
608658
},
609659

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

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

0 commit comments

Comments
 (0)