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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.5.0",
"version": "1.5.1",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down
17 changes: 11 additions & 6 deletions src/models/eventsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -926,19 +926,19 @@ class EventsFactory extends Factory {
}

/**
* Bulk mark for resolved / ignored (not the same as per-event toggleEventMark).
* Bulk mark for resolved / ignored / starred (not the same as per-event toggleEventMark).
* - If every found event already has the mark: remove it from all (bulk "undo").
* - Otherwise: set the mark on every found event that does not have it yet (never remove
* from a subset when the selection is mixed).
* Only 'resolved' and 'ignored' are allowed for bulk.
* Only 'resolved', 'ignored' and 'starred' are allowed for bulk.
*
* @param {string[]} eventIds - original event ids
* @param {string} mark - 'resolved' | 'ignored'
* @returns {Promise<{ updatedCount: number, failedEventIds: string[] }>}
* @param {string} mark - 'resolved' | 'ignored' | 'starred'
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkToggleEventMark(eventIds, mark) {
if (mark !== 'resolved' && mark !== 'ignored') {
throw new Error(`bulkToggleEventMark: mark must be resolved or ignored, got ${mark}`);
if (mark !== 'resolved' && mark !== 'ignored' && mark !== 'starred') {
throw new Error(`bulkToggleEventMark: mark must be resolved, ignored or starred, got ${mark}`);
}

const max = EventsFactory.BULK_TOGGLE_EVENT_MARK_MAX;
Expand All @@ -962,6 +962,7 @@ class EventsFactory extends Factory {
if (validObjectIds.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds,
};
}
Expand All @@ -982,6 +983,7 @@ class EventsFactory extends Factory {
const markKey = `marks.${mark}`;
const allHaveMark = found.length > 0 && found.every(doc => doc.marks && doc.marks[mark]);
const ops = [];
const updatedEventIds = [];

for (const doc of found) {
const hasMark = doc.marks && doc.marks[mark];
Expand All @@ -1001,11 +1003,13 @@ class EventsFactory extends Factory {
update,
},
});
updatedEventIds.push(doc._id.toString());
}

if (ops.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds,
};
}
Expand All @@ -1014,6 +1018,7 @@ class EventsFactory extends Factory {

return {
updatedCount: bulkResult.modifiedCount + bulkResult.upsertedCount,
updatedEventIds,
failedEventIds,
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/resolvers/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ module.exports = {
* @return {Promise<{ updatedCount: number, failedEventIds: string[] }>}
*/
async bulkToggleEventMarks(_obj, { projectId, eventIds, mark }, context) {
if (mark !== 'resolved' && mark !== 'ignored') {
throw new UserInputError('bulkToggleEventMarks supports only resolved and ignored marks');
if (mark !== 'resolved' && mark !== 'ignored' && mark !== 'starred') {
throw new UserInputError('bulkToggleEventMarks supports only resolved, ignored and starred marks');
}

if (!eventIds || !eventIds.length) {
Expand Down
9 changes: 7 additions & 2 deletions src/typeDefs/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ type BulkToggleEventMarksResult {
"""
updatedCount: Int!

"""
Original event ids actually toggled in this operation
"""
updatedEventIds: [ID!]!

"""
Event ids that were not updated (invalid id or not found)
"""
Expand Down Expand Up @@ -520,7 +525,7 @@ extend type Mutation {
): Boolean!

"""
Toggle the same mark on many original events at once (only resolved or ignored).
Toggle the same mark on many original events at once (resolved, ignored or starred).
Same toggle semantics as toggleEventMark per event.
"""
bulkToggleEventMarks(
Expand All @@ -535,7 +540,7 @@ extend type Mutation {
eventIds: [ID!]!

"""
Mark (resolved or ignored only): if every selected event already has it, clear it for all;
Mark (resolved, ignored or starred): if every selected event already has it, clear it for all;
otherwise set it on every selected event that does not have it yet.
"""
mark: EventMark!
Expand Down
43 changes: 40 additions & 3 deletions test/models/eventsFactory-bulk-toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,43 @@ describe('EventsFactory.bulkToggleEventMark', () => {
});
});

it('should throw when mark is not resolved or ignored', async () => {
it('should throw when mark is unsupported', async () => {
const factory = new EventsFactory(projectId);

await expect(factory.bulkToggleEventMark([], 'starred' as any)).rejects.toThrow(
'bulkToggleEventMark: mark must be resolved or ignored'
await expect(factory.bulkToggleEventMark([], 'some-unknown-mark' as any)).rejects.toThrow(
'bulkToggleEventMark: mark must be resolved, ignored or starred'
);
});

it('should support starred mark', async () => {
const factory = new EventsFactory(projectId);
const id = new ObjectId();

collectionMock.find.mockReturnValue({
toArray: () =>
Promise.resolve([
{
_id: id,
marks: {},
},
]),
});
collectionMock.bulkWrite.mockResolvedValue({
modifiedCount: 1,
upsertedCount: 0,
});

const result = await factory.bulkToggleEventMark([ id.toString() ], 'starred');

expect(result.updatedCount).toBe(1);
expect(result.updatedEventIds).toEqual([ id.toString() ]);
const ops = collectionMock.bulkWrite.mock.calls[0][0];

expect(ops).toHaveLength(1);
expect(ops[0].updateOne.update).toEqual(
expect.objectContaining({
$set: { 'marks.starred': expect.any(Number) },
})
);
});

Expand Down Expand Up @@ -99,6 +131,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
const result = await factory.bulkToggleEventMark([ 'not-a-valid-id' ], 'resolved');

expect(result.updatedCount).toBe(0);
expect(result.updatedEventIds).toEqual([]);
expect(result.failedEventIds).toContain('not-a-valid-id');
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
});
Expand All @@ -114,6 +147,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
const result = await factory.bulkToggleEventMark([ missing.toString() ], 'ignored');

expect(result.updatedCount).toBe(0);
expect(result.updatedEventIds).toEqual([]);
expect(result.failedEventIds).toContain(missing.toString());
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
});
Expand All @@ -138,6 +172,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');

expect(result.updatedCount).toBe(1);
expect(result.updatedEventIds).toEqual([ b.toString() ]);
const ops = collectionMock.bulkWrite.mock.calls[0][0];

expect(ops).toHaveLength(1);
Expand Down Expand Up @@ -169,6 +204,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'resolved');

expect(result.updatedCount).toBe(2);
expect(result.updatedEventIds).toEqual([ a.toString(), b.toString() ]);
const ops = collectionMock.bulkWrite.mock.calls[0][0];

expect(ops).toHaveLength(2);
Expand Down Expand Up @@ -196,6 +232,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');

expect(result.updatedCount).toBe(1);
expect(result.updatedEventIds).toEqual([ b.toString() ]);
const ops = collectionMock.bulkWrite.mock.calls[0][0];

expect(ops).toHaveLength(1);
Expand Down
34 changes: 28 additions & 6 deletions test/resolvers/event-bulk-toggle-marks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const eventResolvers = require('../../src/resolvers/event') as {
o: unknown,
args: { projectId: string; eventIds: string[]; mark: string },
ctx: unknown
) => Promise<{ updatedCount: number; failedEventIds: string[] }>;
) => Promise<{ updatedCount: number; updatedEventIds: string[]; failedEventIds: string[] }>;
};
};

Expand All @@ -29,22 +29,22 @@ describe('Mutation.bulkToggleEventMarks', () => {
(getEventsFactory as unknown as jest.Mock).mockReturnValue({ bulkToggleEventMark });
});

it('should throw when mark is not resolved or ignored', async () => {
it('should throw when mark is not supported', async () => {
await expect(
eventResolvers.Mutation.bulkToggleEventMarks(
{},
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'some-unknown-mark' },
ctx
)
).rejects.toThrow(UserInputError);

await expect(
eventResolvers.Mutation.bulkToggleEventMarks(
{},
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'some-unknown-mark' },
ctx
)
).rejects.toThrow('bulkToggleEventMarks supports only resolved and ignored marks');
).rejects.toThrow('bulkToggleEventMarks supports only resolved, ignored and starred marks');

expect(bulkToggleEventMark).not.toHaveBeenCalled();
});
Expand All @@ -62,7 +62,7 @@ describe('Mutation.bulkToggleEventMarks', () => {
});

it('should call factory with original event ids and return its result', async () => {
const payload = { updatedCount: 2, failedEventIds: [ 'x' ] };
const payload = { updatedCount: 2, updatedEventIds: [ 'a', 'b' ], failedEventIds: [ 'x' ] };

bulkToggleEventMark.mockResolvedValue(payload);

Expand All @@ -84,6 +84,28 @@ describe('Mutation.bulkToggleEventMarks', () => {
expect(result).toEqual(payload);
});

it('should allow starred mark for bulk toggle', async () => {
const payload = { updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [] };

bulkToggleEventMark.mockResolvedValue(payload);

const result = await eventResolvers.Mutation.bulkToggleEventMarks(
{},
{
projectId: 'p1',
eventIds: [ '507f1f77bcf86cd799439011' ],
mark: 'starred',
},
ctx
);

expect(bulkToggleEventMark).toHaveBeenCalledWith(
[ '507f1f77bcf86cd799439011' ],
'starred'
);
expect(result).toEqual(payload);
});

it('should map factory max-length error to UserInputError', async () => {
bulkToggleEventMark.mockRejectedValue(
new Error('bulkToggleEventMark: at most 100 event ids allowed')
Expand Down