Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.4.11",
"version": "1.4.12",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down Expand Up @@ -41,7 +41,7 @@
"@graphql-tools/merge": "^8.3.1",
"@graphql-tools/schema": "^8.5.1",
"@graphql-tools/utils": "^8.9.0",
"@hawk.so/nodejs": "^3.3.1",
"@hawk.so/nodejs": "^3.3.2",
"@hawk.so/types": "^0.5.9",
"@n1ru4l/json-patch-plus": "^0.2.0",
"@node-saml/node-saml": "^5.0.1",
Expand Down
50 changes: 50 additions & 0 deletions src/resolvers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,54 @@ const GROUPING_TIMESTAMP_INDEX_NAME = 'groupingTimestamp';
const GROUPING_TIMESTAMP_AND_LAST_REPETITION_TIME_AND_ID_INDEX_NAME = 'groupingTimestampAndLastRepetitionTimeAndId';
const GROUPING_TIMESTAMP_AND_GROUP_HASH_INDEX_NAME = 'groupingTimestampAndGroupHash';
const MAX_SEARCH_QUERY_LENGTH = 50;
const FALLBACK_EVENT_TITLE = 'Unknown';

/**
* Ensures each daily event has non-empty payload title
* and writes warning log with identifiers when fallback is used.
*
* @param {object} dailyEventsPortion - portion returned by events factory
* @param {string|ObjectId} projectId - project id for logs
* @returns {object}
*/
function normalizeDailyEventsPayloadTitle(dailyEventsPortion, projectId) {
if (!dailyEventsPortion || !Array.isArray(dailyEventsPortion.dailyEvents)) {
return dailyEventsPortion;
}

dailyEventsPortion.dailyEvents = dailyEventsPortion.dailyEvents.map((dailyEvent) => {
const event = dailyEvent && dailyEvent.event ? dailyEvent.event : null;
const payload = event && event.payload ? event.payload : null;
const hasValidTitle = payload &&
typeof payload.title === 'string' &&
payload.title.trim().length > 0;

if (hasValidTitle) {
return dailyEvent;
}

console.warn('🔴🔴🔴 [ProjectResolver.dailyEventsPortion] Missing event payload title. Fallback title applied.', {
projectId: projectId ? projectId.toString() : null,
dailyEventId: dailyEvent && dailyEvent.id ? dailyEvent.id.toString() : null,
dailyEventGroupHash: dailyEvent && dailyEvent.groupHash ? dailyEvent.groupHash.toString() : null,
eventOriginalId: event && event.originalEventId ? event.originalEventId.toString() : null,
eventId: event && event._id ? event._id.toString() : null,
});

return {
...dailyEvent,
event: {
...(event || {}),
payload: {
...(payload || {}),
title: FALLBACK_EVENT_TITLE,
},
},
};
});

return dailyEventsPortion;
}

/**
* See all types and fields here {@see ../typeDefs/project.graphql}
Expand Down Expand Up @@ -604,6 +652,8 @@ module.exports = {
assignee
);

normalizeDailyEventsPayloadTitle(dailyEventsPortion, project._id);

return dailyEventsPortion;
},

Expand Down
102 changes: 102 additions & 0 deletions test/resolvers/project-daily-events-portion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,106 @@ describe('Project resolver dailyEventsPortion', () => {
undefined
);
});

it('should apply fallback title for null, empty and blank payload titles', async () => {
const findDailyEventsPortion = jest.fn().mockResolvedValue({
nextCursor: null,
dailyEvents: [
{
id: 'daily-1',
groupHash: 'group-1',
event: {
_id: 'repetition-1',
originalEventId: 'event-1',
payload: {
title: null,
},
},
},
{
id: 'daily-2',
groupHash: 'group-2',
event: {
_id: 'repetition-2',
originalEventId: 'event-2',
payload: {
title: '',
},
},
},
{
id: 'daily-3',
groupHash: 'group-3',
event: {
_id: 'repetition-3',
originalEventId: 'event-3',
payload: {
title: ' ',
},
},
},
],
});
(getEventsFactory as unknown as jest.Mock).mockReturnValue({
findDailyEventsPortion,
});

const project = { _id: 'project-1' };
const args = {
limit: 10,
nextCursor: null,
sort: 'BY_DATE',
filters: {},
search: '',
};

const result = await projectResolver.Project.dailyEventsPortion(project, args, {}) as {
dailyEvents: Array<{ event: { payload: { title: string } } }>;
};

expect(result.dailyEvents[0].event.payload.title).toBe('Unknown');
expect(result.dailyEvents[1].event.payload.title).toBe('Unknown');
expect(result.dailyEvents[2].event.payload.title).toBe('Unknown');
});

it('should keep payload title when it is valid', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const findDailyEventsPortion = jest.fn().mockResolvedValue({
nextCursor: null,
dailyEvents: [
{
id: 'daily-1',
groupHash: 'group-1',
event: {
_id: 'repetition-1',
originalEventId: 'event-1',
payload: {
title: 'TypeError',
},
},
},
],
});
(getEventsFactory as unknown as jest.Mock).mockReturnValue({
findDailyEventsPortion,
});

const project = { _id: 'project-1' };
const args = {
limit: 10,
nextCursor: null,
sort: 'BY_DATE',
filters: {},
search: '',
};

const result = await projectResolver.Project.dailyEventsPortion(project, args, {}) as {
dailyEvents: Array<{ event: { payload: { title: string } } }>;
};

expect(result.dailyEvents[0].event.payload.title).toBe('TypeError');
expect(warnSpy).not.toHaveBeenCalled();

warnSpy.mockRestore();
});
});
Loading