diff --git a/package.json b/package.json index 3971f7e3..7c782dea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.11", + "version": "1.1.12", "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.23", + "@hawk.so/types": "^0.1.26", "@types/amqp-connection-manager": "^2.0.4", "@types/bson": "^4.0.5", "@types/debug": "^4.1.5", diff --git a/src/models/project.ts b/src/models/project.ts index 98fb71e9..c22bd54c 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -24,7 +24,7 @@ export interface ProjectNotificationsRuleDBScheme { uidAdded: ObjectId; /** - * Receive type: 'ALL' or 'ONLY_NEW' + * Receive type: 'SEEN_MORE' or 'ONLY_NEW' */ whatToReceive: ReceiveTypes; @@ -42,6 +42,16 @@ export interface ProjectNotificationsRuleDBScheme { * 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; } /** @@ -51,7 +61,7 @@ export enum ReceiveTypes { /** * All notifications */ - ALL = 'ALL', + SEEN_MORE = 'SEEN_MORE', /** * Only first occurrence @@ -69,7 +79,7 @@ export interface CreateProjectNotificationsRulePayload { isEnabled: true; /** - * Receive type: 'ALL' or 'ONLY_NEW' + * Receive type: 'SEEN_MORE' or 'ONLY_NEW' */ whatToReceive: ReceiveTypes; @@ -92,6 +102,16 @@ export interface CreateProjectNotificationsRulePayload { * 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; } /** @@ -127,6 +147,16 @@ interface UpdateProjectNotificationsRulePayload { * 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; } /** @@ -232,6 +262,11 @@ export default class ProjectModel extends AbstractModel impleme excluding: payload.excluding, }; + if (rule.whatToReceive === ReceiveTypes.SEEN_MORE) { + rule.threshold = payload.threshold; + rule.thresholdPeriod = payload.thresholdPeriod; + } + await this.collection.updateOne({ _id: this._id, }, @@ -261,6 +296,11 @@ export default class ProjectModel extends AbstractModel impleme excluding: payload.excluding, }; + if (rule.whatToReceive === ReceiveTypes.SEEN_MORE) { + rule.threshold = payload.threshold; + rule.thresholdPeriod = payload.thresholdPeriod; + } + const result = await this.collection.findOneAndUpdate( { _id: this._id, diff --git a/src/resolvers/projectNotifications.ts b/src/resolvers/projectNotifications.ts index dcdcf673..8755b576 100644 --- a/src/resolvers/projectNotifications.ts +++ b/src/resolvers/projectNotifications.ts @@ -39,6 +39,16 @@ interface CreateProjectNotificationsRuleMutationPayload { * Available channels to receive */ channels: NotificationsChannelsDBScheme; + + /** + * Threshold to receive notification + */ + threshold: number; + + /** + * Period to receive notification + */ + thresholdPeriod: number; } /** @@ -67,16 +77,50 @@ interface ProjectNotificationsRulePointer { } /** - * Return true if all passed channels are empty - * @param channels - project notifications channels + * Returns true is threshold and threshold period are valid + * @param threshold - threshold of the notification rule to be checked + * @param thresholdPeriod - threshold period of the notification rule to be checked */ -function isChannelsEmpty(channels: NotificationsChannelsDBScheme): boolean { - const notEmptyChannels = Object.entries(channels) - .filter(([_, channel]) => { - return (channel as NotificationsChannelSettingsDBScheme).endpoint.replace(/\s+/, '').trim().length !== 0; - }); +function validateNotificationsRuleTresholdAndPeriod( + threshold: ProjectNotificationsRuleDBScheme['threshold'], + thresholdPeriod: ProjectNotificationsRuleDBScheme['thresholdPeriod'] +): string | null { + const validThresholdPeriods = [60_000, 3_600_000, 86_400_000, 604_800_000]; + + if (thresholdPeriod === undefined || !validThresholdPeriods.includes(thresholdPeriod)) { + return 'Threshold period should be one of the following: 60000, 3600000, 86400000, 604800000'; + } + + if (threshold === undefined || threshold < 1) { + return 'Threshold should be greater than 0'; + } + + return null; +} - return notEmptyChannels.length === 0; +/** + * Return true if all passed channels are filled with correct endpoints + */ +function validateNotificationsRuleChannels(channels: NotificationsChannelsDBScheme): string | null { + if (channels.email!.isEnabled) { + if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(channels.email!.endpoint)) { + return 'Invalid email endpoint passed'; + } + } + + if (channels.slack!.isEnabled) { + if (!/^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9]+\/[A-Za-z0-9]+\/[A-Za-z0-9]+$/.test(channels.slack!.endpoint)) { + return 'Invalid slack endpoint passed'; + } + } + + if (channels.telegram!.isEnabled) { + if (!/^https:\/\/notify\.bot\.codex\.so\/u\/[A-Za-z0-9]+$/.test(channels.telegram!.endpoint)) { + return 'Invalid telegram endpoint passed'; + } + } + + return null; } /** @@ -102,8 +146,18 @@ export default { throw new ApolloError('No project with such id'); } - if (isChannelsEmpty(input.channels)) { - throw new UserInputError('At least one channel is required'); + const channelsValidationResult = validateNotificationsRuleChannels(input.channels); + + if (channelsValidationResult !== null) { + throw new UserInputError(channelsValidationResult); + } + + if (input.whatToReceive === ReceiveTypes.SEEN_MORE) { + const thresholdValidationResult = validateNotificationsRuleTresholdAndPeriod(input.threshold, input.thresholdPeriod); + + if (thresholdValidationResult !== null) { + throw new UserInputError(thresholdValidationResult); + } } return project.createNotificationsRule({ @@ -130,8 +184,18 @@ export default { throw new ApolloError('No project with such id'); } - if (isChannelsEmpty(input.channels)) { - throw new UserInputError('At least one channel is required'); + const channelsValidationResult = validateNotificationsRuleChannels(input.channels); + + if (channelsValidationResult !== null) { + throw new UserInputError(channelsValidationResult); + } + + if (input.whatToReceive === ReceiveTypes.SEEN_MORE) { + const thresholdValidationResult = validateNotificationsRuleTresholdAndPeriod(input.threshold, input.thresholdPeriod); + + if (thresholdValidationResult !== null) { + throw new UserInputError(thresholdValidationResult); + } } return project.updateNotificationsRule(input); diff --git a/src/typeDefs/projectNotifications.ts b/src/typeDefs/projectNotifications.ts index 430b3f2a..76909b14 100644 --- a/src/typeDefs/projectNotifications.ts +++ b/src/typeDefs/projectNotifications.ts @@ -11,9 +11,9 @@ export default gql` ONLY_NEW """ - Receive all events + Receive all events that reached threshold in period """ - ALL + SEEN_MORE } """ @@ -49,5 +49,15 @@ export default gql` Notification channels to recieve events """ channels: NotificationsChannels + + """ + Threshold to receive notification + """ + threshold: Int + + """ + Period to receive notification + """ + thresholdPeriod: Int } `; diff --git a/src/typeDefs/projectNotificationsMutations.ts b/src/typeDefs/projectNotificationsMutations.ts index e01b7291..61efca08 100644 --- a/src/typeDefs/projectNotificationsMutations.ts +++ b/src/typeDefs/projectNotificationsMutations.ts @@ -34,6 +34,16 @@ export default gql` Notification channels to recieve events """ channels: NotificationsChannelsInput! + + """ + Threshold to receive notification + """ + threshold: Int + + """ + Period to receive notification + """ + thresholdPeriod: Int } """ @@ -74,6 +84,16 @@ export default gql` Notification channels to recieve events """ channels: NotificationsChannelsInput! + + """ + Threshold to receive notification + """ + threshold: Int + + """ + Period to receive notification + """ + thresholdPeriod: Int } """ diff --git a/yarn.lock b/yarn.lock index ab3a23f7..ec798ed3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -451,13 +451,6 @@ axios "^0.21.1" stack-trace "^0.0.10" -"@hawk.so/types@0.1.23": - version "0.1.23" - resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.23.tgz#45dae057fd29d4735a51baa5f00d8e0d245075f4" - integrity sha512-b9W8TZJj6kBh3rVS4tKCmVbM44XJ/Ya8kwXY12QNf5/U4O2iuegIIEcFwr6N10SJI1VbuPLYFrckxt/8ymQScw== - dependencies: - "@types/mongodb" "^3.5.34" - "@hawk.so/types@^0.1.15": version "0.1.18" resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.18.tgz#746537634756825f066182737429d11ea124d5c5" @@ -465,6 +458,13 @@ 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== + dependencies: + "@types/mongodb" "^3.5.34" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"