From d4bc87f86f3bab3234d9b9f62e8e62b2f2246959 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 9 Aug 2025 19:52:32 +0300 Subject: [PATCH 01/14] merging repetitions on api side --- src/models/eventsFactory.js | 108 ++++++++++++++++++++++++++++-------- src/resolvers/event.js | 45 +++++---------- src/resolvers/project.js | 12 ++-- src/typeDefs/event.ts | 106 ++--------------------------------- 4 files changed, 110 insertions(+), 161 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 1bac1551..6b202bf0 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -6,6 +6,7 @@ const Factory = require('./modelFactory'); const mongo = require('../mongo'); const Event = require('../models/event'); const { ObjectID } = require('mongodb'); +const { composeFullRepetitionEvent } = require('../utils/merge'); /** * @typedef {Object} RecentEventSchema @@ -399,15 +400,29 @@ class EventsFactory extends Factory { * * @todo move to Repetitions(?) model */ - async getEventRepetitions(eventId, limit = 10, skip = 0) { + async getEventRepetitionsByGroupHash(groupHash, limit = 10, cursor = undefined) { limit = this.validateLimit(limit); skip = this.validateSkip(skip); + cursor = cursor ? new ObjectID(cursor) : undefined; + + const result = { + repetitions: [], + cursor: undefined, + }; + /** * Get original event * @type {EventSchema} */ - const eventOriginal = await this.findById(eventId); + const eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + groupHash: groupHash, + }); + + if (!eventOriginal) { + return result; + } /** * Collect repetitions @@ -416,13 +431,27 @@ class EventsFactory extends Factory { const repetitions = await this.getCollection(this.TYPES.REPETITIONS) .find({ groupHash: eventOriginal.groupHash, + _id: cursor ? { $lte: cursor } : {}, }) .sort({ _id: -1 }) - .limit(limit) - .skip(skip) + .limit(limit + 1) .toArray(); - const isLastPortion = repetitions.length < limit && skip === 0; + if (repetitions.length === limit + 1) { + result.cursor = repetitions.pop()._id; + } + + for (const repetition of repetitions) { + result.repetitions.push({ + ...eventOriginal, + _id: repetition._id, + payload: composeFullRepetitionEvent(eventOriginal, repetition).payload, + timestamp: repetition.timestamp, + firstAppearanceTimestamp: eventOriginal.timestamp, + }); + } + + const isLastPortion = repetitions.length < limit; /** * For last portion: @@ -434,16 +463,14 @@ class EventsFactory extends Factory { * @type {EventRepetitionSchema} */ const firstRepetition = { - _id: eventOriginal._id, - payload: eventOriginal.payload, - groupHash: eventOriginal.groupHash, - timestamp: eventOriginal.timestamp, + ...eventOriginal, + firstAppearanceTimestamp: eventOriginal.timestamp, }; - repetitions.push(firstRepetition); + result.repetitions.push(firstRepetition); } - return repetitions; + return result; } /** @@ -455,10 +482,40 @@ class EventsFactory extends Factory { * @todo move to Repetitions(?) model */ async getEventRepetition(repetitionId) { - return this.getCollection(this.TYPES.REPETITIONS) + const repetition = await this.getCollection(this.TYPES.REPETITIONS) .findOne({ _id: ObjectID(repetitionId), }); + + if (!repetition) { + /** + * If repetition is not found, it can mean that client is trying to get original event + */ + const event = await this.findById(repetitionId); + + if (!event) { + return null; + } + + return { + ...event, + firstAppearanceTimestamp: event.timestamp, + }; + } + + const originalEvent = await this.findById(repetition.eventId); + + if (!originalEvent) { + return null; + } + + return { + ...originalEvent, + _id: repetition._id, + payload: composeFullRepetitionEvent(originalEvent, repetition).payload, + timestamp: repetition.timestamp, + firstAppearanceTimestamp: originalEvent.timestamp, + }; } /** @@ -479,11 +536,14 @@ class EventsFactory extends Factory { /** * Get a release from corresponding to this event * - * @param {string} eventId - id of event to get the release + * @param {string} groupHash - hash of event to get the release * @returns {Release|null} */ - async getEventRelease(eventId) { - const eventOriginal = await this.findById(eventId); + async getEventRelease(groupHash) { + const eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + groupHash: groupHash, + }); const release = await mongo.databases.events.collection(this.TYPES.RELEASES).findOne({ release: eventOriginal.payload.release, @@ -496,15 +556,15 @@ class EventsFactory extends Factory { /** * Mark event as visited for passed user * - * @param {string|ObjectId} eventId + * @param {string|ObjectId} groupHash * @param {string|ObjectId} userId * * @return {Promise} */ - async visitEvent(eventId, userId) { + async visitEvent(groupHash, userId) { return this.getCollection(this.TYPES.EVENTS) .updateOne( - { _id: new ObjectID(eventId) }, + { groupHash: groupHash }, { $addToSet: { visitedBy: new ObjectID(userId) } } ); } @@ -512,14 +572,14 @@ class EventsFactory extends Factory { /** * Mark or unmark event as Resolved, Ignored or Starred * - * @param {string|ObjectId} eventId - event to mark + * @param {string|ObjectId} groupHash - event to mark * @param {string} mark - mark label * * @return {Promise} */ - async toggleEventMark(eventId, mark) { + async toggleEventMark(groupHash, mark) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { _id: new ObjectID(eventId) }; + const query = { groupHash: groupHash }; const event = await collection.findOne(query); const markKey = `marks.${mark}`; @@ -565,13 +625,13 @@ class EventsFactory extends Factory { /** * Update assignee to selected event * - * @param {string} eventId - event id + * @param {string} groupHash - event id * @param {string} assignee - assignee id for this event * @return {Promise} */ - async updateAssignee(eventId, assignee) { + async updateAssignee(groupHash, assignee) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { _id: new ObjectID(eventId) }; + const query = { groupHash: groupHash }; const update = { $set: { assignee: assignee }, }; diff --git a/src/resolvers/event.js b/src/resolvers/event.js index 5a2cbb51..2e82baa0 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -18,23 +18,6 @@ module.exports = { }, }, Event: { - /** - * Returns Event with concrete repetition - * - * @param {string} eventId - id of Event of which repetition requested - * @param {string} projectId - projectId of Event of which repetition requested - * @param {string|null} [repetitionId] - if not specified, last repetition will returned - * @return {Promise} - */ - async repetition({ id: eventId, projectId }, { id: repetitionId }) { - const factory = new EventsFactory(projectId); - - if (!repetitionId) { - return factory.getEventLastRepetition(eventId); - } - - return factory.getEventRepetition(repetitionId); - }, /** * Returns repetitions list of the event @@ -47,10 +30,10 @@ module.exports = { * * @return {EventRepetitionSchema[]} */ - async repetitions({ _id: eventId, projectId }, { limit, skip }) { + async repetitions({ groupHash, projectId }, { limit, skip }) { const factory = new EventsFactory(projectId); - return factory.getEventRepetitions(eventId, limit, skip); + return factory.getEventRepetitions(groupHash, limit, skip); }, /** @@ -115,9 +98,9 @@ module.exports = { * @param {String} eventId - event id * @returns {Promise} */ - async release({ projectId, id: eventId }) { + async release({ projectId, groupHash }) { const factory = new EventsFactory(new ObjectID(projectId)); - const release = await factory.getEventRelease(eventId); + const release = await factory.getEventRelease(groupHash); return release; }, @@ -132,10 +115,10 @@ module.exports = { * @param {UserInContext} user - user context * @return {Promise} */ - async visitEvent(_obj, { project, id }, { user }) { + async visitEvent(_obj, { project, groupHash }, { user }) { const factory = new EventsFactory(project); - const { result } = await factory.visitEvent(id, user.id); + const { result } = await factory.visitEvent(groupHash, user.id); return !!result.ok; }, @@ -145,14 +128,14 @@ module.exports = { * * @param {ResolverObj} _obj - resolver context * @param {string} project - project id - * @param {string} id - event id + * @param {string} groupHash - event id * @param {string} mark - mark to set * @return {Promise} */ - async toggleEventMark(_obj, { project, eventId, mark }) { + async toggleEventMark(_obj, { project, groupHash, mark }) { const factory = new EventsFactory(project); - const { result } = await factory.toggleEventMark(eventId, mark); + const { result } = await factory.toggleEventMark(groupHash, mark); return !!result.ok; }, @@ -174,7 +157,7 @@ module.exports = { * @return {Promise} */ async updateAssignee(_obj, { input }, { factories, user }) { - const { projectId, eventId, assignee } = input; + const { projectId, groupHash, assignee } = input; const factory = new EventsFactory(projectId); const userExists = await factories.usersFactory.findById(assignee); @@ -196,7 +179,7 @@ module.exports = { }; } - const { result } = await factory.updateAssignee(eventId, assignee); + const { result } = await factory.updateAssignee(groupHash, assignee); const assigneeData = await factories.usersFactory.dataLoaders.userById.load(assignee); @@ -206,7 +189,7 @@ module.exports = { assigneeId: assignee, projectId, whoAssignedId: user.id, - eventId, + groupHash, }, }); @@ -225,10 +208,10 @@ module.exports = { * @return {Promise} */ async removeAssignee(_obj, { input }) { - const { projectId, eventId } = input; + const { projectId, groupHash } = input; const factory = new EventsFactory(projectId); - const { result } = await factory.updateAssignee(eventId, ''); + const { result } = await factory.updateAssignee(groupHash, ''); return { success: !!result.ok, diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 3849787e..8227bdda 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -287,17 +287,17 @@ module.exports = { * * @returns {Event} */ - async event(project, { id: eventId }) { + async event(project, { id: repetitionId }) { const factory = new EventsFactory(project._id); - const event = await factory.findById(eventId); - - if (!event) { + const repetition = await factory.getEventRepetition(repetitionId); + + if (!repetition) { return null; } - event.projectId = project._id; + repetition.projectId = project._id; - return event; + return repetition; }, /** diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index a7990679..5f4a7ce5 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -176,95 +176,6 @@ type EventPayload { addons: EncodedJSON } -""" -Type representing Event payload. All fields can be omitted if there are no difference with the original -""" -type RepetitionPayload { - """ - Event title - """ - title: String - - """ - Event type: TypeError, ReferenceError etc. - """ - type: String - - """ - Event severity level - """ - level: Int - - """ - Event stack array from the latest call to the earliest - """ - backtrace: [EventBacktraceFrame!] - - """ - Additional data about GET request - """ - get: JSONObject - - """ - Additional data about POST request - """ - post: JSONObject - - """ - HTTP headers - """ - headers: JSONObject - - """ - Source code version identifier - """ - release: String - - """ - Current authenticated user - """ - user: EventUser - - """ - Any additional data of Event - """ - context: EncodedJSON - - """ - Custom data provided by project users - """ - addons: EncodedJSON -} - -""" -Repetition of the event -""" -type Repetition { - """ - Standalone repetition ID - """ - id: ID! @renameFrom(name: "_id") - - """ - Event's hash - """ - groupHash: String! - - """ - Event's payload patch - """ - payload: RepetitionPayload - - """ - Delta of the event's payload, stringified JSON - """ - delta: String - - """ - Event timestamp - """ - timestamp: Float! -} """ Possible event marks @@ -328,15 +239,10 @@ type Event { """ release: Release - """ - Event concrete repetition - """ - repetition(id: ID): Repetition - """ Event repetitions """ - repetitions(skip: Int = 0, limit: Int = 10): [Repetition!] + repetitions(cursor: ID = undefined, limit: Int = 10): [Event!] """ Array of users who visited event @@ -433,9 +339,9 @@ input UpdateAssigneeInput { projectId: ID! """ - ID of the selected event + Event group hash """ - eventId: ID! + groupHash: ID! """ Assignee id to set @@ -464,7 +370,7 @@ input RemoveAssigneeInput { """ ID of the selected event """ - eventId: ID! + groupHash: ID! } type RemoveAssigneeResponse { @@ -509,9 +415,9 @@ extend type Mutation { project: ID! """ - EvenID of the event to set the mark + Event group hash """ - eventId: ID! + groupHash: ID! """ Mark to set From 99899a5895a60f5a8c60baf2553b6f7e0d592247 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 9 Aug 2025 19:53:37 +0300 Subject: [PATCH 02/14] fix lint --- src/models/eventsFactory.js | 7 +++---- src/resolvers/project.js | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 6b202bf0..9e88a2f5 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -392,9 +392,9 @@ class EventsFactory extends Factory { /** * Returns Event repetitions * - * @param {string|ObjectID} eventId - Event's id + * @param {string|ObjectID} groupHash - Event's group hash * @param {Number} limit - count limitations - * @param {Number} skip - selection offset + * @param {Number} cursor - selection offset * * @return {EventRepetitionSchema[]} * @@ -402,7 +402,6 @@ class EventsFactory extends Factory { */ async getEventRepetitionsByGroupHash(groupHash, limit = 10, cursor = undefined) { limit = this.validateLimit(limit); - skip = this.validateSkip(skip); cursor = cursor ? new ObjectID(cursor) : undefined; @@ -494,7 +493,7 @@ class EventsFactory extends Factory { const event = await this.findById(repetitionId); if (!event) { - return null; + return null; } return { diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 8227bdda..1408fc22 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -290,7 +290,7 @@ module.exports = { async event(project, { id: repetitionId }) { const factory = new EventsFactory(project._id); const repetition = await factory.getEventRepetition(repetitionId); - + if (!repetition) { return null; } From 0d3f614ac26250dd49103be8f92099435b1b2209 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:56:09 +0000 Subject: [PATCH 03/14] Bump version up to 1.1.31 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dd90aca0..83c4b7ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.30", + "version": "1.1.31", "main": "index.ts", "license": "UNLICENSED", "scripts": { From 6f0104849a0f4ae4b14ba88a6f0b7ce9e08f734e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:45:50 +0000 Subject: [PATCH 04/14] Bump version up to 1.1.33 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 09296a53..ab70df85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.32", + "version": "1.1.33", "main": "index.ts", "license": "UNLICENSED", "scripts": { @@ -44,9 +44,9 @@ "@types/debug": "^4.1.5", "@types/escape-html": "^1.0.0", "@types/graphql-upload": "^8.0.11", + "@types/jsonwebtoken": "^8.3.5", "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.mergewith": "^4.6.9", - "@types/jsonwebtoken": "^8.3.5", "@types/mime-types": "^2.1.0", "@types/mongodb": "^3.6.20", "@types/node": "^16.11.46", From 795acae9e75bb73f2247bdf379a2b53ea54e72b1 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Thu, 14 Aug 2025 22:19:43 +0300 Subject: [PATCH 05/14] revert some types changes --- src/models/eventsFactory.js | 141 ++++++++++++++++++++++++++++++++---- src/resolvers/event.js | 28 +++---- src/typeDefs/event.ts | 12 +-- 3 files changed, 147 insertions(+), 34 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 9e88a2f5..321b5288 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -392,7 +392,7 @@ class EventsFactory extends Factory { /** * Returns Event repetitions * - * @param {string|ObjectID} groupHash - Event's group hash + * @param {string|ObjectID} eventId - Event's id (may be repetition id) * @param {Number} limit - count limitations * @param {Number} cursor - selection offset * @@ -400,7 +400,7 @@ class EventsFactory extends Factory { * * @todo move to Repetitions(?) model */ - async getEventRepetitionsByGroupHash(groupHash, limit = 10, cursor = undefined) { + async getEventRepetitions(eventId, limit = 10, cursor = undefined) { limit = this.validateLimit(limit); cursor = cursor ? new ObjectID(cursor) : undefined; @@ -414,11 +414,30 @@ class EventsFactory extends Factory { * Get original event * @type {EventSchema} */ - const eventOriginal = await this.getCollection(this.TYPES.EVENTS) + let eventOriginal = await this.getCollection(this.TYPES.EVENTS) .findOne({ - groupHash: groupHash, + _id: new ObjectID(eventId), }); + /** + * If event is not found, try to find it as repetition + */ + if (!eventOriginal) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ + _id: new ObjectID(eventId), + }); + + if (!repetition) { + return result; + } + + eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: repetition.eventId, + }); + } + if (!eventOriginal) { return result; } @@ -538,12 +557,35 @@ class EventsFactory extends Factory { * @param {string} groupHash - hash of event to get the release * @returns {Release|null} */ - async getEventRelease(groupHash) { + async getEventRelease(eventId) { const eventOriginal = await this.getCollection(this.TYPES.EVENTS) .findOne({ - groupHash: groupHash, + _id: new ObjectID(eventId), }); + /** + * If event is not found, try to find it as repetition + */ + if (!eventOriginal) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ + _id: new ObjectID(eventId), + }); + + if (!repetition) { + return null; + } + + eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: repetition.eventId, + }); + + if (!eventOriginal) { + return null; + } + } + const release = await mongo.databases.events.collection(this.TYPES.RELEASES).findOne({ release: eventOriginal.payload.release, projectId: this.projectId.toString(), @@ -555,15 +597,43 @@ class EventsFactory extends Factory { /** * Mark event as visited for passed user * - * @param {string|ObjectId} groupHash + * @param {string|ObjectId} eventId * @param {string|ObjectId} userId * * @return {Promise} */ - async visitEvent(groupHash, userId) { + async visitEvent(eventId, userId) { + let event = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: new ObjectID(eventId), + }); + + /** + * If event is not found, try to find it as repetition + */ + if (!event) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ + _id: new ObjectID(eventId), + }); + + if (!repetition) { + return null; + } + + event = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: repetition.eventId, + }); + + if (!event) { + return null; + } + } + return this.getCollection(this.TYPES.EVENTS) .updateOne( - { groupHash: groupHash }, + { _id: new ObjectID(eventId) }, { $addToSet: { visitedBy: new ObjectID(userId) } } ); } @@ -576,11 +646,32 @@ class EventsFactory extends Factory { * * @return {Promise} */ - async toggleEventMark(groupHash, mark) { + async toggleEventMark(eventId, mark) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { groupHash: groupHash }; + const query = { _id: new ObjectID(eventId) }; const event = await collection.findOne(query); + + + /** + * If event is not found, try to find it as repetition + */ + if (!event) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ + _id: new ObjectID(eventId), + }); + + if (repetition) { + event = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: repetition.eventId, + }); + + query._id = new ObjectID(repetition.eventId); + } + } + const markKey = `marks.${mark}`; let update; @@ -624,13 +715,35 @@ class EventsFactory extends Factory { /** * Update assignee to selected event * - * @param {string} groupHash - event id + * @param {string} eventId - event id * @param {string} assignee - assignee id for this event * @return {Promise} */ - async updateAssignee(groupHash, assignee) { + async updateAssignee(eventId, assignee) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { groupHash: groupHash }; + const query = { _id: new ObjectID(eventId) }; + + const event = await collection.findOne(query); + + /** + * If event is not found, try to find it as repetition + */ + if (!event) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ + _id: new ObjectID(eventId), + }); + + if (repetition) { + event = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: repetition.eventId, + }); + + query._id = new ObjectID(repetition.eventId); + } + } + const update = { $set: { assignee: assignee }, }; diff --git a/src/resolvers/event.js b/src/resolvers/event.js index 2e82baa0..ff1367ef 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -26,14 +26,14 @@ module.exports = { * @param {String} eventId * @param {String} projectId * @param {Number} limit - * @param {Number} skip + * @param {Number} cursor * * @return {EventRepetitionSchema[]} */ - async repetitions({ groupHash, projectId }, { limit, skip }) { + async repetitions({ _id: eventId, projectId }, { limit, cursor }) { const factory = new EventsFactory(projectId); - return factory.getEventRepetitions(groupHash, limit, skip); + return factory.getEventRepetitions(eventId, limit, cursor); }, /** @@ -98,9 +98,9 @@ module.exports = { * @param {String} eventId - event id * @returns {Promise} */ - async release({ projectId, groupHash }) { + async release({ projectId, id: eventId }) { const factory = new EventsFactory(new ObjectID(projectId)); - const release = await factory.getEventRelease(groupHash); + const release = await factory.getEventRelease(eventId); return release; }, @@ -115,10 +115,10 @@ module.exports = { * @param {UserInContext} user - user context * @return {Promise} */ - async visitEvent(_obj, { project, groupHash }, { user }) { + async visitEvent(_obj, { project, id }, { user }) { const factory = new EventsFactory(project); - const { result } = await factory.visitEvent(groupHash, user.id); + const { result } = await factory.visitEvent(id, user.id); return !!result.ok; }, @@ -132,10 +132,10 @@ module.exports = { * @param {string} mark - mark to set * @return {Promise} */ - async toggleEventMark(_obj, { project, groupHash, mark }) { + async toggleEventMark(_obj, { project, eventId, mark }) { const factory = new EventsFactory(project); - const { result } = await factory.toggleEventMark(groupHash, mark); + const { result } = await factory.toggleEventMark(eventId, mark); return !!result.ok; }, @@ -157,7 +157,7 @@ module.exports = { * @return {Promise} */ async updateAssignee(_obj, { input }, { factories, user }) { - const { projectId, groupHash, assignee } = input; + const { projectId, eventId, assignee } = input; const factory = new EventsFactory(projectId); const userExists = await factories.usersFactory.findById(assignee); @@ -179,7 +179,7 @@ module.exports = { }; } - const { result } = await factory.updateAssignee(groupHash, assignee); + const { result } = await factory.updateAssignee(eventId, assignee); const assigneeData = await factories.usersFactory.dataLoaders.userById.load(assignee); @@ -189,7 +189,7 @@ module.exports = { assigneeId: assignee, projectId, whoAssignedId: user.id, - groupHash, + eventId, }, }); @@ -208,10 +208,10 @@ module.exports = { * @return {Promise} */ async removeAssignee(_obj, { input }) { - const { projectId, groupHash } = input; + const { projectId, eventId } = input; const factory = new EventsFactory(projectId); - const { result } = await factory.updateAssignee(groupHash, ''); + const { result } = await factory.updateAssignee(eventId, ''); return { success: !!result.ok, diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 5f4a7ce5..67f9f245 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -242,7 +242,7 @@ type Event { """ Event repetitions """ - repetitions(cursor: ID = undefined, limit: Int = 10): [Event!] + repetitions(cursor: ID = null, limit: Int = 10): [Event!] """ Array of users who visited event @@ -339,9 +339,9 @@ input UpdateAssigneeInput { projectId: ID! """ - Event group hash + Event id """ - groupHash: ID! + eventId: ID! """ Assignee id to set @@ -370,7 +370,7 @@ input RemoveAssigneeInput { """ ID of the selected event """ - groupHash: ID! + eventId: ID! } type RemoveAssigneeResponse { @@ -415,9 +415,9 @@ extend type Mutation { project: ID! """ - Event group hash + EvenID of the event to set the mark """ - groupHash: ID! + eventId: ID! """ Mark to set From ce24a61882b2ed7522c49342c8eaa86772797e5e Mon Sep 17 00:00:00 2001 From: slaveeks Date: Thu, 14 Aug 2025 22:23:53 +0300 Subject: [PATCH 06/14] fix lint --- src/models/eventsFactory.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 321b5288..5674b3ce 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -558,7 +558,7 @@ class EventsFactory extends Factory { * @returns {Release|null} */ async getEventRelease(eventId) { - const eventOriginal = await this.getCollection(this.TYPES.EVENTS) + let eventOriginal = await this.getCollection(this.TYPES.EVENTS) .findOne({ _id: new ObjectID(eventId), }); @@ -650,8 +650,7 @@ class EventsFactory extends Factory { const collection = this.getCollection(this.TYPES.EVENTS); const query = { _id: new ObjectID(eventId) }; - const event = await collection.findOne(query); - + let event = await collection.findOne(query); /** * If event is not found, try to find it as repetition @@ -723,7 +722,7 @@ class EventsFactory extends Factory { const collection = this.getCollection(this.TYPES.EVENTS); const query = { _id: new ObjectID(eventId) }; - const event = await collection.findOne(query); + let event = await collection.findOne(query); /** * If event is not found, try to find it as repetition From bcd792971ca6c6df7ac776b6b8df532716a09d6d Mon Sep 17 00:00:00 2001 From: slaveeks Date: Thu, 14 Aug 2025 22:45:33 +0300 Subject: [PATCH 07/14] fix docs --- src/models/eventsFactory.js | 4 ++-- src/resolvers/event.js | 2 +- src/typeDefs/event.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 5674b3ce..f2249cac 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -554,7 +554,7 @@ class EventsFactory extends Factory { /** * Get a release from corresponding to this event * - * @param {string} groupHash - hash of event to get the release + * @param {string} eventId - id of event to get the release * @returns {Release|null} */ async getEventRelease(eventId) { @@ -641,7 +641,7 @@ class EventsFactory extends Factory { /** * Mark or unmark event as Resolved, Ignored or Starred * - * @param {string|ObjectId} groupHash - event to mark + * @param {string|ObjectId} eventId - event to mark * @param {string} mark - mark label * * @return {Promise} diff --git a/src/resolvers/event.js b/src/resolvers/event.js index ff1367ef..f1a77c1b 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -128,7 +128,7 @@ module.exports = { * * @param {ResolverObj} _obj - resolver context * @param {string} project - project id - * @param {string} groupHash - event id + * @param {string} id - event id * @param {string} mark - mark to set * @return {Promise} */ diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 67f9f245..c313f8b1 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -339,7 +339,7 @@ input UpdateAssigneeInput { projectId: ID! """ - Event id + ID of the selected event """ eventId: ID! From cb54d3a000bcf431675dfd591c05ecf95325b638 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Fri, 15 Aug 2025 01:59:56 +0300 Subject: [PATCH 08/14] fixes --- src/models/eventsFactory.js | 30 ++++++++++++++++++------------ src/typeDefs/event.ts | 26 +++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index f2249cac..26c04deb 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -288,6 +288,7 @@ class EventsFactory extends Factory { if (result && result.events) { result.events.forEach(event => { event.projectId = this.projectId; + event.firstAppearanceTimestamp = event.timestamp; }); } @@ -400,14 +401,14 @@ class EventsFactory extends Factory { * * @todo move to Repetitions(?) model */ - async getEventRepetitions(eventId, limit = 10, cursor = undefined) { + async getEventRepetitions(eventId, limit = 10, cursor = null) { limit = this.validateLimit(limit); - cursor = cursor ? new ObjectID(cursor) : undefined; + cursor = cursor ? new ObjectID(cursor) : null; const result = { repetitions: [], - cursor: undefined, + cursor: null, }; /** @@ -449,7 +450,7 @@ class EventsFactory extends Factory { const repetitions = await this.getCollection(this.TYPES.REPETITIONS) .find({ groupHash: eventOriginal.groupHash, - _id: cursor ? { $lte: cursor } : {}, + _id: cursor ? { $lte: cursor } : { $exists: true }, }) .sort({ _id: -1 }) .limit(limit + 1) @@ -466,10 +467,11 @@ class EventsFactory extends Factory { payload: composeFullRepetitionEvent(eventOriginal, repetition).payload, timestamp: repetition.timestamp, firstAppearanceTimestamp: eventOriginal.timestamp, + projectId: this.projectId, }); } - const isLastPortion = repetitions.length < limit; + const isLastPortion = result.cursor === null; /** * For last portion: @@ -483,6 +485,7 @@ class EventsFactory extends Factory { const firstRepetition = { ...eventOriginal, firstAppearanceTimestamp: eventOriginal.timestamp, + projectId: this.projectId, }; result.repetitions.push(firstRepetition); @@ -521,7 +524,10 @@ class EventsFactory extends Factory { }; } - const originalEvent = await this.findById(repetition.eventId); + const originalEvent = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + groupHash: repetition.groupHash, + }); if (!originalEvent) { return null; @@ -578,7 +584,7 @@ class EventsFactory extends Factory { eventOriginal = await this.getCollection(this.TYPES.EVENTS) .findOne({ - _id: repetition.eventId, + groupHash: repetition.groupHash, }); if (!eventOriginal) { @@ -623,7 +629,7 @@ class EventsFactory extends Factory { event = await this.getCollection(this.TYPES.EVENTS) .findOne({ - _id: repetition.eventId, + groupHash: repetition.groupHash, }); if (!event) { @@ -664,10 +670,10 @@ class EventsFactory extends Factory { if (repetition) { event = await this.getCollection(this.TYPES.EVENTS) .findOne({ - _id: repetition.eventId, + groupHash: repetition.groupHash, }); - query._id = new ObjectID(repetition.eventId); + query._id = new ObjectID(event._id); } } @@ -736,10 +742,10 @@ class EventsFactory extends Factory { if (repetition) { event = await this.getCollection(this.TYPES.EVENTS) .findOne({ - _id: repetition.eventId, + groupHash: repetition.groupHash, }); - query._id = new ObjectID(repetition.eventId); + query._id = new ObjectID(event._id); } } diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index c313f8b1..e084b957 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -195,6 +195,14 @@ type EventMarks { ignored: Boolean! } +""" +Object returned in repetitions property of event object +""" +type RepetitionsResponse { + repetitions: [Event!] + cursor: String +} + """ Type representing Hawk single Event """ @@ -234,6 +242,11 @@ type Event { """ timestamp: Float! + """ + Event first appearance timestamp + """ + firstAppearanceTimestamp: Float! + """ Release data """ @@ -242,7 +255,7 @@ type Event { """ Event repetitions """ - repetitions(cursor: ID = null, limit: Int = 10): [Event!] + repetitions(cursor: String = null, limit: Int = 10): RepetitionsResponse! """ Array of users who visited event @@ -401,8 +414,15 @@ extend type Mutation { Mutation marks event as visited for current user """ visitEvent( - project: ID! - id: ID! + """ + ID of project event is related to + """ + projectId: ID! + + """ + ID of the event to visit + """ + eventId: ID! ): Boolean! @requireAuth """ From d0099288dde82d75e657a24c8701608c4969041a Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 18:47:03 +0300 Subject: [PATCH 09/14] fix get repetitions by repetition --- src/models/eventsFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 26c04deb..56fe2c06 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -435,7 +435,7 @@ class EventsFactory extends Factory { eventOriginal = await this.getCollection(this.TYPES.EVENTS) .findOne({ - _id: repetition.eventId, + groupHash: repetition.groupHash, }); } From ada2a243f3c2685a917f2e730ca5b6e3304c18b2 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 21:16:47 +0300 Subject: [PATCH 10/14] review changes --- src/models/eventsFactory.js | 198 +++++++++++++----------------------- src/typeDefs/event.ts | 6 +- src/utils/merge.ts | 33 +++--- test/utils/merge.test.ts | 45 ++++---- 4 files changed, 112 insertions(+), 170 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 56fe2c06..0af56310 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -6,7 +6,7 @@ const Factory = require('./modelFactory'); const mongo = require('../mongo'); const Event = require('../models/event'); const { ObjectID } = require('mongodb'); -const { composeFullRepetitionEvent } = require('../utils/merge'); +const { composeEventPayloadWithRepetition } = require('../utils/merge'); /** * @typedef {Object} RecentEventSchema @@ -408,36 +408,14 @@ class EventsFactory extends Factory { const result = { repetitions: [], - cursor: null, + nextCursor: null, }; /** * Get original event * @type {EventSchema} */ - let eventOriginal = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - _id: new ObjectID(eventId), - }); - - /** - * If event is not found, try to find it as repetition - */ - if (!eventOriginal) { - const repetition = await this.getCollection(this.TYPES.REPETITIONS) - .findOne({ - _id: new ObjectID(eventId), - }); - - if (!repetition) { - return result; - } - - eventOriginal = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - groupHash: repetition.groupHash, - }); - } + const eventOriginal = await this._findOriginalEvent(eventId); if (!eventOriginal) { return result; @@ -457,21 +435,19 @@ class EventsFactory extends Factory { .toArray(); if (repetitions.length === limit + 1) { - result.cursor = repetitions.pop()._id; + result.nextCursor = repetitions.pop()._id; } for (const repetition of repetitions) { + const event = this._composeEventWithRepetition(eventOriginal, repetition); + result.repetitions.push({ - ...eventOriginal, - _id: repetition._id, - payload: composeFullRepetitionEvent(eventOriginal, repetition).payload, - timestamp: repetition.timestamp, - firstAppearanceTimestamp: eventOriginal.timestamp, + ...event, projectId: this.projectId, }); } - const isLastPortion = result.cursor === null; + const isLastPortion = result.nextCursor === null; /** * For last portion: @@ -514,14 +490,10 @@ class EventsFactory extends Factory { */ const event = await this.findById(repetitionId); - if (!event) { - return null; - } - - return { + return event ? { ...event, firstAppearanceTimestamp: event.timestamp, - }; + } : null; } const originalEvent = await this.getCollection(this.TYPES.EVENTS) @@ -533,13 +505,7 @@ class EventsFactory extends Factory { return null; } - return { - ...originalEvent, - _id: repetition._id, - payload: composeFullRepetitionEvent(originalEvent, repetition).payload, - timestamp: repetition.timestamp, - firstAppearanceTimestamp: originalEvent.timestamp, - }; + return this._composeEventWithRepetition(originalEvent, repetition); } /** @@ -564,32 +530,10 @@ class EventsFactory extends Factory { * @returns {Release|null} */ async getEventRelease(eventId) { - let eventOriginal = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - _id: new ObjectID(eventId), - }); + const eventOriginal = await this._findOriginalEvent(eventId); - /** - * If event is not found, try to find it as repetition - */ if (!eventOriginal) { - const repetition = await this.getCollection(this.TYPES.REPETITIONS) - .findOne({ - _id: new ObjectID(eventId), - }); - - if (!repetition) { - return null; - } - - eventOriginal = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - groupHash: repetition.groupHash, - }); - - if (!eventOriginal) { - return null; - } + return null; } const release = await mongo.databases.events.collection(this.TYPES.RELEASES).findOne({ @@ -609,37 +553,15 @@ class EventsFactory extends Factory { * @return {Promise} */ async visitEvent(eventId, userId) { - let event = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - _id: new ObjectID(eventId), - }); + const event = await this._findOriginalEvent(eventId); - /** - * If event is not found, try to find it as repetition - */ if (!event) { - const repetition = await this.getCollection(this.TYPES.REPETITIONS) - .findOne({ - _id: new ObjectID(eventId), - }); - - if (!repetition) { - return null; - } - - event = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - groupHash: repetition.groupHash, - }); - - if (!event) { - return null; - } + return null; } return this.getCollection(this.TYPES.EVENTS) .updateOne( - { _id: new ObjectID(eventId) }, + { _id: new ObjectID(event._id) }, { $addToSet: { visitedBy: new ObjectID(userId) } } ); } @@ -654,29 +576,15 @@ class EventsFactory extends Factory { */ async toggleEventMark(eventId, mark) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { _id: new ObjectID(eventId) }; - let event = await collection.findOne(query); + const event = await this._findOriginalEvent(eventId); - /** - * If event is not found, try to find it as repetition - */ if (!event) { - const repetition = await this.getCollection(this.TYPES.REPETITIONS) - .findOne({ - _id: new ObjectID(eventId), - }); - - if (repetition) { - event = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - groupHash: repetition.groupHash, - }); - - query._id = new ObjectID(event._id); - } + return null; } + const query = { _id: new ObjectID(event._id) }; + const markKey = `marks.${mark}`; let update; @@ -726,34 +634,72 @@ class EventsFactory extends Factory { */ async updateAssignee(eventId, assignee) { const collection = this.getCollection(this.TYPES.EVENTS); - const query = { _id: new ObjectID(eventId) }; - let event = await collection.findOne(query); + const event = await this._findOriginalEvent(eventId); + + if (!event) { + return null; + } + + const query = { _id: new ObjectID(event._id) }; + + const update = { + $set: { assignee: assignee }, + }; + + return collection.updateOne(query, update); + } + + /** + * Find original event by eventId. If event is not found directly, + * try to find it as repetition and get original event by groupHash + * + * @param {string|ObjectID} eventId - event's id, may be repetition id + * @returns {Promise} original event or null if not found + */ + async _findOriginalEvent(eventId) { + let eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + _id: new ObjectID(eventId), + }); /** * If event is not found, try to find it as repetition */ - if (!event) { + if (!eventOriginal) { const repetition = await this.getCollection(this.TYPES.REPETITIONS) .findOne({ _id: new ObjectID(eventId), }); - if (repetition) { - event = await this.getCollection(this.TYPES.EVENTS) - .findOne({ - groupHash: repetition.groupHash, - }); - - query._id = new ObjectID(event._id); + if (!repetition) { + return null; } + + eventOriginal = await this.getCollection(this.TYPES.EVENTS) + .findOne({ + groupHash: repetition.groupHash, + }); } - const update = { - $set: { assignee: assignee }, - }; + return eventOriginal; + } - return collection.updateOne(query, update); + /** + * Compose event with repetition + * + * @param {Event} event - event + * @param {Repetition} repetition - repetition + * @returns {Event} event merged with repetition + */ + _composeEventWithRepetition(event, repetition) { + return { + ...event, + _id: repetition._id, + firstAppearanceTimestamp: event.timestamp, + timestamp: repetition.timestamp, + payload: composeEventPayloadWithRepetition(event.payload, repetition), + }; } } diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index e084b957..d11827cf 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -198,9 +198,9 @@ type EventMarks { """ Object returned in repetitions property of event object """ -type RepetitionsResponse { +type RepetitionsPortion { repetitions: [Event!] - cursor: String + nextCursor: String } """ @@ -255,7 +255,7 @@ type Event { """ Event repetitions """ - repetitions(cursor: String = null, limit: Int = 10): RepetitionsResponse! + repetitions(cursor: String = null, limit: Int = 10): RepetitionsPortion! """ Array of users who visited event diff --git a/src/utils/merge.ts b/src/utils/merge.ts index 090ada79..3c336d92 100644 --- a/src/utils/merge.ts +++ b/src/utils/merge.ts @@ -71,22 +71,21 @@ function stringifyPayloadField(payload: GroupedEventDBScheme['payload'], field: } /** - * Helps to merge original event and repetition due to delta format, + * Helps to merge original event payload and repetition due to delta format, * in case of old delta format, we need to patch the payload * in case of new delta format, we need to assemble the payload * - * @param originalEvent {HawkEvent} - The original event - * @param repetition {HawkEventRepetition} - The repetition to process - * @returns {HawkEvent} Updated event with processed repetition payload + * @param originalEventPayload {GroupedEventDBScheme['payload']} - The original event payload + * @param repetition {RepetitionDBScheme} - The repetition to process + * @returns {GroupedEventDBScheme['payload']} Updated event with processed repetition payload */ -export function composeFullRepetitionEvent(originalEvent: GroupedEventDBScheme, repetition: RepetitionDBScheme | undefined): GroupedEventDBScheme { +export function composeEventPayloadWithRepetition(originalEventPayload: GroupedEventDBScheme['payload'], repetition: RepetitionDBScheme | undefined): GroupedEventDBScheme['payload'] { /** * Make a deep copy of the original event, because we need to avoid mutating the original event */ - const event = cloneDeep(originalEvent); if (!repetition) { - return event; + return originalEventPayload; } /** @@ -96,35 +95,35 @@ export function composeFullRepetitionEvent(originalEvent: GroupedEventDBScheme, /** * Parse addons and context fields from string to object before patching */ - event.payload = parsePayloadField(event.payload, 'addons'); - event.payload = parsePayloadField(event.payload, 'context'); + originalEventPayload = parsePayloadField(originalEventPayload, 'addons'); + originalEventPayload = parsePayloadField(originalEventPayload, 'context'); - event.payload = patch({ - left: event.payload, + originalEventPayload = patch({ + left: originalEventPayload, delta: JSON.parse(repetition.delta), }); /** * Stringify addons and context fields from object to string after patching */ - event.payload = stringifyPayloadField(event.payload, 'addons'); - event.payload = stringifyPayloadField(event.payload, 'context'); + originalEventPayload = stringifyPayloadField(originalEventPayload, 'addons'); + originalEventPayload = stringifyPayloadField(originalEventPayload, 'context'); - return event; + return originalEventPayload; } /** * New delta format (repetition.payload is null) and repetition.delta is null (there is no delta between original and repetition) */ if (!repetition.payload) { - return event; + return originalEventPayload; } /** * Old delta format (repetition.payload is not null) * @todo remove after 6 september 2025 */ - event.payload = repetitionAssembler(event.payload, repetition.payload); + originalEventPayload = repetitionAssembler(originalEventPayload, repetition.payload); - return event; + return originalEventPayload; } diff --git a/test/utils/merge.test.ts b/test/utils/merge.test.ts index c065d338..4ab62484 100644 --- a/test/utils/merge.test.ts +++ b/test/utils/merge.test.ts @@ -1,9 +1,9 @@ -import { composeFullRepetitionEvent } from '../../src/utils/merge'; +import { composeEventPayloadWithRepetition } from '../../src/utils/merge'; import { GroupedEventDBScheme, RepetitionDBScheme } from '@hawk.so/types'; import { diff } from '@n1ru4l/json-patch-plus'; -describe('composeFullRepetitionEvent', () => { +describe('composeEventPayloadWithRepetition', () => { const mockOriginalEvent: GroupedEventDBScheme = { groupHash: 'original-event-1', totalCount: 1, @@ -33,14 +33,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result).toEqual(mockOriginalEvent); - expect(result).toMatchObject(mockOriginalEvent); - expect(result.payload).toMatchObject(mockOriginalEvent.payload); + expect(result).toMatchObject(mockOriginalEvent.payload); }); }); @@ -67,12 +65,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'Updated message', type: 'warning', addons: JSON.stringify({ userId: 123 }), @@ -102,12 +100,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'Original message', type: 'error', release: 'v1.0.0', @@ -133,13 +131,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result).toEqual(mockOriginalEvent); - expect(result).not.toBe(mockOriginalEvent); // Должна быть глубокая копия + expect(result).toEqual(mockOriginalEvent.payload); }); }); @@ -162,12 +159,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'Updated message', type: 'warning', release: 'v1.0.0', @@ -194,12 +191,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'Updated title', // repetition value replaces original type: 'info', // Addons and context should be, because old format doesn't remove fields @@ -225,12 +222,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(mockOriginalEvent, repetition); + const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'Original message', // null в repetition должно сохранить оригинальное значение type: 'info', addons: JSON.stringify({ userId: 123 }), @@ -273,12 +270,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(eventWithEmptyPayload, repetition); + const result = composeEventPayloadWithRepetition(eventWithEmptyPayload.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'New message', }); }); @@ -313,12 +310,12 @@ describe('composeFullRepetitionEvent', () => { /** * Act */ - const result = composeFullRepetitionEvent(eventWithNullPayload, repetition); + const result = composeEventPayloadWithRepetition(eventWithNullPayload.payload, repetition); /** * Assert */ - expect(result.payload).toEqual({ + expect(result).toEqual({ title: 'New message', }); }); @@ -359,7 +356,7 @@ describe('composeFullRepetitionEvent', () => { * Act & Assert */ expect(() => { - composeFullRepetitionEvent(eventWithInvalidJSON, repetition); + composeEventPayloadWithRepetition(eventWithInvalidJSON.payload, repetition); }).toThrow(); // Должно выбросить ошибку при парсинге невалидного JSON }); }); From a507bb948c8bc296acf1088ca74bbdd1a6d7cc74 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 22:09:38 +0300 Subject: [PATCH 11/14] fixes --- src/models/eventsFactory.js | 25 +++++++++++++------------ src/typeDefs/event.ts | 4 ++-- test/utils/merge.test.ts | 10 +++++++++- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 0af56310..7641800e 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -395,7 +395,7 @@ class EventsFactory extends Factory { * * @param {string|ObjectID} eventId - Event's id (may be repetition id) * @param {Number} limit - count limitations - * @param {Number} cursor - selection offset + * @param {Number} cursor - pointer to the next repetition * * @return {EventRepetitionSchema[]} * @@ -658,31 +658,32 @@ class EventsFactory extends Factory { * @returns {Promise} original event or null if not found */ async _findOriginalEvent(eventId) { - let eventOriginal = await this.getCollection(this.TYPES.EVENTS) + let originalEvent; + + /** + * Try to find it by repetitionId + */ + const repetition = await this.getCollection(this.TYPES.REPETITIONS) .findOne({ _id: new ObjectID(eventId), }); /** - * If event is not found, try to find it as repetition + * If repetition is not found by eventId, try to find it by eventId */ - if (!eventOriginal) { - const repetition = await this.getCollection(this.TYPES.REPETITIONS) + if (!repetition) { + originalEvent = await this.getCollection(this.TYPES.EVENTS) .findOne({ _id: new ObjectID(eventId), }); - - if (!repetition) { - return null; - } - - eventOriginal = await this.getCollection(this.TYPES.EVENTS) + } else { + originalEvent = await this.getCollection(this.TYPES.EVENTS) .findOne({ groupHash: repetition.groupHash, }); } - return eventOriginal; + return originalEvent; } /** diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index d11827cf..b4e91181 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -253,9 +253,9 @@ type Event { release: Release """ - Event repetitions + Event repetitions portion """ - repetitions(cursor: String = null, limit: Int = 10): RepetitionsPortion! + repetitionsPortion(cursor: String = null, limit: Int = 10): RepetitionsPortion! """ Array of users who visited event diff --git a/test/utils/merge.test.ts b/test/utils/merge.test.ts index 4ab62484..fa9d703a 100644 --- a/test/utils/merge.test.ts +++ b/test/utils/merge.test.ts @@ -206,6 +206,14 @@ describe('composeEventPayloadWithRepetition', () => { }); it('should preserve original value when repetition payload has null', () => { + + const originalEventPayload = { + title: 'Original message', + type: 'error', + addons: JSON.stringify({ userId: 123 }), + context: JSON.stringify({ sessionId: 'abc' }), + }; + /** * Arrange */ @@ -222,7 +230,7 @@ describe('composeEventPayloadWithRepetition', () => { /** * Act */ - const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); + const result = composeEventPayloadWithRepetition(originalEventPayload, repetition); /** * Assert From ff67dc70a731cb292798777d72855fd8955d892f Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 22:12:33 +0300 Subject: [PATCH 12/14] fixes --- src/resolvers/event.js | 6 +++--- test/utils/merge.test.ts | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/resolvers/event.js b/src/resolvers/event.js index f1a77c1b..78eea7d1 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/event.js @@ -20,7 +20,7 @@ module.exports = { Event: { /** - * Returns repetitions list of the event + * Returns repetitions portion of the event * * @param {ResolverObj} _obj * @param {String} eventId @@ -28,9 +28,9 @@ module.exports = { * @param {Number} limit * @param {Number} cursor * - * @return {EventRepetitionSchema[]} + * @return {RepetitionsPortion} */ - async repetitions({ _id: eventId, projectId }, { limit, cursor }) { + async repetitionsPortion({ _id: eventId, projectId }, { limit, cursor }) { const factory = new EventsFactory(projectId); return factory.getEventRepetitions(eventId, limit, cursor); diff --git a/test/utils/merge.test.ts b/test/utils/merge.test.ts index fa9d703a..c65bc8bf 100644 --- a/test/utils/merge.test.ts +++ b/test/utils/merge.test.ts @@ -175,6 +175,12 @@ describe('composeEventPayloadWithRepetition', () => { }); it('should handle null values in repetition payload', () => { + const originalEventPayload = { + title: 'Original message', + type: 'error', + addons: JSON.stringify({ userId: 123 }), + context: JSON.stringify({ sessionId: 'abc' }), + }; /** * Arrange */ @@ -191,7 +197,7 @@ describe('composeEventPayloadWithRepetition', () => { /** * Act */ - const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); + const result = composeEventPayloadWithRepetition(originalEventPayload, repetition); /** * Assert From fc948730181327e00f767f92663b9a20b0916835 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 22:14:48 +0300 Subject: [PATCH 13/14] fixes test --- test/utils/merge.test.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test/utils/merge.test.ts b/test/utils/merge.test.ts index c65bc8bf..0c30f31c 100644 --- a/test/utils/merge.test.ts +++ b/test/utils/merge.test.ts @@ -79,13 +79,20 @@ describe('composeEventPayloadWithRepetition', () => { }); it('should handle delta with new fields', () => { + + const originalEventPayload = { + title: 'Original message', + type: 'error', + addons: JSON.stringify({ userId: 123 }), + context: JSON.stringify({ sessionId: 'abc' }), + }; /** * Arrange */ const delta = diff({ - left: mockOriginalEvent.payload, + left: originalEventPayload, right: { - ...mockOriginalEvent.payload, + ...originalEventPayload, release: 'v1.0.0', catcherVersion: '2.0.0', }, @@ -100,7 +107,7 @@ describe('composeEventPayloadWithRepetition', () => { /** * Act */ - const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); + const result = composeEventPayloadWithRepetition(originalEventPayload, repetition); /** * Assert @@ -141,6 +148,14 @@ describe('composeEventPayloadWithRepetition', () => { }); describe('when repetition.delta is undefined and repetition.payload is provided (old delta format)', () => { + + const originalEventPayload = { + title: 'Original message', + type: 'error', + addons: JSON.stringify({ userId: 123 }), + context: JSON.stringify({ sessionId: 'abc' }), + }; + it('should use repetitionAssembler to merge payloads', () => { /** * Arrange @@ -159,7 +174,7 @@ describe('composeEventPayloadWithRepetition', () => { /** * Act */ - const result = composeEventPayloadWithRepetition(mockOriginalEvent.payload, repetition); + const result = composeEventPayloadWithRepetition(originalEventPayload, repetition); /** * Assert @@ -175,6 +190,7 @@ describe('composeEventPayloadWithRepetition', () => { }); it('should handle null values in repetition payload', () => { + const originalEventPayload = { title: 'Original message', type: 'error', From 69d64e1a97c27266fdd0fa7dd966ed7cb8689020 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 16 Aug 2025 22:25:39 +0300 Subject: [PATCH 14/14] fix --- src/utils/merge.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/utils/merge.ts b/src/utils/merge.ts index 3c336d92..cc8d4d2e 100644 --- a/src/utils/merge.ts +++ b/src/utils/merge.ts @@ -83,9 +83,10 @@ export function composeEventPayloadWithRepetition(originalEventPayload: GroupedE /** * Make a deep copy of the original event, because we need to avoid mutating the original event */ + let result = cloneDeep(originalEventPayload); if (!repetition) { - return originalEventPayload; + return result; } /** @@ -95,35 +96,35 @@ export function composeEventPayloadWithRepetition(originalEventPayload: GroupedE /** * Parse addons and context fields from string to object before patching */ - originalEventPayload = parsePayloadField(originalEventPayload, 'addons'); - originalEventPayload = parsePayloadField(originalEventPayload, 'context'); + result = parsePayloadField(result, 'addons'); + result = parsePayloadField(result, 'context'); - originalEventPayload = patch({ - left: originalEventPayload, + result = patch({ + left: result, delta: JSON.parse(repetition.delta), }); /** * Stringify addons and context fields from object to string after patching */ - originalEventPayload = stringifyPayloadField(originalEventPayload, 'addons'); - originalEventPayload = stringifyPayloadField(originalEventPayload, 'context'); + result = stringifyPayloadField(result, 'addons'); + result = stringifyPayloadField(result, 'context'); - return originalEventPayload; + return result; } /** * New delta format (repetition.payload is null) and repetition.delta is null (there is no delta between original and repetition) */ if (!repetition.payload) { - return originalEventPayload; + return result; } /** * Old delta format (repetition.payload is not null) * @todo remove after 6 september 2025 */ - originalEventPayload = repetitionAssembler(originalEventPayload, repetition.payload); + result = repetitionAssembler(result, repetition.payload); - return originalEventPayload; + return result; }