Skip to content
Open
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.4.12",
"version": "1.5.3",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down
161 changes: 161 additions & 0 deletions src/models/eventsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,48 @@ class EventsFactory extends Factory {
return result;
}

/**
* Mark many original events as visited for passed user
*
* @param {string[]} eventIds - original event ids
* @param {string|ObjectId} userId - id of the user who is visiting events
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkVisitEvent(eventIds, userId) {
const {
collection,
found,
failedEventIds,
} = await this._resolveBulkEventsByIds(eventIds);
const userIdStr = String(userId);

const docsToUpdate = found.filter((doc) => {
const visitedBy = Array.isArray(doc.visitedBy) ? doc.visitedBy : [];

return !visitedBy.some((visitedUserId) => String(visitedUserId) === userIdStr);
});
const updatedEventIds = docsToUpdate.map(doc => doc._id.toString());

if (docsToUpdate.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds,
};
}

const updateManyResult = await collection.updateMany(
{ _id: { $in: docsToUpdate.map(doc => doc._id) } },
{ $addToSet: { visitedBy: new ObjectId(userId) } }
);

return {
updatedCount: updateManyResult.modifiedCount,
updatedEventIds,
failedEventIds,
};
}

/**
* Mark or unmark event as Resolved, Ignored or Starred
*
Expand Down Expand Up @@ -918,6 +960,102 @@ class EventsFactory extends Factory {
return collection.updateOne(query, update);
}

/**
* Bulk toggle mark for original events.
*
* @param {string[]} eventIds - original event ids
* @param {string} mark - 'resolved' | 'ignored' | 'starred'
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkToggleEventMark(eventIds, mark) {
const {
collection,
found,
failedEventIds,
} = await this._resolveBulkEventsByIds(eventIds);

const nowSec = Math.floor(Date.now() / 1000);
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];
let update;

if (allHaveMark) {
update = { $unset: { [markKey]: '' } };
} else if (!hasMark) {
update = { $set: { [markKey]: nowSec } };
} else {
continue;
}

ops.push({
updateOne: {
filter: { _id: doc._id },
update,
},
});
updatedEventIds.push(doc._id.toString());
}

if (ops.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds,
};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try using updateMany

const bulkResult = await collection.bulkWrite(ops, { ordered: false });

return {
updatedCount: bulkResult.modifiedCount + bulkResult.upsertedCount,
updatedEventIds,
failedEventIds,
};
}

/**
* Bulk set/clear assignee for many original events.
*
* @param {string[]} eventIds - original event ids
* @param {string|null|undefined} assignee - target assignee id, null/undefined to clear
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkUpdateAssignee(eventIds, assignee) {
const {
collection,
found,
failedEventIds,
} = await this._resolveBulkEventsByIds(eventIds);

const normalizedAssignee = assignee ? String(assignee) : '';
const docsToUpdate = found.filter(doc => String(doc.assignee || '') !== normalizedAssignee);
const updatedEventIds = docsToUpdate.map(doc => doc._id.toString());

if (docsToUpdate.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds,
};
}

const updateManyResult = await collection.updateMany(
{ _id: { $in: docsToUpdate.map(doc => doc._id) } },
{ $set: { assignee: normalizedAssignee } }
);

return {
updatedCount: updateManyResult.modifiedCount,
updatedEventIds,
failedEventIds,
};
}

/**
* Remove all project events
*
Expand Down Expand Up @@ -966,6 +1104,29 @@ class EventsFactory extends Factory {
return result;
}

/**
* Resolve original events for bulk operations and collect not found ids.
*
* @param {string[]} eventIds - original event ids
* @returns {Promise<{ collection: any, found: any[], failedEventIds: string[] }>}
*/
async _resolveBulkEventsByIds(eventIds) {
const unique = [ ...new Set((eventIds || []).map(id => String(id))) ];
const objectIds = unique.map(id => new ObjectId(id));
const collection = this.getCollection(this.TYPES.EVENTS);
const found = await collection.find({ _id: { $in: objectIds } }).toArray();
const foundByIdStr = new Set(found.map(doc => doc._id.toString()));
const failedEventIds = objectIds
.map(id => id.toString())
.filter(id => !foundByIdStr.has(id));

return {
collection,
found,
failedEventIds,
};
}

/**
* Compose event with repetition
*
Expand Down
140 changes: 131 additions & 9 deletions src/resolvers/event.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const getEventsFactory = require('./helpers/eventsFactory').default;
const sendPersonalNotification = require('../utils/personalNotifications').default;
const {
fireAndForgetAssigneeNotifications,
parseBulkEventIds,
mergeFailedEventIds,
} = require('./helpers/bulkEvents');
const { aiService } = require('../services/ai');
const { UserInputError } = require('apollo-server-express');

/**
* See all types and fields here {@see ../typeDefs/event.graphql}
Expand Down Expand Up @@ -135,6 +140,34 @@ module.exports = {

return !!result.acknowledged;
},
/**
* Mark many original events as visited for current user
*
* @param {ResolverObj} _obj - resolver context
* @param {string} projectId - project id
* @param {string[]} eventIds - original event ids
* @param {UserInContext} user - user context
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkVisitEvents(_obj, { projectId, eventIds }, { user, ...context }) {
const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds);

if (validEventIds.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds: invalidEventIds,
};
}

const factory = getEventsFactory(context, projectId);
const result = await factory.bulkVisitEvent(validEventIds, user.id);

return {
...result,
failedEventIds: mergeFailedEventIds(result, invalidEventIds),
};
},

/**
* Mark event with one of the event marks
Expand All @@ -153,6 +186,37 @@ module.exports = {
return !!result.acknowledged;
},

/**
* Bulk set resolved/ignored: always set mark on events that lack it, unless all selected
* already have the mark — then remove from all.
*
* @param {ResolverObj} _obj - resolver context
* @param {string} projectId - project id
* @param {string[]} eventIds - original event ids
* @param {string} mark - EventMark enum value
* @param {object} context - gql context
* @return {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkToggleEventMarks(_obj, { projectId, eventIds, mark }, context) {
const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds);

if (validEventIds.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds: invalidEventIds,
};
}

const factory = getEventsFactory(context, projectId);
const result = await factory.bulkToggleEventMark(validEventIds, mark);

return {
...result,
failedEventIds: mergeFailedEventIds(result, invalidEventIds),
};
},

/**
* Mutations namespace
*
Expand Down Expand Up @@ -196,14 +260,12 @@ module.exports = {

const assigneeData = await factories.usersFactory.dataLoaders.userById.load(assignee);

await sendPersonalNotification(assigneeData, {
type: 'assignee',
payload: {
assigneeId: assignee,
projectId,
whoAssignedId: user.id,
eventId,
},
fireAndForgetAssigneeNotifications({
assigneeData,
eventIds: [ eventId ],
projectId,
assigneeId: assignee,
whoAssignedId: user.id,
});

return {
Expand All @@ -230,5 +292,65 @@ module.exports = {
success: !!result.acknowledged,
};
},

/**
* Bulk set/clear assignee for selected original events
*
* @param {ResolverObj} _obj - resolver context
* @param {BulkUpdateAssigneeInput} input - object of arguments
* @param factories - factories for working with models
* @return {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
*/
async bulkUpdateAssignee(_obj, { input }, { factories, user, ...context }) {
const { projectId, eventIds, assignee } = input;
const { validEventIds, invalidEventIds } = parseBulkEventIds(eventIds);
let assigneeData = null;

if (validEventIds.length === 0) {
return {
updatedCount: 0,
updatedEventIds: [],
failedEventIds: invalidEventIds,
};
}

const factory = getEventsFactory(context, projectId);

if (assignee) {
const userExists = await factories.usersFactory.findById(assignee);

if (!userExists) {
throw new UserInputError('assignee not found');
}

assigneeData = userExists;

const project = await factories.projectsFactory.findById(projectId);
const workspace = await factories.workspacesFactory.findById(project.workspaceId);
const assigneeExistsInWorkspace = await workspace.getMemberInfo(assignee);

if (!assigneeExistsInWorkspace) {
throw new UserInputError('assignee is not a workspace member');
}
}

const result = await factory.bulkUpdateAssignee(validEventIds, assignee);
const resultWithInvalid = {
...result,
failedEventIds: mergeFailedEventIds(result, invalidEventIds),
};

if (assignee && resultWithInvalid.updatedEventIds.length > 0) {
fireAndForgetAssigneeNotifications({
assigneeData,
eventIds: resultWithInvalid.updatedEventIds,
projectId,
assigneeId: assignee,
whoAssignedId: user.id,
});
}

return resultWithInvalid;
},
},
};
Loading