diff --git a/.github/workflows/build-and-push-docker-image.yml b/.github/workflows/build-and-push-docker-image.yml index 0c1bab31..b87d991c 100644 --- a/.github/workflows/build-and-push-docker-image.yml +++ b/.github/workflows/build-and-push-docker-image.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 + with: + ref: stage - name: Login to GitHub registry uses: docker/login-action@v2 @@ -55,4 +57,5 @@ jobs: file: docker/Dockerfile.prod tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - push: ${{ startsWith(github.ref, 'refs/tags/v') || endsWith(github.ref, '/stage') || endsWith(github.ref, '/prod') }} + push: ${{ github.ref == 'refs/heads/stage' || github.ref == 'refs/heads/prod' || startsWith(github.ref, 'refs/tags/v') }} + no-cache: true diff --git a/package.json b/package.json index a0c255f1..10a4f8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.16", + "version": "1.1.17", "main": "index.ts", "license": "UNLICENSED", "scripts": { @@ -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", @@ -48,6 +48,7 @@ "@types/mongodb": "^3.6.20", "@types/node": "^16.11.46", "@types/node-fetch": "^2.5.4", + "@types/safe-regex": "^1.1.6", "@types/uuid": "^8.3.4", "amqp-connection-manager": "^3.1.0", "amqplib": "^0.5.5", @@ -72,6 +73,7 @@ "migrate-mongo": "^7.0.1", "mime-types": "^2.1.25", "mongodb": "^3.7.3", + "safe-regex": "^2.1.0", "ts-node-dev": "^2.0.0", "uuid": "^8.3.2" } diff --git a/src/models/project.ts b/src/models/project.ts index c22bd54c..256cfdd7 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,36 @@ 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 of the pattern to be removed + */ + id: string; +} + /** * Project model to work with project data */ @@ -208,6 +188,11 @@ export default class ProjectModel extends AbstractModel impleme */ public notifications!: ProjectNotificationsRuleDBScheme[]; + /** + * Project events grouping pattern list + */ + public eventGroupingPatterns!: ProjectEventGroupingPatternsDBScheme[]; + /** * Model's collection */ @@ -282,6 +267,93 @@ 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, + }; + + await this.collection.updateOne({ + _id: this._id, + }, + { + $push: { + eventGroupingPatterns: { + $each: [ pattern ], + $position: 0, + }, + }, + }); + + 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 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 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..aa3d372f --- /dev/null +++ b/src/resolvers/projectPatterns.ts @@ -0,0 +1,153 @@ +import { ResolverContextWithUser } from '../types/graphql'; +import { ApolloError } from 'apollo-server-express'; +import { ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; +import safe from 'safe-regex'; + +/** + * 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 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 { + /* eslint-disable-next-line no-new */ + new RegExp(newEventGroupingPattern); + + /** + * Check if pattern is safe RegExp + */ + if (!safe(newEventGroupingPattern)) { + throw new ApolloError('Invalid regular expression pattern'); + } + } catch (error) { + throw new ApolloError('Invalid regular expression pattern'); + } +} + +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'); + } + + validateNewEventGroupingPattern(input.pattern); + + return 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'); + } + + validateNewEventGroupingPattern(input.pattern); + + return 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 project.removeProjectEventGroupingPattern({ id: input.id }); + }, + }, +}; \ No newline at end of file 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 2bac1daa..f0fab0e2 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -151,6 +151,11 @@ type Project { Project notification settings """ notifications: [ProjectNotificationsRule] + + """ + Event grouping patterns + """ + eventGroupingPatterns: [ProjectEventGroupingPattern] } extend type Query { @@ -233,4 +238,12 @@ extend type Mutation { projectId: ID! ): DateTime! @requireUserInWorkspace } + +input EventsFilter { + """ + Search query string. Only alphanumeric characters, spaces, and some special characters are allowed. + Max length: 100 characters + """ + search: String +} `; 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 + } +`; diff --git a/yarn.lock b/yarn.lock index ec798ed3..b7f1989a 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" @@ -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"