From 7bbf4721df474bcf0e961a1df5fa628a68e4e29c Mon Sep 17 00:00:00 2001 From: Pavel Zotikov Date: Sun, 16 Mar 2025 20:21:59 +0300 Subject: [PATCH 01/16] Feature: Add search field for mongo queries (#474) * Feature: Add search field for mongo queries * Bump version up to 1.1.15 * add search in comment * Update eventsFactory.js * Update project.js * Update project.ts * rm unused * fix eslint --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Peter Savchenko --- .eslintrc.js | 10 ++- package.json | 2 +- src/models/eventsFactory.js | 125 ++++++++++++++++++------------------ src/resolvers/project.js | 12 +++- src/typeDefs/project.ts | 3 + 5 files changed, 86 insertions(+), 66 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 00f90d1c..12245e12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,5 +11,13 @@ module.exports = { 'require-jsdoc': 'warn', 'no-shadow': 'warn', 'no-unused-expressions': 'warn' - } + }, + overrides: [ + { + files: ['*.js'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off' + } + } + ] }; diff --git a/package.json b/package.json index 8b051888..4f3b0fde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.14", + "version": "1.1.15", "main": "index.ts", "license": "UNLICENSED", "scripts": { diff --git a/src/models/eventsFactory.js b/src/models/eventsFactory.js index 3ffe309c..a11b4f8f 100644 --- a/src/models/eventsFactory.js +++ b/src/models/eventsFactory.js @@ -149,6 +149,7 @@ class EventsFactory extends Factory { * @param {Number} skip - certain number of documents to skip * @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order * @param {EventsFilters} filters - marks by which events should be filtered + * @param {String} search - Search query * * @return {RecentEventSchema[]} */ @@ -156,8 +157,15 @@ class EventsFactory extends Factory { limit = 10, skip = 0, sort = 'BY_DATE', - filters = {} + filters = {}, + search = '' ) { + if (typeof search !== 'string') { + throw new Error('Search parameter must be a string'); + } + + const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + limit = this.validateLimit(limit); switch (sort) { @@ -184,71 +192,64 @@ class EventsFactory extends Factory { }, ]; - /** - * If some events should be omitted, use alternative pipeline - */ - if (Object.values(filters).length > 0) { - pipeline.push( - /** - * Lookup events object for each daily event - */ - { - $lookup: { - from: 'events:' + this.projectId, - localField: 'groupHash', - foreignField: 'groupHash', - as: 'event', + const searchFilter = search.trim().length > 0 + ? { + $or: [ + { + 'event.payload.title': { + $regex: escapedSearch, + $options: 'i', + }, }, - }, - { - $unwind: '$event', - }, - /** - * Match filters - */ - { - $match: { - ...Object.fromEntries( - Object - .entries(filters) - .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) - ), + { + 'event.payload.backtrace.file': { + $regex: escapedSearch, + $options: 'i', + }, }, + ], + } + : {}; + + const matchFilter = filters + ? Object.fromEntries( + Object + .entries(filters) + .map(([mark, exists]) => [`event.marks.${mark}`, { $exists: exists } ]) + ) + : {}; + + pipeline.push( + { + $lookup: { + from: 'events:' + this.projectId, + localField: 'groupHash', + foreignField: 'groupHash', + as: 'event', }, - { $skip: skip }, - { $limit: limit }, - { - $group: { - _id: null, - dailyInfo: { $push: '$$ROOT' }, - events: { $push: '$event' }, - }, + }, + { + $unwind: '$event', + }, + { + $match: { + ...matchFilter, + ...searchFilter, }, - { - $unset: 'dailyInfo.event', - } - ); - } else { - pipeline.push( - { $skip: skip }, - { $limit: limit }, - { - $group: { - _id: null, - groupHash: { $addToSet: '$groupHash' }, - dailyInfo: { $push: '$$ROOT' }, - }, + }, + { $skip: skip }, + { $limit: limit }, + { + $group: { + _id: null, + dailyInfo: { $push: '$$ROOT' }, + events: { $push: '$event' }, }, - { - $lookup: { - from: 'events:' + this.projectId, - localField: 'groupHash', - foreignField: 'groupHash', - as: 'events', - }, - } - ); - } + }, + { + $unset: 'dailyInfo.event', + } + ); const cursor = this.getCollection(this.TYPES.DAILY_EVENTS).aggregate(pipeline); @@ -316,7 +317,7 @@ class EventsFactory extends Factory { }); /** - * Group events using 'groupByTimestamp:NNNNNNNN' key + * Group events using 'groupingTimestamp:NNNNNNNN' key * @type {ProjectChartItem[]} */ const groupedData = groupBy('groupingTimestamp')(dailyEvents); diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 27811b3e..9c0ada37 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -11,6 +11,7 @@ const ProjectModel = require('../models/project').default; const EVENTS_GROUP_HASH_INDEX_NAME = 'groupHashUnique'; const REPETITIONS_GROUP_HASH_INDEX_NAME = 'groupHash_hashed'; const REPETITIONS_USER_ID_INDEX_NAME = 'userId'; +const MAX_SEARCH_QUERY_LENGTH = 50; /** * See all types and fields here {@see ../typeDefs/project.graphql} @@ -304,13 +305,20 @@ module.exports = { * @param {Number} skip - certain number of documents to skip * @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order * @param {EventsFilters} filters - marks by which events should be filtered + * @param {String} search - search query * * @return {Promise} */ - async recentEvents(project, { limit, skip, sort, filters }) { + async recentEvents(project, { limit, skip, sort, filters, search }) { + if (search) { + if (search.length > MAX_SEARCH_QUERY_LENGTH) { + search = search.slice(0, MAX_SEARCH_QUERY_LENGTH); + } + } + const factory = new EventsFactory(project._id); - return factory.findRecent(limit, skip, sort, filters); + return factory.findRecent(limit, skip, sort, filters, search); }, /** diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index 9101ae59..2bac1daa 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -124,6 +124,9 @@ type Project { "Event marks by which events should be sorted" filters: EventsFiltersInput + + "Search query" + search: String ): RecentEvents """ Return events that occurred after a certain timestamp From fe49f5413b6453ea8471e89cfee4b466e78a324a Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:24:57 +0300 Subject: [PATCH 02/16] deps(): update types dependencie --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8b051888..734b51cc 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@graphql-tools/schema": "^8.5.1", "@graphql-tools/utils": "^8.9.0", "@hawk.so/nodejs": "^3.1.1", - "@hawk.so/types": "^0.1.26", + "@hawk.so/types": "^0.1.28", "@types/amqp-connection-manager": "^2.0.4", "@types/bson": "^4.0.5", "@types/debug": "^4.1.5", diff --git a/yarn.lock b/yarn.lock index ec798ed3..1202912e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -458,10 +458,10 @@ dependencies: "@types/mongodb" "^3.5.34" -"@hawk.so/types@^0.1.26": - version "0.1.26" - resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.26.tgz#780d68c317024cd918011f1edfee4ef4001c4ad6" - integrity sha512-7WYhvfGgb3Q9pj3cWjpIFdcoxKNVsK+iqt1LgFdFqfCyLVLZXo9qxujaoTHB6OlC2IJ7WNjeTDUvb6yD4k+oIw== +"@hawk.so/types@^0.1.28": + version "0.1.28" + resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.28.tgz#a479f411a4ae1855a6661084fa4396c7f323b170" + integrity sha512-W8xNlbkQuffwhVn/ja5Bo4EglN0waSM0Rx3R+jGmcrbYi1a4g6kGPQFYkMSd0WadikOH1nd9NrfmyJB9cVOBWA== dependencies: "@types/mongodb" "^3.5.34" From 5aed97e2df11cf75c9ef4e7595776db32b9dffb0 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:27:40 +0300 Subject: [PATCH 03/16] feat(models): added project patterns methods --- src/models/project.ts | 187 ++++++++++++++++++++++++++++++------------ 1 file changed, 136 insertions(+), 51 deletions(-) diff --git a/src/models/project.ts b/src/models/project.ts index c22bd54c..84f3ed64 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -1,59 +1,9 @@ import { Collection, ObjectId } from 'mongodb'; import AbstractModel from './abstractModel'; import { NotificationsChannelsDBScheme } from '../types/notification-channels'; -import { ProjectDBScheme } from '@hawk.so/types'; +import { ProjectDBScheme, ProjectNotificationsRuleDBScheme, ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; import { v4 as uuid } from 'uuid'; -/** - * This structure represents a single rule of notifications settings - */ -export interface ProjectNotificationsRuleDBScheme { - /** - * Id of Rule - */ - _id: ObjectId; - - /** - * Allows to disable rule without removing - */ - isEnabled: boolean; - - /** - * Creator of the rule - */ - uidAdded: ObjectId; - - /** - * Receive type: 'SEEN_MORE' or 'ONLY_NEW' - */ - whatToReceive: ReceiveTypes; - - /** - * Only those which contains passed words - */ - including: string[]; - - /** - * Skip those which contains passed words - */ - excluding: string[]; - - /** - * Available channels to receive - */ - channels: NotificationsChannelsDBScheme; - - /** - * If this number of events is reached in the eventThresholdPeriod, the rule will be triggered - */ - threshold?: number; - - /** - * Size of period (in milliseconds) to count events to compare to rule threshold - */ - thresholdPeriod?: number; -} - /** * Available options of 'What to receive' */ @@ -159,6 +109,33 @@ interface UpdateProjectNotificationsRulePayload { thresholdPeriod?: number; } +/** + * Payload for creating new project pattern + */ +type CreateProjectPatternPayload = { + pattern: string, +}; + +/** + * Payload for updating project patterns + * It will just rewrite the whole lits of patterns + */ +type UpdateProjectPatternPayload = { + /** + * Id of the pattern to be updated + */ + id: string, + + /** + * New pattern string + */ + pattern: string, +}; + +type RemoveProjectPatternPayload = { + id: string, +} + /** * Project model to work with project data */ @@ -208,6 +185,11 @@ export default class ProjectModel extends AbstractModel impleme */ public notifications!: ProjectNotificationsRuleDBScheme[]; + /** + * Project events grouping pattern list + */ + public eventGroupingPatterns!: ProjectEventGroupingPatternsDBScheme[]; + /** * Model's collection */ @@ -282,6 +264,109 @@ export default class ProjectModel extends AbstractModel impleme return rule; } + /** + * Method for appending patterns list with new pattern + * @param payload - object that contains pattern string + * @returns - pattern, that has been added + */ + public async createProjectEventGroupingPattern(payload: CreateProjectPatternPayload): Promise { + const pattern: ProjectEventGroupingPatternsDBScheme = { + _id: new ObjectId(), + pattern: payload.pattern, + } + + console.log('inserted new pattern to the db'); + + await this.collection.updateOne({ + _id: this._id, + }, + { + $push: { + eventGroupingPatterns: { + $each: [ pattern ], + $position: 0, + } + } + }); + + console.log('pattern: ', pattern); + + return pattern; + } + + /** + * Method that rewrites pattern by id + * @param payload - object that contains id of the pattern to be updated and new pattern string + * @returns - updated pattern + */ + public async updateProjectEventGroupingPattern(payload: UpdateProjectPatternPayload): Promise { + const udpatedPattern = { + _id: new ObjectId(payload.id), + pattern: payload.pattern + } + + await this.collection.updateOne({ + _id: this._id, "eventGroupingPatterns._id": new ObjectId(udpatedPattern._id) + }, + { + $set: { "eventGroupingPatterns.$.pattern": udpatedPattern.pattern } + }); + + return udpatedPattern; + } + + /** + * Method that gets all patterns for project + * @returns - list of patterns related to the current project + */ + public async getProjectPatterns(): Promise { + const project = await this.collection.findOne({ + _id: this._id, + }); + + if (!project) { + throw new Error('Project with such id does not exist'); + } + + return project.eventGroupingPatterns ?? []; + } + + /** + * Method that removes pattern by its id + * @param payload - object that contains id of the pattern to be removed + */ + public async removeProjectEventGroupingPattern(payload: RemoveProjectPatternPayload): Promise { + const project = await this.collection.findOne({ + _id: this._id, + }); + + if (!project) { + throw new Error ('Project with such id does not exist'); + } + + const patternList = await this.collection.findOne( + { _id: this._id, "eventGroupingPatterns._id": new ObjectId(payload.id) }, + { projection: { "eventGroupingPatterns.$": 1 } } + ); + + const deletedPattern = patternList?.eventGroupingPatterns[0]; + + if (deletedPattern === undefined) { + throw new Error ('Pattern with such id does not exist') + } + + await this.collection.updateOne( + { + _id: new ObjectId(this._id) + }, + { + $pull: { eventGroupingPatterns: { _id: new ObjectId(payload.id) } } + } + ); + + return deletedPattern; + } + /** * Updates notifications rule in project * @param payload - data for updating From 4794e6d46c0a5cc4de46b33fb547f1ad35c72cd3 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:28:13 +0300 Subject: [PATCH 04/16] feat(resolvers): add project patterns resolvers --- src/resolvers/index.js | 2 + src/resolvers/projectNotifications.ts | 6 +- src/resolvers/projectPatterns.ts | 132 ++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/resolvers/projectPatterns.ts diff --git a/src/resolvers/index.js b/src/resolvers/index.js index 28589515..5bfeb603 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -13,6 +13,7 @@ const project = require('./project'); const event = require('./event'); const plans = require('./plans').default; const projectNotifications = require('./projectNotifications').default; +const projectPatterns = require('./projectPatterns').default; const userNotifications = require('./userNotifications').default; const billing = require('./billingNew').default; const EncodedJSON = require('./encodedJSON').default; @@ -71,6 +72,7 @@ const resolvers = [ project, event, projectNotifications, + projectPatterns, userNotifications, plans, billing, diff --git a/src/resolvers/projectNotifications.ts b/src/resolvers/projectNotifications.ts index 8755b576..d9d67bc4 100644 --- a/src/resolvers/projectNotifications.ts +++ b/src/resolvers/projectNotifications.ts @@ -1,10 +1,10 @@ import { - ProjectNotificationsRuleDBScheme, ReceiveTypes } from '../models/project'; +import { ProjectNotificationsRuleDBScheme } from '@hawk.so/types'; import { ResolverContextWithUser } from '../types/graphql'; import { ApolloError, UserInputError } from 'apollo-server-express'; -import { NotificationsChannelsDBScheme, NotificationsChannelSettingsDBScheme } from '../types/notification-channels'; +import { NotificationsChannelsDBScheme } from '../types/notification-channels'; /** * Mutation payload for creating notifications rule from GraphQL Schema @@ -223,7 +223,7 @@ export default { }, /** - * Toggles isEnabled field in in project notifications rule + * Toggles isEnabled field in project notifications rule * @param _obj - parent object * @param user - current authorized user {@see ../index.js} * @param factories - factories for working with models diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts new file mode 100644 index 00000000..b8c86cee --- /dev/null +++ b/src/resolvers/projectPatterns.ts @@ -0,0 +1,132 @@ +import { ResolverContextWithUser } from "../types/graphql"; +import { ApolloError } from 'apollo-server-express'; +import { ProjectEventGroupingPatternsDBScheme } from "@hawk.so/types"; + +/** + * Type that represents payload for create project pattern mutation + */ +interface CreateProjectPatternMutationPayload { + /** + * Id of the project to create new pattern + */ + projectId: string; + + /** + * New pattern to be inserted + */ + pattern: string; +}; + +/** + * Type that represents payload for update project pattern mutation + */ +interface UpdateProjectPatternMutationPayload { + /** + * Id of the pattern to be updated + */ + id: string, + + /** + * ProjectId of the pattern to be updated + */ + projectId: string, + + /** + * New pattern + */ + pattern: string, +}; + +/** + * Type that represents payload for remove project pattern mutation + */ +interface RemoveProjectPatternMutationPayload { + id: string, + + projectId: string, +} + +export default { + Mutation: { + /** + * Creates new events grouping pattern + * @param _obj - parent object + * @param user - current authorized user {@see ../index.js} + * @param factories - factories for working with models + * @param input - input data for creating + */ + async createProjectEventGroupingPattern( + _obj: undefined, + { input }: { input: CreateProjectPatternMutationPayload }, + { user, factories }: ResolverContextWithUser + ): Promise { + const project = await factories.projectsFactory.findById(input.projectId); + + if (!project) { + throw new ApolloError('No project with such id'); + } + + const existingPatterns = await project.getProjectPatterns(); + + existingPatterns.forEach(pattern => { + if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { + throw new ApolloError('New pattern collides with existing one') + } + }) + + return await project.createProjectEventGroupingPattern({ pattern: input.pattern }); + }, + + /** + * Updates one events grouping pattern + * @param _obj - parent object + * @param user - current authorized user {@see ../index.js} + * @param factories - factories for working with models + * @param input - input data for creating + */ + async updateProjectEventGroupingPattern( + _obj: undefined, + { input }: { input: UpdateProjectPatternMutationPayload }, + { user, factories }: ResolverContextWithUser + ): Promise { + const project = await factories.projectsFactory.findById(input.projectId); + + if (!project) { + throw new ApolloError('No project with such id'); + } + + const existingPatterns = await project.getProjectPatterns(); + + existingPatterns.forEach(pattern => { + if (pattern._id.toString() !== input.id) { + if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { + throw new ApolloError('New pattern collides with existing one') + } + } + }); + + return await project.updateProjectEventGroupingPattern(input); + }, + + /** + * Updates one events grouping pattern + * @param _obj - parent object + * @param user - current authorized user {@see ../index.js} + * @param factories - factories for working with models + * @param input - input data for creating + */ + async removeProjectEventGroupingPattern( + obj: undefined, + { input }: { input: RemoveProjectPatternMutationPayload }, + { user, factories }: ResolverContextWithUser + ): Promise { + const project = await factories.projectsFactory.findById(input.projectId); + + if (!project) { + throw new ApolloError('No project with such id'); + } + + return await project.removeProjectEventGroupingPattern({ id: input.id }); + } + } +} \ No newline at end of file From a2e9d263f9affffceccdfd33538b781b3695aa3b Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:30:27 +0300 Subject: [PATCH 05/16] feat(typeDefs): add type definitions for event grouping patterns --- src/typeDefs/index.ts | 4 + src/typeDefs/project.ts | 5 ++ src/typeDefs/projectEventGroupingPattern.ts | 32 ++++++++ .../projectEventGroupingPatternMutations.ts | 79 +++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 src/typeDefs/projectEventGroupingPattern.ts create mode 100644 src/typeDefs/projectEventGroupingPatternMutations.ts diff --git a/src/typeDefs/index.ts b/src/typeDefs/index.ts index 117b64e4..49b1a7ca 100644 --- a/src/typeDefs/index.ts +++ b/src/typeDefs/index.ts @@ -6,6 +6,8 @@ import notifications from './notifications'; import notificationsInput from './notificationsInput'; import projectNotifications from './projectNotifications'; import projectNotificationsMutations from './projectNotificationsMutations'; +import projectEventGroupingPattern from './projectEventGroupingPattern'; +import projectEventGroupingPatternMutations from './projectEventGroupingPatternMutations'; import project from './project'; import user from './user'; import userNotifications from './userNotifications'; @@ -98,6 +100,8 @@ const typeDefinitions = [ workspaceMutations, chart, plans, + projectEventGroupingPattern, + projectEventGroupingPatternMutations, ]; if (isE2E) { diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index 9101ae59..b11feb3c 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -148,6 +148,11 @@ type Project { Project notification settings """ notifications: [ProjectNotificationsRule] + + """ + Event grouping patterns + """ + eventGroupingPatterns: [ProjectEventGroupingPattern] } extend type Query { diff --git a/src/typeDefs/projectEventGroupingPattern.ts b/src/typeDefs/projectEventGroupingPattern.ts new file mode 100644 index 00000000..6cb20884 --- /dev/null +++ b/src/typeDefs/projectEventGroupingPattern.ts @@ -0,0 +1,32 @@ +import { gql } from 'apollo-server-express'; + +export default gql` + """ + Project event grouping settings + """ + type ProjectEventGroupingPattern { + """ + id of the event grouping pattern + """ + id: ID! @renameFrom(name: "_id") + + """ + event grouping pattern string + """ + pattern: String! + } + + type ProjectEventGroupingPatternContent { + """ + event grouping pattern string + """ + pattern: String! + } + + type ProjectEventGroupingPatternPointer { + """ + id of the event grouping pattern + """ + id: ID! @renameFrom(name: "_id") + } +`; diff --git a/src/typeDefs/projectEventGroupingPatternMutations.ts b/src/typeDefs/projectEventGroupingPatternMutations.ts new file mode 100644 index 00000000..555adb5d --- /dev/null +++ b/src/typeDefs/projectEventGroupingPatternMutations.ts @@ -0,0 +1,79 @@ +import { gql } from 'apollo-server-express'; + +export default gql` + """ + Input type for creating new event grouping pattern + """ + input CreateProjectEventGroupingPatternInput { + """ + Pattern string + """ + pattern: String! + + """ + Id of the project + """ + projectId: ID! + } + + """ + Input type for updating of the event grouping pattern + """ + input UpdateProjectEventGroupingPatternInput { + """ + Id of the pattern to be updated + """ + id: ID! + + """ + New pattern string + """ + pattern: String! + + """ + Id of the project + """ + projectId: ID! + } + + """ + Input type for deleting of the event grouping pattern + """ + input RemoveProjectEventGroupingPatternInput { + """ + Id of the pattern to be removed + """ + id: ID! + + """ + Id of the project + """ + projectId: ID! + } + + extend type Mutation { + """ + Creates new event grouping pattern + """ + createProjectEventGroupingPattern( + "Data for creating" + input: CreateProjectEventGroupingPatternInput! + ): ProjectEventGroupingPattern @requireAdmin + + """ + Updates existing event grouping pattern + """ + updateProjectEventGroupingPattern( + "Data for updating" + input: UpdateProjectEventGroupingPatternInput! + ): ProjectEventGroupingPattern @requireAdmin + + """ + Removes notifications rule from project + """ + removeProjectEventGroupingPattern( + "Data for deleting" + input: RemoveProjectEventGroupingPatternInput! + ): ProjectEventGroupingPattern @requireAdmin + } +`; From 8dc001651938b36e0021f3042ef6eda1f433227d Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:33:42 +0300 Subject: [PATCH 06/16] docs(): improved type docs --- src/models/project.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/project.ts b/src/models/project.ts index 84f3ed64..d06e100c 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -133,6 +133,9 @@ type UpdateProjectPatternPayload = { }; type RemoveProjectPatternPayload = { + /** + * Id of the pattern to be removed + */ id: string, } From 00fa9df31eae6a5ea9f70bbfc1a36c561c32ae34 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:48:23 +0300 Subject: [PATCH 07/16] fix(): remove redundant logs --- src/models/project.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/models/project.ts b/src/models/project.ts index d06e100c..5c5df44a 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -278,8 +278,6 @@ export default class ProjectModel extends AbstractModel impleme pattern: payload.pattern, } - console.log('inserted new pattern to the db'); - await this.collection.updateOne({ _id: this._id, }, @@ -292,8 +290,6 @@ export default class ProjectModel extends AbstractModel impleme } }); - console.log('pattern: ', pattern); - return pattern; } From 1e78a1fe6406e56198c3e60b612227bfb6189869 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:00:54 +0300 Subject: [PATCH 08/16] chore(): lint fix --- src/models/project.ts | 50 +++++++++++++++++--------------- src/resolvers/projectPatterns.ts | 36 +++++++++++------------ 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/models/project.ts b/src/models/project.ts index 5c5df44a..6ba3574c 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -113,7 +113,7 @@ interface UpdateProjectNotificationsRulePayload { * Payload for creating new project pattern */ type CreateProjectPatternPayload = { - pattern: string, + pattern: string; }; /** @@ -124,19 +124,19 @@ type UpdateProjectPatternPayload = { /** * Id of the pattern to be updated */ - id: string, + id: string; /** * New pattern string */ - pattern: string, + pattern: string; }; type RemoveProjectPatternPayload = { /** * Id of the pattern to be removed */ - id: string, + id: string; } /** @@ -276,18 +276,18 @@ export default class ProjectModel extends AbstractModel impleme const pattern: ProjectEventGroupingPatternsDBScheme = { _id: new ObjectId(), pattern: payload.pattern, - } + }; await this.collection.updateOne({ _id: this._id, }, { - $push: { + $push: { eventGroupingPatterns: { $each: [ pattern ], $position: 0, - } - } + }, + }, }); return pattern; @@ -301,16 +301,17 @@ export default class ProjectModel extends AbstractModel impleme public async updateProjectEventGroupingPattern(payload: UpdateProjectPatternPayload): Promise { const udpatedPattern = { _id: new ObjectId(payload.id), - pattern: payload.pattern - } - + pattern: payload.pattern, + }; + await this.collection.updateOne({ - _id: this._id, "eventGroupingPatterns._id": new ObjectId(udpatedPattern._id) + _id: this._id, + 'eventGroupingPatterns._id': new ObjectId(udpatedPattern._id), }, - { - $set: { "eventGroupingPatterns.$.pattern": udpatedPattern.pattern } + { + $set: { 'eventGroupingPatterns.$.pattern': udpatedPattern.pattern }, }); - + return udpatedPattern; } @@ -340,26 +341,29 @@ export default class ProjectModel extends AbstractModel impleme }); if (!project) { - throw new Error ('Project with such id does not exist'); + throw new Error('Project with such id does not exist'); } const patternList = await this.collection.findOne( - { _id: this._id, "eventGroupingPatterns._id": new ObjectId(payload.id) }, - { projection: { "eventGroupingPatterns.$": 1 } } + { + _id: this._id, + 'eventGroupingPatterns._id': new ObjectId(payload.id), + }, + { projection: { 'eventGroupingPatterns.$': 1 } } ); - + const deletedPattern = patternList?.eventGroupingPatterns[0]; if (deletedPattern === undefined) { - throw new Error ('Pattern with such id does not exist') + throw new Error('Pattern with such id does not exist'); } await this.collection.updateOne( { - _id: new ObjectId(this._id) + _id: new ObjectId(this._id), }, - { - $pull: { eventGroupingPatterns: { _id: new ObjectId(payload.id) } } + { + $pull: { eventGroupingPatterns: { _id: new ObjectId(payload.id) } }, } ); diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index b8c86cee..5ab3e72d 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -1,6 +1,6 @@ -import { ResolverContextWithUser } from "../types/graphql"; +import { ResolverContextWithUser } from '../types/graphql'; import { ApolloError } from 'apollo-server-express'; -import { ProjectEventGroupingPatternsDBScheme } from "@hawk.so/types"; +import { ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; /** * Type that represents payload for create project pattern mutation @@ -24,26 +24,26 @@ interface UpdateProjectPatternMutationPayload { /** * Id of the pattern to be updated */ - id: string, + id: string; /** * ProjectId of the pattern to be updated */ - projectId: string, + projectId: string; /** * New pattern */ - pattern: string, + pattern: string; }; /** * Type that represents payload for remove project pattern mutation */ interface RemoveProjectPatternMutationPayload { - id: string, + id: string; - projectId: string, + projectId: string; } export default { @@ -70,9 +70,9 @@ export default { existingPatterns.forEach(pattern => { if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { - throw new ApolloError('New pattern collides with existing one') - } - }) + throw new ApolloError('New pattern collides with existing one'); + } + }); return await project.createProjectEventGroupingPattern({ pattern: input.pattern }); }, @@ -87,7 +87,7 @@ export default { async updateProjectEventGroupingPattern( _obj: undefined, { input }: { input: UpdateProjectPatternMutationPayload }, - { user, factories }: ResolverContextWithUser + { user, factories }: ResolverContextWithUser ): Promise { const project = await factories.projectsFactory.findById(input.projectId); @@ -100,8 +100,8 @@ export default { existingPatterns.forEach(pattern => { if (pattern._id.toString() !== input.id) { if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { - throw new ApolloError('New pattern collides with existing one') - } + throw new ApolloError('New pattern collides with existing one'); + } } }); @@ -114,11 +114,11 @@ export default { * @param user - current authorized user {@see ../index.js} * @param factories - factories for working with models * @param input - input data for creating - */ + */ async removeProjectEventGroupingPattern( obj: undefined, { input }: { input: RemoveProjectPatternMutationPayload }, - { user, factories }: ResolverContextWithUser + { user, factories }: ResolverContextWithUser ): Promise { const project = await factories.projectsFactory.findById(input.projectId); @@ -127,6 +127,6 @@ export default { } return await project.removeProjectEventGroupingPattern({ id: input.id }); - } - } -} \ No newline at end of file + }, + }, +}; \ No newline at end of file From 71a52a0231b4b7c64b022a8c253333447ce00f4e Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:04:24 +0300 Subject: [PATCH 09/16] imp(resolver): docs improved --- src/resolvers/projectPatterns.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index 5ab3e72d..d14115c3 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -74,7 +74,7 @@ export default { } }); - return await project.createProjectEventGroupingPattern({ pattern: input.pattern }); + return project.createProjectEventGroupingPattern({ pattern: input.pattern }); }, /** @@ -105,7 +105,7 @@ export default { } }); - return await project.updateProjectEventGroupingPattern(input); + return project.updateProjectEventGroupingPattern(input); }, /** @@ -116,7 +116,7 @@ export default { * @param input - input data for creating */ async removeProjectEventGroupingPattern( - obj: undefined, + _obj: undefined, { input }: { input: RemoveProjectPatternMutationPayload }, { user, factories }: ResolverContextWithUser ): Promise { @@ -126,7 +126,7 @@ export default { throw new ApolloError('No project with such id'); } - return await project.removeProjectEventGroupingPattern({ id: input.id }); + return project.removeProjectEventGroupingPattern({ id: input.id }); }, }, }; \ No newline at end of file From e3ae799c10a1cc641b0771800b56750ca128b4db Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:05:04 +0300 Subject: [PATCH 10/16] bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 734b51cc..851a17c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.14", + "version": "1.1.15", "main": "index.ts", "license": "UNLICENSED", "scripts": { From e56f1dbf4f7b6510158633997e34f67a2f94d948 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:05:09 +0000 Subject: [PATCH 11/16] Bump version up to 1.1.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 851a17c7..d74fed80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.15", + "version": "1.1.16", "main": "index.ts", "license": "UNLICENSED", "scripts": { From 265154be82fb194670b475f99776d2a3b0ed27a6 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:37:48 +0300 Subject: [PATCH 12/16] fix(): fix new pattern validation --- src/models/project.ts | 16 ------------ src/resolvers/projectPatterns.ts | 45 ++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/models/project.ts b/src/models/project.ts index 6ba3574c..256cfdd7 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -315,22 +315,6 @@ export default class ProjectModel extends AbstractModel impleme return udpatedPattern; } - /** - * Method that gets all patterns for project - * @returns - list of patterns related to the current project - */ - public async getProjectPatterns(): Promise { - const project = await this.collection.findOne({ - _id: this._id, - }); - - if (!project) { - throw new Error('Project with such id does not exist'); - } - - return project.eventGroupingPatterns ?? []; - } - /** * Method that removes pattern by its id * @param payload - object that contains id of the pattern to be removed diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index d14115c3..728eb02c 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -41,11 +41,38 @@ interface UpdateProjectPatternMutationPayload { * Type that represents payload for remove project pattern mutation */ interface RemoveProjectPatternMutationPayload { + /** + * Id of the pattern to be removed + */ id: string; + /** + * Id of the project to remove pattern + */ projectId: string; } +/** + * Validates a new event grouping pattern against existing patterns + * + * @param newEventGroupingPattern - The new pattern to validate + * @param existingEventGroupingPatternList - List of existing patterns to check against + * @throws Error if pattern is invalid or collides with existing patterns + */ +function validateNewEventGroupingPattern( + newEventGroupingPattern: string +): void { + /** + * Check if pattern is valid RegExp + */ + try { + new RegExp(newEventGroupingPattern); + } catch (error) { + throw new ApolloError('Invalid regular expression pattern'); + } +} + + export default { Mutation: { /** @@ -66,13 +93,7 @@ export default { throw new ApolloError('No project with such id'); } - const existingPatterns = await project.getProjectPatterns(); - - existingPatterns.forEach(pattern => { - if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { - throw new ApolloError('New pattern collides with existing one'); - } - }); + validateNewEventGroupingPattern(input.pattern); return project.createProjectEventGroupingPattern({ pattern: input.pattern }); }, @@ -95,15 +116,7 @@ export default { throw new ApolloError('No project with such id'); } - const existingPatterns = await project.getProjectPatterns(); - - existingPatterns.forEach(pattern => { - if (pattern._id.toString() !== input.id) { - if (pattern.pattern.match(new RegExp(input.pattern)) || input.pattern.match(new RegExp(pattern.pattern))) { - throw new ApolloError('New pattern collides with existing one'); - } - } - }); + validateNewEventGroupingPattern(input.pattern); return project.updateProjectEventGroupingPattern(input); }, From 2c2e1577019e119ff341be81f63d1b1784ded474 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Mon, 17 Mar 2025 00:21:24 +0300 Subject: [PATCH 13/16] fix(): eslint fix --- src/resolvers/projectPatterns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index 728eb02c..d10c72a7 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -54,7 +54,7 @@ interface RemoveProjectPatternMutationPayload { /** * Validates a new event grouping pattern against existing patterns - * + * * @param newEventGroupingPattern - The new pattern to validate * @param existingEventGroupingPatternList - List of existing patterns to check against * @throws Error if pattern is invalid or collides with existing patterns @@ -66,13 +66,13 @@ function validateNewEventGroupingPattern( * Check if pattern is valid RegExp */ try { + /* eslint-disable-next-line no-new */ new RegExp(newEventGroupingPattern); } catch (error) { throw new ApolloError('Invalid regular expression pattern'); } } - export default { Mutation: { /** From 49c4c19b2d257904a29ff99ec4afe693d5def926 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Mon, 17 Mar 2025 01:01:31 +0300 Subject: [PATCH 14/16] imp(resolvers): check that regexps are safe before saving --- package.json | 4 +++- src/resolvers/projectPatterns.ts | 9 +++++++++ yarn.lock | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d74fed80..d4cd585d 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@types/node": "^16.11.46", "@types/node-fetch": "^2.5.4", "@types/uuid": "^8.3.4", + "@types/safe-regex": "^1.1.6", "amqp-connection-manager": "^3.1.0", "amqplib": "^0.5.5", "apollo-server-express": "^3.10.0", @@ -73,6 +74,7 @@ "mime-types": "^2.1.25", "mongodb": "^3.7.3", "ts-node-dev": "^2.0.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "safe-regex": "^2.1.0" } } diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index d10c72a7..241caca3 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -1,6 +1,7 @@ import { ResolverContextWithUser } from '../types/graphql'; import { ApolloError } from 'apollo-server-express'; import { ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; +import { isSafeRegex } from 'safe-regex'; /** * Type that represents payload for create project pattern mutation @@ -68,6 +69,14 @@ function validateNewEventGroupingPattern( try { /* eslint-disable-next-line no-new */ new RegExp(newEventGroupingPattern); + + /** + * Check if pattern is safe RegExp + */ + if (!isSafeRegex(newEventGroupingPattern)) { + throw new ApolloError('Invalid regular expression pattern'); + } + } catch (error) { throw new ApolloError('Invalid regular expression pattern'); } diff --git a/yarn.lock b/yarn.lock index 1202912e..b7f1989a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1176,6 +1176,11 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/safe-regex@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/safe-regex/-/safe-regex-1.1.6.tgz#1f13a950b77869e19626ae2dcf79e12902b38c0b" + integrity sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ== + "@types/serve-static@*": version "1.15.0" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" @@ -5632,6 +5637,11 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -5803,6 +5813,13 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-regex@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" From 3a7d6379252579d149136e99477e7fc17d8427fd Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Mon, 17 Mar 2025 01:04:44 +0300 Subject: [PATCH 15/16] fix(): fix safe regex types --- src/resolvers/projectPatterns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index 241caca3..bf9ef92f 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -1,7 +1,7 @@ import { ResolverContextWithUser } from '../types/graphql'; import { ApolloError } from 'apollo-server-express'; import { ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; -import { isSafeRegex } from 'safe-regex'; +import safe from 'safe-regex'; /** * Type that represents payload for create project pattern mutation @@ -73,7 +73,7 @@ function validateNewEventGroupingPattern( /** * Check if pattern is safe RegExp */ - if (!isSafeRegex(newEventGroupingPattern)) { + if (!safe(newEventGroupingPattern)) { throw new ApolloError('Invalid regular expression pattern'); } From fd6c5044a83cc03c57c94584d6795d38814c1ce7 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Mon, 17 Mar 2025 01:06:17 +0300 Subject: [PATCH 16/16] fix(): lint fix --- src/resolvers/projectPatterns.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resolvers/projectPatterns.ts b/src/resolvers/projectPatterns.ts index bf9ef92f..aa3d372f 100644 --- a/src/resolvers/projectPatterns.ts +++ b/src/resolvers/projectPatterns.ts @@ -76,7 +76,6 @@ function validateNewEventGroupingPattern( if (!safe(newEventGroupingPattern)) { throw new ApolloError('Invalid regular expression pattern'); } - } catch (error) { throw new ApolloError('Invalid regular expression pattern'); }