From 4928357e8a16d87cf76d27db0c424d139770894d Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sat, 23 Jul 2022 21:03:34 +0700 Subject: [PATCH 01/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/couchdb-datasources/changes.ts | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/couchdb-datasources/changes.ts diff --git a/src/couchdb-datasources/changes.ts b/src/couchdb-datasources/changes.ts new file mode 100644 index 0000000..925a485 --- /dev/null +++ b/src/couchdb-datasources/changes.ts @@ -0,0 +1,32 @@ +import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb'; +import { Injectable, Logger } from '@nestjs/common'; +import nano from 'nano'; + +@Injectable() +export class Changes { + private static logger = new Logger(Changes.name); + private static changesDb = null; + private static initilized = false; + + // TODO: Указать полные типы данных + + static async getDatasource(): Promise> { + if (Changes.initilized) { + return Changes.changesDb; + } + Changes.initilized = true; + const n = CouchDb.getCouchDb(); + const changesDbName = ''; // TODO: Загрузить из конфига + const dbs = await n.db.list(); + if (!dbs.includes(changesDbName)) { + await n.db.create(changesDbName); + } + Changes.changesDb = await n.db.use(changesDbName); + Changes.logger.log(`Connected to changes db - ${changesDbName}`); + return Changes.changesDb; + } + + async getDatasource(): Promise> { + return await Changes.getDatasource(); + } +} From a49f89bf97956abcca5a57e7b17720afd42db03c Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sun, 24 Jul 2022 06:44:20 +0700 Subject: [PATCH 02/16] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D1=87=D0=B8=D0=BA=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/main-config.jsonc.dist | 7 +++++++ src/configs/app.ts | 36 ++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 configs/main-config.jsonc.dist diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist new file mode 100644 index 0000000..90c7a6b --- /dev/null +++ b/configs/main-config.jsonc.dist @@ -0,0 +1,7 @@ +{ + "couchDb": { + "dbs": { + "changes": "" + } + } +} \ No newline at end of file diff --git a/src/configs/app.ts b/src/configs/app.ts index 64e2c38..03017c7 100644 --- a/src/configs/app.ts +++ b/src/configs/app.ts @@ -1,23 +1,29 @@ import RedmineIssueEventEmitterConfigLoader from '@app/event-emitter/configs/main-config'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { parse } from 'jsonc-parser'; const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); -const appConfig = { - couchDbUrl: - process.env['ELTEX_REDMINE_HELPER_COUCHDB_URL'] || 'http://localhost:5984', - - dbs: { - issues: - process.env['ELTEX_REDMINE_HELPER_COUCHDB_ISSUES_DB_NAME'] || - 'redmine_issues', - users: - process.env['ELTEX_REDMINE_HELPER_COUCHDB_USERS_DB_NAME'] || - 'redmine_users', - }, - - redmineIssueEventEmitterConfig: redmineIssueEventEmitterConfig, -}; +let appConfig; export default () => { + if (appConfig) { + return appConfig; + } + + const userDefinedConfigPath = process.env['ELTEX_REDMINE_HELPER_CONFIG_PATH']; + const defaultConfigPath = join('configs', 'main-config.jsonc'); + const configPath = userDefinedConfigPath || defaultConfigPath; + + const rawData = readFileSync(configPath, { encoding: 'utf-8' }); + + const data = parse(rawData); + + appConfig = { + ...data, + redmineIssueEventEmitterConfig: redmineIssueEventEmitterConfig, + }; + return appConfig; }; From de05a86f590fcfaa1e896e730251201b6e4f313f Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sun, 24 Jul 2022 06:44:46 +0700 Subject: [PATCH 03/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=B9=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/configs/app.ts | 5 +++-- src/models/app-config.model.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/models/app-config.model.ts diff --git a/src/configs/app.ts b/src/configs/app.ts index 03017c7..ca49fd1 100644 --- a/src/configs/app.ts +++ b/src/configs/app.ts @@ -2,12 +2,13 @@ import RedmineIssueEventEmitterConfigLoader from '@app/event-emitter/configs/mai import { readFileSync } from 'fs'; import { join } from 'path'; import { parse } from 'jsonc-parser'; +import { AppConfig } from 'src/models/app-config.model'; const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); -let appConfig; +let appConfig: AppConfig; -export default () => { +export default (): AppConfig => { if (appConfig) { return appConfig; } diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts new file mode 100644 index 0000000..e0a9fb1 --- /dev/null +++ b/src/models/app-config.model.ts @@ -0,0 +1,10 @@ +import { MainConfigModel } from '@app/event-emitter/models/main-config-model'; + +export type AppConfig = { + redmineIssueEventEmitterConfig: MainConfigModel; + couchDb: { + dbs: { + changes: string; + }; + }; +}; From 5d4fd808f351428d9aa2dbaeb9b3a0738c3ca2ce Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sun, 24 Jul 2022 06:45:15 +0700 Subject: [PATCH 04/16] =?UTF-8?q?=D0=98=D0=BC=D1=8F=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/couchdb-datasources/changes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/couchdb-datasources/changes.ts b/src/couchdb-datasources/changes.ts index 925a485..5e742d5 100644 --- a/src/couchdb-datasources/changes.ts +++ b/src/couchdb-datasources/changes.ts @@ -1,6 +1,9 @@ import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb'; import { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; +import configuration from '../configs/app'; + +const config = configuration(); @Injectable() export class Changes { @@ -16,7 +19,7 @@ export class Changes { } Changes.initilized = true; const n = CouchDb.getCouchDb(); - const changesDbName = ''; // TODO: Загрузить из конфига + const changesDbName = config.couchDb.dbs.changes; const dbs = await n.db.list(); if (!dbs.includes(changesDbName)) { await n.db.create(changesDbName); From 8d5cf6ac4428138b62feb1ef6603e7c4c3dba0ff Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sun, 24 Jul 2022 11:47:55 +0700 Subject: [PATCH 05/16] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D1=81=D1=8B=D0=BB=D0=B0=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/change-message.model.ts | 7 +++++++ src/models/change.model.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/models/change-message.model.ts create mode 100644 src/models/change.model.ts diff --git a/src/models/change-message.model.ts b/src/models/change-message.model.ts new file mode 100644 index 0000000..ec1f881 --- /dev/null +++ b/src/models/change-message.model.ts @@ -0,0 +1,7 @@ +import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; + +export type ChangeMessage = { + changes_message?: string | null; + notification_message?: string | null; + recipient?: RedmineTypes.PublicUser | null; +}; diff --git a/src/models/change.model.ts b/src/models/change.model.ts new file mode 100644 index 0000000..affdbba --- /dev/null +++ b/src/models/change.model.ts @@ -0,0 +1,27 @@ +import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; +import { ChangeMessage } from './change-message.model'; + +export class Change { + initiator?: RedmineTypes.PublicUser; + dev?: RedmineTypes.PublicUser; + cr?: RedmineTypes.PublicUser; + qa?: RedmineTypes.PublicUser; + current_user?: RedmineTypes.PublicUser; + author?: RedmineTypes.PublicUser; + issue_id: number; + issue_url: string; + issue_tracker: string; + issue_subject: string; + journal_note?: string; + old_status?: { + id?: number; + name?: string; + } | null; + new_status?: { + id?: number; + name?: string; + }; + created_on: string; + created_on_timestamp: number | null; + messages: ChangeMessage[]; +} From 8c7d9f3924aca9ad9f29c6cfbb95d44f16d3066c Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sun, 24 Jul 2022 15:47:03 +0700 Subject: [PATCH 06/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + configs/redmine-statuses-config.jsonc.dist | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 configs/redmine-statuses-config.jsonc.dist diff --git a/.gitignore b/.gitignore index e637fba..fc2026f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ lerna-debug.log* configs/main-config.jsonc configs/issue-event-emitter-config.jsonc tmp/* +configs/redmine-statuses-config.jsonc diff --git a/configs/redmine-statuses-config.jsonc.dist b/configs/redmine-statuses-config.jsonc.dist new file mode 100644 index 0000000..8c067f5 --- /dev/null +++ b/configs/redmine-statuses-config.jsonc.dist @@ -0,0 +1,18 @@ +{ + "redmine_statuses": [ + { + "id": 1, + "name": "New" + }, + { + "id": 2, + "name": "In Progress" + }, + { + "id": 3, + "name": "Closed", + "is_closed": true + } + // ... + ] +} \ No newline at end of file From a7284aa1284936550afc339f8b4aa7d3f1f30ead Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 01:42:08 +0700 Subject: [PATCH 07/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D1=87=D0=B8?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=81=D0=BE=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/redmine-statuses-config.jsonc.dist | 34 ++++++++++------------ src/configs/app.ts | 3 ++ src/configs/statuses.config.ts | 23 +++++++++++++++ src/models/app-config.model.ts | 2 ++ src/models/statuses-config.model.ts | 5 ++++ 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 src/configs/statuses.config.ts create mode 100644 src/models/statuses-config.model.ts diff --git a/configs/redmine-statuses-config.jsonc.dist b/configs/redmine-statuses-config.jsonc.dist index 8c067f5..a8804fc 100644 --- a/configs/redmine-statuses-config.jsonc.dist +++ b/configs/redmine-statuses-config.jsonc.dist @@ -1,18 +1,16 @@ -{ - "redmine_statuses": [ - { - "id": 1, - "name": "New" - }, - { - "id": 2, - "name": "In Progress" - }, - { - "id": 3, - "name": "Closed", - "is_closed": true - } - // ... - ] -} \ No newline at end of file +[ + { + "id": 1, + "name": "New" + }, + { + "id": 2, + "name": "In Progress" + }, + { + "id": 3, + "name": "Closed", + "is_closed": true + } + // ... +] \ No newline at end of file diff --git a/src/configs/app.ts b/src/configs/app.ts index ca49fd1..ae976fb 100644 --- a/src/configs/app.ts +++ b/src/configs/app.ts @@ -3,8 +3,10 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { parse } from 'jsonc-parser'; import { AppConfig } from 'src/models/app-config.model'; +import RedmineStatusesConfigLoader from './statuses.config'; const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); +const redmineStatusesConfig = RedmineStatusesConfigLoader(); let appConfig: AppConfig; @@ -23,6 +25,7 @@ export default (): AppConfig => { appConfig = { ...data, + redmineStatuses: redmineStatusesConfig, redmineIssueEventEmitterConfig: redmineIssueEventEmitterConfig, }; diff --git a/src/configs/statuses.config.ts b/src/configs/statuses.config.ts new file mode 100644 index 0000000..b01d7d9 --- /dev/null +++ b/src/configs/statuses.config.ts @@ -0,0 +1,23 @@ +import { StatusesConfig } from 'src/models/statuses-config.model'; +import { parse } from 'jsonc-parser'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +let redmineStatues: StatusesConfig; + +export default (): StatusesConfig => { + if (redmineStatues) { + return redmineStatues; + } + + const userDefinedConfigPath = + process.env['ELTEX_REDMINE_HELPER_STATUSES_CONFIG_PATH']; + const defaultConfigPath = join('configs', 'redmine-statuses-config.jsonc'); + const configPath = userDefinedConfigPath || defaultConfigPath; + + const rawData = readFileSync(configPath, { encoding: 'utf-8' }); + + redmineStatues = parse(rawData); + + return redmineStatues; +}; diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index e0a9fb1..f5d9aba 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -1,7 +1,9 @@ import { MainConfigModel } from '@app/event-emitter/models/main-config-model'; +import { StatusesConfig } from './statuses-config.model'; export type AppConfig = { redmineIssueEventEmitterConfig: MainConfigModel; + redmineStatuses: StatusesConfig; couchDb: { dbs: { changes: string; diff --git a/src/models/statuses-config.model.ts b/src/models/statuses-config.model.ts new file mode 100644 index 0000000..9366352 --- /dev/null +++ b/src/models/statuses-config.model.ts @@ -0,0 +1,5 @@ +export type StatusesConfig = { + id: number; + name: string; + is_closed?: boolean; +}[]; From 17146f2f0589fa0fd132b7d6d8072c6bce0e008e Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 05:32:11 +0700 Subject: [PATCH 08/16] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B2=20namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/configs/statuses.config.ts | 4 ++-- src/models/app-config.model.ts | 2 +- src/models/statuses-config.model.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/configs/statuses.config.ts b/src/configs/statuses.config.ts index b01d7d9..e3eb714 100644 --- a/src/configs/statuses.config.ts +++ b/src/configs/statuses.config.ts @@ -3,9 +3,9 @@ import { parse } from 'jsonc-parser'; import { join } from 'path'; import { readFileSync } from 'fs'; -let redmineStatues: StatusesConfig; +let redmineStatues: StatusesConfig.Config; -export default (): StatusesConfig => { +export default (): StatusesConfig.Config => { if (redmineStatues) { return redmineStatues; } diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index f5d9aba..4662d3a 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -3,7 +3,7 @@ import { StatusesConfig } from './statuses-config.model'; export type AppConfig = { redmineIssueEventEmitterConfig: MainConfigModel; - redmineStatuses: StatusesConfig; + redmineStatuses: StatusesConfig.Config; couchDb: { dbs: { changes: string; diff --git a/src/models/statuses-config.model.ts b/src/models/statuses-config.model.ts index 9366352..2b90a0c 100644 --- a/src/models/statuses-config.model.ts +++ b/src/models/statuses-config.model.ts @@ -1,5 +1,10 @@ -export type StatusesConfig = { - id: number; - name: string; - is_closed?: boolean; -}[]; +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace StatusesConfig { + export type Item = { + id: number; + name: string; + is_closed?: boolean; + }; + + export type Config = Item[]; +} From 91740429714f212244c8cc65643360cb39fb4a97 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 06:43:22 +0700 Subject: [PATCH 09/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=B8=D1=86=D0=B5=D0=BD=D0=B7=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=B0=20redmine-issue-event-emitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/event-emitter/LICENSE | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 libs/event-emitter/LICENSE diff --git a/libs/event-emitter/LICENSE b/libs/event-emitter/LICENSE new file mode 100644 index 0000000..c0a1501 --- /dev/null +++ b/libs/event-emitter/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 Gnedov Pavel + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From 1a65ab7fcad80ffd7c29dea3163edca9dbd5100b Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 06:44:48 +0700 Subject: [PATCH 10/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0?= =?UTF-8?q?=20status-changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../redmine-status-changes-config.jsonc.dist | 17 ++++++++++++ src/configs/app.ts | 3 +++ src/configs/status-changes.config.ts | 26 +++++++++++++++++++ src/models/app-config.model.ts | 2 ++ src/models/status-changes-config.model.ts | 17 ++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 configs/redmine-status-changes-config.jsonc.dist create mode 100644 src/configs/status-changes.config.ts create mode 100644 src/models/status-changes-config.model.ts diff --git a/.gitignore b/.gitignore index fc2026f..2148fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ configs/main-config.jsonc configs/issue-event-emitter-config.jsonc tmp/* configs/redmine-statuses-config.jsonc +configs/redmine-status-changes-config.jsonc diff --git a/configs/redmine-status-changes-config.jsonc.dist b/configs/redmine-status-changes-config.jsonc.dist new file mode 100644 index 0000000..d79d57f --- /dev/null +++ b/configs/redmine-status-changes-config.jsonc.dist @@ -0,0 +1,17 @@ +[ + { + "default": false, + "from": "New", + "to": "In Progress", + "messages": [ + { + "recipient": "", + // Handlebars - template engine + "changes_message": "{{qa.name}} got issue #{{issue_id}} after development {{dev.name}}", + "notification_message": "{{ issue_tracker }} #{{ issue_id }} {{ issue_subject }}:\n{{dev.name}} finished development. You can test issue.\n\n{{journal_note}}" + } + // ... + ] + } + // ... +] \ No newline at end of file diff --git a/src/configs/app.ts b/src/configs/app.ts index ae976fb..1177c8b 100644 --- a/src/configs/app.ts +++ b/src/configs/app.ts @@ -4,9 +4,11 @@ import { join } from 'path'; import { parse } from 'jsonc-parser'; import { AppConfig } from 'src/models/app-config.model'; import RedmineStatusesConfigLoader from './statuses.config'; +import RedmineStatusChangesConfigLoader from './status-changes.config'; const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); const redmineStatusesConfig = RedmineStatusesConfigLoader(); +const redmineStatusChanges = RedmineStatusChangesConfigLoader(); let appConfig: AppConfig; @@ -27,6 +29,7 @@ export default (): AppConfig => { ...data, redmineStatuses: redmineStatusesConfig, redmineIssueEventEmitterConfig: redmineIssueEventEmitterConfig, + redmineStatusChanges: redmineStatusChanges, }; return appConfig; diff --git a/src/configs/status-changes.config.ts b/src/configs/status-changes.config.ts new file mode 100644 index 0000000..1035e6b --- /dev/null +++ b/src/configs/status-changes.config.ts @@ -0,0 +1,26 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { StatusChangesConfig } from 'src/models/status-changes-config.model'; +import { parse } from 'jsonc-parser'; + +let statusChanges: StatusChangesConfig.Config; + +export default (): StatusChangesConfig.Config => { + if (statusChanges) { + return statusChanges; + } + + const userDefinedConfigPath = + process.env['ELTEX_REDMINE_HELPER_STATUS_CHANGES_CONFIG_PATH']; + const defaultConfigPath = join( + 'configs', + 'redmine-status-changes-config.jsonc', + ); + const configPath = userDefinedConfigPath || defaultConfigPath; + + const rawData = readFileSync(configPath, { encoding: 'utf-8' }); + + statusChanges = parse(rawData); + + return statusChanges; +}; diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index 4662d3a..de21ac0 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -1,9 +1,11 @@ import { MainConfigModel } from '@app/event-emitter/models/main-config-model'; +import { StatusChangesConfig } from './status-changes-config.model'; import { StatusesConfig } from './statuses-config.model'; export type AppConfig = { redmineIssueEventEmitterConfig: MainConfigModel; redmineStatuses: StatusesConfig.Config; + redmineStatusChanges: StatusChangesConfig.Config; couchDb: { dbs: { changes: string; diff --git a/src/models/status-changes-config.model.ts b/src/models/status-changes-config.model.ts new file mode 100644 index 0000000..43369f8 --- /dev/null +++ b/src/models/status-changes-config.model.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace StatusChangesConfig { + export type Message = { + recipient: string; + changes_message: string; + notification_message: string; + }; + + export type Item = { + default: boolean; + from: string; + to: string; + messages: Message[]; + }; + + export type Config = Item[]; +} From d833c68455844ae2aac0b3da7800e018f6f6d6cb Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 11:36:06 +0700 Subject: [PATCH 11/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=8B=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=20=D0=B8=20url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/utils/timestamp-converter.ts | 9 +++++++++ src/converters/redmine-public-url.converter.ts | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 libs/event-emitter/src/utils/timestamp-converter.ts create mode 100644 src/converters/redmine-public-url.converter.ts diff --git a/libs/event-emitter/src/utils/timestamp-converter.ts b/libs/event-emitter/src/utils/timestamp-converter.ts new file mode 100644 index 0000000..5a10f3d --- /dev/null +++ b/libs/event-emitter/src/utils/timestamp-converter.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace TimestampConverter { + export function toTimestamp(date: string): number { + return new Date(date).getTime(); + } + export function toString(date: number): string { + return new Date(date).toISOString(); + } +} diff --git a/src/converters/redmine-public-url.converter.ts b/src/converters/redmine-public-url.converter.ts new file mode 100644 index 0000000..1abf0b1 --- /dev/null +++ b/src/converters/redmine-public-url.converter.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class RedminePublicUrlConverter { + private redminePublicUrlPrefix: string; + + constructor(private configService: ConfigService) { + this.redminePublicUrlPrefix = this.configService.get( + 'redmineIssueEventEmitterConfig.redmineUrlPublic', + ); + } + + convert(issueId: number | string): string { + return `${this.redminePublicUrlPrefix}/issues/${issueId}`; + } +} From e60f29a3318580a019ca3dd232a319c5f9566af7 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 11:40:50 +0700 Subject: [PATCH 12/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=20handlebars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 69 ++++++++++++++++++++++++++++++++++++++++++----- package.json | 1 + 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5b4a29..cc76f76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/serve-static": "^2.2.2", "@nestjs/websockets": "^8.4.4", "axios": "^0.27.2", + "handlebars": "^4.7.7", "imap-simple": "^5.1.0", "nano": "^10.0.0", "reflect-metadata": "^0.1.13", @@ -4372,6 +4373,26 @@ "dev": true, "license": "ISC" }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "license": "MIT", @@ -5987,7 +6008,6 @@ }, "node_modules/neo-async": { "version": "2.6.2", - "dev": true, "license": "MIT" }, "node_modules/node-emoji": { @@ -6984,7 +7004,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7624,6 +7643,18 @@ "node": ">=4.2.0" } }, + "node_modules/uglify-js": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.3.tgz", + "integrity": "sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/universalify": { "version": "2.0.0", "dev": true, @@ -7946,6 +7977,11 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "dev": true, @@ -10922,6 +10958,18 @@ "version": "4.2.10", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has": { "version": "1.0.3", "requires": { @@ -12013,8 +12061,7 @@ "version": "0.6.3" }, "neo-async": { - "version": "2.6.2", - "dev": true + "version": "2.6.2" }, "node-emoji": { "version": "1.11.0", @@ -12610,8 +12657,7 @@ } }, "source-map": { - "version": "0.6.1", - "dev": true + "version": "0.6.1" }, "source-map-support": { "version": "0.5.21", @@ -12989,6 +13035,12 @@ "version": "4.6.3", "dev": true }, + "uglify-js": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.3.tgz", + "integrity": "sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==", + "optional": true + }, "universalify": { "version": "2.0.0", "dev": true @@ -13199,6 +13251,11 @@ "version": "1.2.3", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "wrap-ansi": { "version": "7.0.0", "dev": true, diff --git a/package.json b/package.json index a61e0d0..e5cf9db 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@nestjs/serve-static": "^2.2.2", "@nestjs/websockets": "^8.4.4", "axios": "^0.27.2", + "handlebars": "^4.7.7", "imap-simple": "^5.1.0", "nano": "^10.0.0", "reflect-metadata": "^0.1.13", From 030f7c81012a3703a6d0e28d5e4bd07d15ca19c5 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 25 Jul 2022 11:41:09 +0700 Subject: [PATCH 13/16] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/issue-enhancers/current-user-enhancer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/issue-enhancers/current-user-enhancer.ts b/src/issue-enhancers/current-user-enhancer.ts index 2b4a15b..e48e2e0 100644 --- a/src/issue-enhancers/current-user-enhancer.ts +++ b/src/issue-enhancers/current-user-enhancer.ts @@ -6,6 +6,8 @@ import { Injectable } from '@nestjs/common'; export class CurrentUserEnhancer implements IssueEnhancerInterface { name = 'current-user'; + // TODO: Переместить правила в конфиг + private rules = { New: 'dev', 'In Progress': 'dev', From 8dc886de49e8474190ad74cfa710854b5a61b449 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Wed, 27 Jul 2022 11:42:45 +0700 Subject: [PATCH 14/16] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 6 + src/models/change-message.model.ts | 2 +- .../status-change-notifications.service.ts | 180 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/notifications/status-change-notifications.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6469bd1..02f2906 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,9 +8,12 @@ import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import configuration from './configs/app'; +import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter'; +import { Changes } from './couchdb-datasources/changes'; import { CurrentUserEnhancer } from './issue-enhancers/current-user-enhancer'; import { CustomFieldsEnhancer } from './issue-enhancers/custom-fields-enhancer'; import { PersonalNotificationsService } from './notifications/personal-notifications.service'; +import { StatusChangeNotificationsService } from './notifications/status-change-notifications.service'; @Module({ imports: [ @@ -25,6 +28,9 @@ import { PersonalNotificationsService } from './notifications/personal-notificat CustomFieldsEnhancer, CurrentUserEnhancer, PersonalNotificationsService, + StatusChangeNotificationsService, + Changes, + RedminePublicUrlConverter, ], }) export class AppModule implements OnModuleInit { diff --git a/src/models/change-message.model.ts b/src/models/change-message.model.ts index ec1f881..b3c890e 100644 --- a/src/models/change-message.model.ts +++ b/src/models/change-message.model.ts @@ -1,7 +1,7 @@ import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; export type ChangeMessage = { - changes_message?: string | null; + change_message?: string | null; notification_message?: string | null; recipient?: RedmineTypes.PublicUser | null; }; diff --git a/src/notifications/status-change-notifications.service.ts b/src/notifications/status-change-notifications.service.ts new file mode 100644 index 0000000..51d517f --- /dev/null +++ b/src/notifications/status-change-notifications.service.ts @@ -0,0 +1,180 @@ +import { RedmineTypes } from '@app/event-emitter/models/redmine-types'; +import { SaveResponse } from '@app/event-emitter/models/save-response'; +import { UsersService } from '@app/event-emitter/users/users.service'; +import { TimestampConverter } from '@app/event-emitter/utils/timestamp-converter'; +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { RedminePublicUrlConverter } from 'src/converters/redmine-public-url.converter'; +import { Change } from 'src/models/change.model'; +import { StatusChangesConfig } from 'src/models/status-changes-config.model'; +import { StatusesConfig } from 'src/models/statuses-config.model'; +import Handlebars from 'handlebars'; +import { ChangeMessage } from 'src/models/change-message.model'; + +@Injectable() +export class StatusChangeNotificationsService { + private logger = new Logger(StatusChangeNotificationsService.name); + private statuses: StatusesConfig.Config; + private statusChanges: StatusChangesConfig.Config; + + constructor( + private usersService: UsersService, + private config: ConfigService, + private redminePublicUrlConverter: RedminePublicUrlConverter, + ) { + this.statuses = this.config.get('redmineStatuses'); + this.statusChanges = this.config.get( + 'redmineStatusChanges', + ); + this.logger.debug( + `StatusChangeNotificationsService created, ` + + `statuses = ${JSON.stringify(this.statuses.map((s) => s.name))}, ` + + `statusChanges.length = ${this.statusChanges.length}`, + ); + } + + async getChanges(saveResponse: SaveResponse): Promise { + this.logger.debug( + `Analize change statuses for issue #${saveResponse.current.id} ` + + `(${saveResponse.current.subject}) for journalsDiff.length = ${saveResponse.journalsDiff.length} start`, + ); + + const changes: Change[] = []; + + const issue = saveResponse.current; + + for (let i = 0; i < saveResponse.journalsDiff.length; i++) { + const journal = saveResponse.journalsDiff[i]; + const change = await this.getMessagesForChangeStatus(issue, journal); + if (change) { + changes.push(change); + } + } + + this.logger.debug( + `Analize change statuses for issue #${saveResponse.current.id} ` + + `(${saveResponse.current.subject}) for journalsDiff.length = ${saveResponse.journalsDiff.length} finished`, + ); + return changes; + } + + private getStatusChangeDetails( + journal: RedmineTypes.Journal, + ): RedmineTypes.JournalDetail | null { + if (!journal?.details || journal?.details.length <= 0) return null; + const details: RedmineTypes.JournalDetail[] = journal?.details; + return ( + details.find((d) => { + return d.name === 'status_id'; + }) || null + ); + } + + private async getMessagesForChangeStatus( + issue: any, + journal: any, + ): Promise { + const statusChangeDetails = this.getStatusChangeDetails(journal); + if (!statusChangeDetails) return null; + const change: Change = { + initiator: await this.usersService.getUser(journal?.user?.id), + dev: await this.usersService.getUser(issue?.dev?.id), + qa: await this.usersService.getUser(issue?.qa?.id), + cr: await this.usersService.getUser(issue?.cr?.id), + current_user: await this.usersService.getUser(issue?.current_user?.id), + author: await this.usersService.getUser(issue?.author?.id), + old_status: this.findStatusById(statusChangeDetails.old_value), + new_status: this.findStatusById(statusChangeDetails.new_value), + issue_id: issue.id, + issue_url: this.redminePublicUrlConverter.convert(issue.id), + issue_tracker: issue.tracker?.name || '', + issue_subject: issue.subject || '', + created_on: journal.created_on, + created_on_timestamp: TimestampConverter.toTimestamp(journal.created_on), + journal_note: journal.notes || '', + messages: [], + }; + const messages = await this.generateMessages(statusChangeDetails, change); + if (messages) { + change.messages = messages; + } + return change; + } + + private findStatusById(id: number | string): StatusesConfig.Item | null { + if (typeof id === 'string' && !Number.isNaN(id) && Number.isFinite(id)) { + id = Number(id); + } + return this.statuses.find((s) => s.id === id) || null; + } + + private async generateMessages( + detail: RedmineTypes.JournalDetail, + change: Change, + ): Promise { + const oldStatus = this.findStatusById(detail.old_value); + const newStatus = this.findStatusById(detail.new_value); + if (!oldStatus || !newStatus) return null; + const changeParams = this.findChangeParams(oldStatus.name, newStatus.name); + if (!changeParams || !changeParams.messages) return null; + const filledMessages = await Promise.all( + changeParams.messages.map(async (messageParams: any) => { + return await this.generateMessage(messageParams, change); + }), + ); + return filledMessages.filter((m) => m); + } + + private findChangeParams( + oldStatus: string, + newStatus: string, + ): StatusChangesConfig.Item | null { + let foundParam: StatusChangesConfig.Item | null = null; + foundParam = this.statusChanges.find( + (p) => p.from == oldStatus && p.to == newStatus, + ); + if (!foundParam) { + foundParam = this.statusChanges.find((p) => !p.from && p.to == newStatus); + } + if (!foundParam) { + foundParam = this.statusChanges.find((p) => p.from == newStatus && !p.to); + } + if (!foundParam) { + foundParam = this.statusChanges.find((p) => p.default); + } + return foundParam; + } + + private async generateMessage( + messageParams: StatusChangesConfig.Message, + change: Change, + ): Promise { + if (!messageParams) return null; + const recipientUser = await this.usersService.getUser( + change[messageParams.recipient]?.id, + ); + if (!recipientUser) return null; + + let changeMessage = null; + if (messageParams.changes_message) { + const changeMessageTemplate = Handlebars.compile( + messageParams.changes_message, + ); + changeMessage = changeMessageTemplate(change); + } + + let notificationMessage = null; + if (messageParams.notification_message) { + const notificationMessageTemplate = Handlebars.compile( + messageParams.notification_message, + ); + notificationMessage = notificationMessageTemplate(change); + } + + return { + recipient: recipientUser, + change_message: changeMessage, + notification_message: notificationMessage, + }; + } +} From 39fe3c7a7952812e647444ace2d874fe307a63f9 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 8 Aug 2022 13:02:41 +0700 Subject: [PATCH 15/16] =?UTF-8?q?=D0=93=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 44 +++++++++++++ src/models/status-changes-config.model.ts | 1 + .../status-change-notifications.service.ts | 61 +++++++++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 02f2906..ea50e85 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { TimestampEnhancer } from '@app/event-emitter/issue-enhancers/timestamps import { MainController } from '@app/event-emitter/main/main.controller'; import { Logger, Module, OnModuleInit } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { switchMap } from 'rxjs'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import configuration from './configs/app'; @@ -43,6 +44,7 @@ export class AppModule implements OnModuleInit { private timestampEnhancer: TimestampEnhancer, private customFieldsEnhancer: CustomFieldsEnhancer, private currentUserEnhancer: CurrentUserEnhancer, + private statusChangeNotificationsService: StatusChangeNotificationsService, ) {} onModuleInit() { @@ -51,14 +53,56 @@ export class AppModule implements OnModuleInit { this.customFieldsEnhancer, this.currentUserEnhancer, ]); + this.personalNotificationsService.$messages.subscribe((message) => { // eslint-disable-next-line prettier/prettier this.logger.log(`Get personal message ${JSON.stringify(message.message)} for recipients ${JSON.stringify(message.recipients)}`); }); + this.statusChangeNotificationsService.$changes.subscribe((change) => { + this.logger.log( + `Get status changes messages for ` + + `issue_id = ${change.issue_id}, ` + + `messages = ${JSON.stringify( + change.messages.map((m) => m.change_message).filter((m) => !!m), + )}`, + ); + }); + this.redmineIssuesCacheWriterService.subject.subscribe( async (saveResult) => { await this.personalNotificationsService.analize(saveResult); }, ); + + this.redmineIssuesCacheWriterService.subject + .pipe( + switchMap(async (saveResult) => { + this.logger.debug( + `Save result process started, issue_id = ${saveResult.current.id}`, + ); + return saveResult; + }), + ) + .pipe( + switchMap(async (saveResult) => { + this.logger.debug(`personalNotificationsService.analize started`); + await this.personalNotificationsService.analize(saveResult); + this.logger.debug('personalNotificationsService.analize successed'); + return saveResult; + }), + switchMap(async (saveResult) => { + // eslint-disable-next-line prettier/prettier + this.logger.debug(`statusChangeNotificationsService.getChanges started`); + await this.statusChangeNotificationsService.getChanges(saveResult); + // eslint-disable-next-line prettier/prettier + this.logger.debug(`statusChangeNotificationsService.getChanges successed`); + return saveResult; + }), + ) + .subscribe(async (saveResult) => { + this.logger.debug( + `Save result process success finished, issue_id = ${saveResult.current.id}`, + ); + }); } } diff --git a/src/models/status-changes-config.model.ts b/src/models/status-changes-config.model.ts index 43369f8..251833a 100644 --- a/src/models/status-changes-config.model.ts +++ b/src/models/status-changes-config.model.ts @@ -8,6 +8,7 @@ export namespace StatusChangesConfig { export type Item = { default: boolean; + new_issue?: boolean; from: string; to: string; messages: Message[]; diff --git a/src/notifications/status-change-notifications.service.ts b/src/notifications/status-change-notifications.service.ts index 51d517f..23063ea 100644 --- a/src/notifications/status-change-notifications.service.ts +++ b/src/notifications/status-change-notifications.service.ts @@ -10,6 +10,7 @@ import { StatusChangesConfig } from 'src/models/status-changes-config.model'; import { StatusesConfig } from 'src/models/statuses-config.model'; import Handlebars from 'handlebars'; import { ChangeMessage } from 'src/models/change-message.model'; +import { Subject } from 'rxjs'; @Injectable() export class StatusChangeNotificationsService { @@ -17,6 +18,8 @@ export class StatusChangeNotificationsService { private statuses: StatusesConfig.Config; private statusChanges: StatusChangesConfig.Config; + $changes = new Subject(); + constructor( private usersService: UsersService, private config: ConfigService, @@ -29,7 +32,10 @@ export class StatusChangeNotificationsService { this.logger.debug( `StatusChangeNotificationsService created, ` + `statuses = ${JSON.stringify(this.statuses.map((s) => s.name))}, ` + - `statusChanges.length = ${this.statusChanges.length}`, + `statusChanges.length = ${this.statusChanges.length}, ` + + `statusChanges = ` + + // eslint-disable-next-line prettier/prettier + `${JSON.stringify(this.statusChanges.map((c) => `${c.from} -> ${c.to}`))}`, ); } @@ -43,6 +49,11 @@ export class StatusChangeNotificationsService { const issue = saveResponse.current; + if (!saveResponse.prev) { + const newChange = await this.getMessagesForNewIssue(issue); + changes.push(newChange); + } + for (let i = 0; i < saveResponse.journalsDiff.length; i++) { const journal = saveResponse.journalsDiff[i]; const change = await this.getMessagesForChangeStatus(issue, journal); @@ -55,6 +66,9 @@ export class StatusChangeNotificationsService { `Analize change statuses for issue #${saveResponse.current.id} ` + `(${saveResponse.current.subject}) for journalsDiff.length = ${saveResponse.journalsDiff.length} finished`, ); + + changes.forEach((c) => this.$changes.next(c)); + return changes; } @@ -71,8 +85,8 @@ export class StatusChangeNotificationsService { } private async getMessagesForChangeStatus( - issue: any, - journal: any, + issue: RedmineTypes.Issue & Record, + journal: RedmineTypes.Journal, ): Promise { const statusChangeDetails = this.getStatusChangeDetails(journal); if (!statusChangeDetails) return null; @@ -101,8 +115,43 @@ export class StatusChangeNotificationsService { return change; } + private async getMessagesForNewIssue( + issue: RedmineTypes.Issue & Record, + ): Promise { + const changeParams = this.statusChanges.find((p) => p.new_issue); + const change: Change = { + initiator: await this.usersService.getUser(issue.author.id), + dev: await this.usersService.getUser(issue?.dev?.id), + qa: await this.usersService.getUser(issue?.qa?.id), + cr: await this.usersService.getUser(issue?.cr?.id), + current_user: await this.usersService.getUser(issue?.current_user?.id), + author: await this.usersService.getUser(issue?.author?.id), + old_status: this.findStatusById(issue.status.id), + new_status: this.findStatusById(issue.status.id), + issue_id: issue.id, + issue_url: this.redminePublicUrlConverter.convert(issue.id), + issue_tracker: issue.tracker?.name || '', + issue_subject: issue.subject || '', + created_on: issue.created_on, + created_on_timestamp: TimestampConverter.toTimestamp(issue.created_on), + journal_note: '', + messages: [], + }; + const filledMessages = await Promise.all( + changeParams.messages.map(async (messageParams: any) => { + return await this.generateMessage(messageParams, change); + }), + ); + change.messages = filledMessages.filter((m) => Boolean(m)); + return change; + } + private findStatusById(id: number | string): StatusesConfig.Item | null { - if (typeof id === 'string' && !Number.isNaN(id) && Number.isFinite(id)) { + if ( + typeof id === 'string' && + !Number.isNaN(id) && + Number.isFinite(Number(id)) + ) { id = Number(id); } return this.statuses.find((s) => s.id === id) || null; @@ -111,7 +160,7 @@ export class StatusChangeNotificationsService { private async generateMessages( detail: RedmineTypes.JournalDetail, change: Change, - ): Promise { + ): Promise { const oldStatus = this.findStatusById(detail.old_value); const newStatus = this.findStatusById(detail.new_value); if (!oldStatus || !newStatus) return null; @@ -122,7 +171,7 @@ export class StatusChangeNotificationsService { return await this.generateMessage(messageParams, change); }), ); - return filledMessages.filter((m) => m); + return filledMessages.filter((m) => Boolean(m)); } private findChangeParams( From 03dc905439f7508410e8c38c2d9f788ba39ca812 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 9 Aug 2022 00:16:57 +0700 Subject: [PATCH 16/16] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B2=20?= =?UTF-8?q?couchdb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 27 ++++++++-- .../changes-cache-writer.service.ts | 54 +++++++++++++++++++ src/couchdb-datasources/changes.ts | 8 +-- 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/changes-cache-writer/changes-cache-writer.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index ea50e85..9c40454 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,9 @@ import { CurrentUserEnhancer } from './issue-enhancers/current-user-enhancer'; import { CustomFieldsEnhancer } from './issue-enhancers/custom-fields-enhancer'; import { PersonalNotificationsService } from './notifications/personal-notifications.service'; import { StatusChangeNotificationsService } from './notifications/status-change-notifications.service'; +import { ChangesCacheWriterService } from './changes-cache-writer/changes-cache-writer.service'; +import { Issues } from '@app/event-emitter/couchdb-datasources/issues'; +import { Users } from '@app/event-emitter/couchdb-datasources/users'; @Module({ imports: [ @@ -32,6 +35,7 @@ import { StatusChangeNotificationsService } from './notifications/status-change- StatusChangeNotificationsService, Changes, RedminePublicUrlConverter, + ChangesCacheWriterService, ], }) export class AppModule implements OnModuleInit { @@ -45,9 +49,14 @@ export class AppModule implements OnModuleInit { private customFieldsEnhancer: CustomFieldsEnhancer, private currentUserEnhancer: CurrentUserEnhancer, private statusChangeNotificationsService: StatusChangeNotificationsService, + private changesCacheWriterService: ChangesCacheWriterService, ) {} onModuleInit() { + Issues.getDatasource(); + Users.getDatasource(); + Changes.getDatasource(); + this.enhancerService.addEnhancer([ this.timestampEnhancer, this.customFieldsEnhancer, @@ -93,15 +102,25 @@ export class AppModule implements OnModuleInit { switchMap(async (saveResult) => { // eslint-disable-next-line prettier/prettier this.logger.debug(`statusChangeNotificationsService.getChanges started`); - await this.statusChangeNotificationsService.getChanges(saveResult); + const changes = + await this.statusChangeNotificationsService.getChanges(saveResult); // eslint-disable-next-line prettier/prettier this.logger.debug(`statusChangeNotificationsService.getChanges successed`); - return saveResult; + return { changes, saveResult }; + }), + switchMap(async (args) => { + this.logger.debug(`Save changes in couchdb started`); + const promises = args.changes.map((c) => + this.changesCacheWriterService.saveChange(c), + ); + await Promise.all(promises); + this.logger.debug('Save changes in couchdb successed'); + return args; }), ) - .subscribe(async (saveResult) => { + .subscribe(async (args) => { this.logger.debug( - `Save result process success finished, issue_id = ${saveResult.current.id}`, + `Save result process success finished, issue_id = ${args.saveResult.current.id}`, ); }); } diff --git a/src/changes-cache-writer/changes-cache-writer.service.ts b/src/changes-cache-writer/changes-cache-writer.service.ts new file mode 100644 index 0000000..375dad1 --- /dev/null +++ b/src/changes-cache-writer/changes-cache-writer.service.ts @@ -0,0 +1,54 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import nano from 'nano'; +import { Subject } from 'rxjs'; +import { Changes } from 'src/couchdb-datasources/changes'; +import { Change } from 'src/models/change.model'; + +@Injectable() +export class ChangesCacheWriterService { + private logger = new Logger(ChangesCacheWriterService.name); + + subject = new Subject(); + + constructor(private changes: Changes) {} + + async saveChange(change: Change): Promise { + this.logger.debug( + `saveChange started, ` + + `issue_id = ${change.issue_id}, ` + + `initiator.name = ${change.initiator.name}`, + ); + if (!change) { + this.logger.debug(`saveChange successed, no data for saving`); + return; + } + const changesDb = await this.changes.getDatasource(); + if (!changesDb) { + this.logger.error(`saveChange failed, changesDb is undefined or null`); + return; + } + const item: Change & nano.MaybeDocument = { ...change }; + item._id = this.getId(); + if (!item) { + this.logger.debug(`saveChange successed, no data for saving`); + return; + } + try { + await changesDb.insert(item); + } catch (ex) { + this.logger.error(`saveChange failed, error = ${ex}`); + return; + } + this.subject.next(change); + this.logger.debug( + `saveChange successed, ` + + `issue_id = ${change.issue_id}, ` + + `initiator.name = ${change.initiator.name}`, + ); + } + + private getId(): string { + return randomUUID(); + } +} diff --git a/src/couchdb-datasources/changes.ts b/src/couchdb-datasources/changes.ts index 5e742d5..c90a12b 100644 --- a/src/couchdb-datasources/changes.ts +++ b/src/couchdb-datasources/changes.ts @@ -1,6 +1,7 @@ import { CouchDb } from '@app/event-emitter/couchdb-datasources/couchdb'; import { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; +import { Change } from 'src/models/change.model'; import configuration from '../configs/app'; const config = configuration(); @@ -11,9 +12,7 @@ export class Changes { private static changesDb = null; private static initilized = false; - // TODO: Указать полные типы данных - - static async getDatasource(): Promise> { + static async getDatasource(): Promise> { if (Changes.initilized) { return Changes.changesDb; } @@ -25,11 +24,12 @@ export class Changes { await n.db.create(changesDbName); } Changes.changesDb = await n.db.use(changesDbName); + Changes.initilized = true; Changes.logger.log(`Connected to changes db - ${changesDbName}`); return Changes.changesDb; } - async getDatasource(): Promise> { + async getDatasource(): Promise> { return await Changes.getDatasource(); } }