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": { diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 1bac1551..54d3100d 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 @@ -393,21 +394,43 @@ class EventsFactory extends Factory { * * @param {string|ObjectID} eventId - Event's id * @param {Number} limit - count limitations - * @param {Number} skip - selection offset + * @param {Number} cursor - cursor for pagination * * @return {EventRepetitionSchema[]} * * @todo move to Repetitions(?) model */ - async getEventRepetitions(eventId, limit = 10, skip = 0) { + async getEventRepetitions(repetitionId, 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); + let eventOriginal = await this.findById(repetitionId); + + /** + * If original event is not found, it can mean that client is trying to get repetitions of original event + */ + if (!eventOriginal) { + const repetition = await this.getEventRepetition(repetitionId); + + if (!repetition) { + return result; + } + + eventOriginal = await this.findById(repetition.eventId); + } + + if (!eventOriginal) { + return result; + } /** * Collect repetitions @@ -416,13 +439,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 +471,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 +490,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, + }; } /** @@ -501,7 +566,15 @@ class EventsFactory extends Factory { * * @return {Promise} */ - async visitEvent(eventId, userId) { + async visitEvent(repetitionId, userId) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ _id: new ObjectID(repetitionId) }); + + /** + * If repetition is not found, it can mean that client is trying to work with original event + */ + const eventId = repetition ? repetition.eventId : repetitionId; + return this.getCollection(this.TYPES.EVENTS) .updateOne( { _id: new ObjectID(eventId) }, @@ -517,7 +590,15 @@ class EventsFactory extends Factory { * * @return {Promise} */ - async toggleEventMark(eventId, mark) { + async toggleEventMark(repetitionId, mark) { + const repetition = await this.getCollection(this.TYPES.REPETITIONS) + .findOne({ _id: new ObjectID(repetitionId) }); + + /** + * If repetition is not found, it can mean that client is trying to work with original event + */ + const eventId = repetition ? repetition.eventId : repetitionId; + const collection = this.getCollection(this.TYPES.EVENTS); const query = { _id: new ObjectID(eventId) }; diff --git a/src/resolvers/index.js b/src/resolvers/index.js index 5bfeb603..9e77b186 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -10,7 +10,7 @@ const { const user = require('./user').default; const workspace = require('./workspace'); const project = require('./project'); -const event = require('./event'); +const repetition = require('./repetition'); const plans = require('./plans').default; const projectNotifications = require('./projectNotifications').default; const projectPatterns = require('./projectPatterns').default; @@ -70,7 +70,7 @@ const resolvers = [ user, workspace, project, - event, + repetition, projectNotifications, projectPatterns, userNotifications, diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 3849787e..8770b21e 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -280,24 +280,26 @@ module.exports = { }, /** - * Find project's event + * Find project's repetition * * @param {ProjectDBScheme} project - result of parent resolver - * @param {String} eventId - event's identifier + * @param {String} repetitionId - repetition's identifier * - * @returns {Event} + * @returns {Repetition} */ - async event(project, { id: eventId }) { + async repetition(project, { id: repetitionId }) { const factory = new EventsFactory(project._id); - const event = await factory.findById(eventId); - if (!event) { - return null; - } + const repetition = await factory.getEventRepetition(repetitionId); - event.projectId = project._id; + /** + * If repetition is not found, it can mean that client is trying to get original event + */ + if (!repetition) { + return factory.getEventLastRepetition(repetitionId); + } - return event; + return repetition; }, /** diff --git a/src/resolvers/event.js b/src/resolvers/repetition.js similarity index 82% rename from src/resolvers/event.js rename to src/resolvers/repetition.js index 5a2cbb51..c6a2431a 100644 --- a/src/resolvers/event.js +++ b/src/resolvers/repetition.js @@ -3,10 +3,10 @@ const { ObjectID } = require('mongodb'); const sendPersonalNotification = require('../utils/personalNotifications').default; /** - * See all types and fields here {@see ../typeDefs/event.graphql} + * See all types and fields here {@see ../typeDefs/repetition.graphql} */ module.exports = { - EventMarks: { + RepetitionMarks: { starred(marks) { return 'starred' in marks; }, @@ -17,40 +17,22 @@ module.exports = { return 'resolved' in marks; }, }, - Event: { + Repetition: { /** - * 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 + * Returns repetitions list of the repetition * * @param {ResolverObj} _obj - * @param {String} eventId + * @param {String} repetitionId * @param {String} projectId * @param {Number} limit * @param {Number} skip * * @return {EventRepetitionSchema[]} */ - async repetitions({ _id: eventId, projectId }, { limit, skip }) { + async repetitions({ _id: repetitionId, projectId }, { limit, cursor }) { const factory = new EventsFactory(projectId); - return factory.getEventRepetitions(eventId, limit, skip); + return factory.getEventRepetitions(repetitionId, limit, cursor); }, /** @@ -124,7 +106,7 @@ module.exports = { }, Mutation: { /** - * Mark event as visited for current user + * Mark repetition as visited for current user * * @param {ResolverObj} _obj - resolver context * @param {string} project - project id @@ -132,7 +114,7 @@ module.exports = { * @param {UserInContext} user - user context * @return {Promise} */ - async visitEvent(_obj, { project, id }, { user }) { + async visitRepetition(_obj, { project, id }, { user }) { const factory = new EventsFactory(project); const { result } = await factory.visitEvent(id, user.id); @@ -149,10 +131,10 @@ module.exports = { * @param {string} mark - mark to set * @return {Promise} */ - async toggleEventMark(_obj, { project, eventId, mark }) { + async toggleRepetitionMark(_obj, { project, repetitionId, mark }) { const factory = new EventsFactory(project); - const { result } = await factory.toggleEventMark(eventId, mark); + const { result } = await factory.toggleEventMark(repetitionId, mark); return !!result.ok; }, @@ -162,9 +144,9 @@ module.exports = { * * @return {Function()} */ - events: () => ({}), + repetitions: () => ({}), }, - EventsMutations: { + RepetitionMutations: { /** * Update assignee to selected event * diff --git a/src/typeDefs/index.ts b/src/typeDefs/index.ts index 49b1a7ca..c242f961 100644 --- a/src/typeDefs/index.ts +++ b/src/typeDefs/index.ts @@ -1,7 +1,7 @@ import { gql } from 'apollo-server-express'; import billing from './billing'; -import event from './event'; +import repetition from './repetition'; import notifications from './notifications'; import notificationsInput from './notificationsInput'; import projectNotifications from './projectNotifications'; @@ -87,7 +87,7 @@ const rootSchema = gql` const typeDefinitions = [ rootSchema, billing, - event, + repetition, notifications, notificationsInput, projectNotifications, diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index f0fab0e2..0fa93123 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -94,9 +94,9 @@ type Project { uidAdded: User! """ - Project's Event + Project repetitions """ - event(id: ID!): Event + repetition: Repetition! """ Project events @@ -107,7 +107,7 @@ type Project { "Certain number of documents to skip" skip: Int = 0 - ): [Event!] + ): [Repetition!] """ Returns recent events grouped by day diff --git a/src/typeDefs/event.ts b/src/typeDefs/repetition.ts similarity index 67% rename from src/typeDefs/event.ts rename to src/typeDefs/repetition.ts index a7990679..c6e3a72c 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/repetition.ts @@ -59,7 +59,7 @@ type Release { """ Event backtrace representation """ -type EventBacktraceFrame { +type RepetitionBacktraceFrame { """ Source filepath """ @@ -92,9 +92,9 @@ type EventBacktraceFrame { } """ -Event user representation +Repetition user representation """ -type EventUser { +type RepetitionUser { """ Internal user's identifier inside an app """ @@ -117,73 +117,13 @@ type EventUser { } """ -Type representing Event payload -""" -type EventPayload { - """ - 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 -} - -""" -Type representing Event payload. All fields can be omitted if there are no difference with the original +Type representing Repetition payload """ type RepetitionPayload { """ Event title """ - title: String + title: String! """ Event type: TypeError, ReferenceError etc. @@ -198,7 +138,7 @@ type RepetitionPayload { """ Event stack array from the latest call to the earliest """ - backtrace: [EventBacktraceFrame!] + backtrace: [RepetitionBacktraceFrame] """ Additional data about GET request @@ -223,7 +163,7 @@ type RepetitionPayload { """ Current authenticated user """ - user: EventUser + user: RepetitionUser """ Any additional data of Event @@ -237,59 +177,29 @@ type RepetitionPayload { } """ -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 +Possible repetition marks """ -enum EventMark { +enum RepetitionMark { resolved starred ignored } """ -Object returned in marks property of event object +Object returned in marks property of repetition object """ -type EventMarks { +type RepetitionMarks { resolved: Boolean! starred: Boolean! ignored: Boolean! } """ -Type representing Hawk single Event +Type representing Hawk single Repetition """ -type Event { +type Repetition { """ - Event id + Repetition id """ id: ID! @renameFrom(name: "_id") @@ -316,7 +226,7 @@ type Event { """ Event payload """ - payload: EventPayload! + payload: RepetitionPayload! """ Event timestamp @@ -324,29 +234,29 @@ type Event { timestamp: Float! """ - Release data + Event first appearance timestamp """ - release: Release + firstAppearanceTimestamp: Float! """ - Event concrete repetition + Release data """ - repetition(id: ID): Repetition + release: Release """ - Event repetitions + Array of users who visited repetition """ - repetitions(skip: Int = 0, limit: Int = 10): [Repetition!] + visitedBy: [User!] """ - Array of users who visited event + Repetition label for current user """ - visitedBy: [User!] + marks: RepetitionMarks! @default(value: "{}") """ - Event label for current user + Repetition's repetitions """ - marks: EventMarks! @default(value: "{}") + repetitions: [Repetition!] """ How many users catch this error @@ -408,7 +318,7 @@ type Subscription { """ Sends new events from all user projects """ - eventOccurred: Event! @requireAuth + repetitionOccurred: Repetition! @requireAuth } """ @@ -418,7 +328,7 @@ type RecentEvents { """ Occured events list """ - events: [Event!] + events: [Repetition!] """ Information about occurred events per day @@ -428,14 +338,14 @@ type RecentEvents { input UpdateAssigneeInput { """ - ID of project event is related to + ID of project repetition is related to """ projectId: ID! """ - ID of the selected event + ID of the selected repetition """ - eventId: ID! + repetitionId: ID! """ Assignee id to set @@ -462,9 +372,9 @@ input RemoveAssigneeInput { projectId: ID! """ - ID of the selected event + ID of the selected repetition """ - eventId: ID! + repetitionId: ID! } type RemoveAssigneeResponse { @@ -474,7 +384,7 @@ type RemoveAssigneeResponse { success: Boolean! } -type EventsMutations { +type RepetitionMutations { """ Set an assignee for the selected event """ @@ -492,36 +402,36 @@ type EventsMutations { extend type Mutation { """ - Mutation marks event as visited for current user + Mutation marks repetition as visited for current user """ - visitEvent( + visitRepetition( project: ID! id: ID! ): Boolean! @requireAuth """ - Mutation sets or unsets passed mark to event + Mutation sets or unsets passed mark to repetition """ - toggleEventMark( + toggleRepetitionMark( """ ID of project event is related to """ project: ID! """ - EvenID of the event to set the mark + Repetition ID to set the mark """ - eventId: ID! + repetitionId: ID! """ Mark to set """ - mark: EventMark! + mark: RepetitionMark! ): Boolean! @requireAuth """ - Namespace that contains only mutations related to the events + Namespace that contains only mutations related to the repetitions """ - events: EventsMutations! + repetitions: RepetitionMutations! } `;