Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
51 changes: 51 additions & 0 deletions src/resolvers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,55 @@ 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 +653,8 @@ module.exports = {
assignee
);

normalizeDailyEventsPayloadTitle(dailyEventsPortion, project._id);

return dailyEventsPortion;
},

Expand Down
92 changes: 92 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,96 @@ describe('Project resolver dailyEventsPortion', () => {
undefined
);
});

it('should apply fallback title when payload title is missing and log warning', 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: null,
Comment thread
Reversean marked this conversation as resolved.
},
},
},
],
});
(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(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
'[ProjectResolver.dailyEventsPortion] Missing event payload title. Fallback title applied.',
expect.objectContaining({
projectId: 'project-1',
dailyEventId: 'daily-1',
dailyEventGroupHash: 'group-1',
eventOriginalId: 'event-1',
eventId: 'repetition-1',
})
);

warnSpy.mockRestore();
});

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();
});
});