@coopenomics/notifications 2025.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/notifications.mdc +20 -0
- package/.env-example +2 -0
- package/README.md +218 -0
- package/build.config.ts +13 -0
- package/dist/index.cjs +3027 -0
- package/dist/index.d.cts +792 -0
- package/dist/index.d.mts +792 -0
- package/dist/index.d.ts +792 -0
- package/dist/index.mjs +3016 -0
- package/dist/sync/novu-sync.service.d.ts +30 -0
- package/dist/sync/novu-sync.service.js +128 -0
- package/dist/sync/sync-runner.d.ts +4 -0
- package/dist/sync/sync-runner.js +130 -0
- package/dist/utils/role-utils.d.ts +8 -0
- package/dist/utils/role-utils.js +18 -0
- package/dist/workflows/incoming-transfer/incoming-transfer-workflow.d.ts +3 -0
- package/dist/workflows/incoming-transfer/incoming-transfer-workflow.js +16 -0
- package/dist/workflows/incoming-transfer/index.d.ts +3 -0
- package/dist/workflows/incoming-transfer/index.js +2 -0
- package/dist/workflows/incoming-transfer/types.d.ts +12 -0
- package/dist/workflows/incoming-transfer/types.js +5 -0
- package/dist/workflows/new-agenda-item/index.d.ts +3 -0
- package/dist/workflows/new-agenda-item/index.js +2 -0
- package/dist/workflows/new-agenda-item/new-agenda-item-workflow.d.ts +3 -0
- package/dist/workflows/new-agenda-item/new-agenda-item-workflow.js +16 -0
- package/dist/workflows/new-agenda-item/types.d.ts +27 -0
- package/dist/workflows/new-agenda-item/types.js +10 -0
- package/dist/workflows/role-based-workflows.d.ts +21 -0
- package/dist/workflows/role-based-workflows.js +65 -0
- package/package.json +51 -0
- package/src/base/defaults.ts +66 -0
- package/src/base/workflow-builder.ts +128 -0
- package/src/index.ts +9 -0
- package/src/sync/README.md +54 -0
- package/src/sync/novu-sync.service.ts +246 -0
- package/src/sync/sync-runner.ts +154 -0
- package/src/types/index.ts +99 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/slugify/builtinReplacements.ts +2065 -0
- package/src/utils/slugify/slugify.ts +284 -0
- package/src/utils/slugify/transliterate.ts +48 -0
- package/src/workflows/approval-request/index.ts +52 -0
- package/src/workflows/approval-response/index.ts +54 -0
- package/src/workflows/decision-approved/index.ts +50 -0
- package/src/workflows/email-verification/index.ts +35 -0
- package/src/workflows/incoming-transfer/index.ts +43 -0
- package/src/workflows/index.ts +74 -0
- package/src/workflows/invite/index.ts +34 -0
- package/src/workflows/meet-ended/index.ts +51 -0
- package/src/workflows/meet-initial/index.ts +53 -0
- package/src/workflows/meet-reminder-end/index.ts +52 -0
- package/src/workflows/meet-reminder-start/index.ts +51 -0
- package/src/workflows/meet-restart/index.ts +53 -0
- package/src/workflows/meet-started/index.ts +51 -0
- package/src/workflows/new-agenda-item/index.ts +51 -0
- package/src/workflows/new-deposit-payment-request/index.ts +50 -0
- package/src/workflows/new-initial-payment-request/index.ts +50 -0
- package/src/workflows/payment-cancelled/index.ts +51 -0
- package/src/workflows/payment-paid/index.ts +50 -0
- package/src/workflows/reset-key/index.ts +36 -0
- package/src/workflows/server-provisioned/index.ts +45 -0
- package/src/workflows/welcome/index.ts +43 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Types } from '../index';
|
|
2
|
+
export interface NovuSyncConfig {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class NovuSyncService {
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly config;
|
|
9
|
+
constructor(config: NovuSyncConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Получить информацию о воркфлоу
|
|
12
|
+
*/
|
|
13
|
+
getWorkflow(workflowId: string): Promise<any>;
|
|
14
|
+
/**
|
|
15
|
+
* Создать новый воркфлоу
|
|
16
|
+
*/
|
|
17
|
+
createWorkflow(data: Types.NovuWorkflowData): Promise<any>;
|
|
18
|
+
/**
|
|
19
|
+
* Обновить существующий воркфлоу
|
|
20
|
+
*/
|
|
21
|
+
updateWorkflow(workflowId: string, data: Types.NovuWorkflowData): Promise<any>;
|
|
22
|
+
/**
|
|
23
|
+
* Создать или обновить воркфлоу (upsert)
|
|
24
|
+
*/
|
|
25
|
+
upsertWorkflow(workflow: Types.WorkflowDefinition): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* Создать или обновить все воркфлоу
|
|
28
|
+
*/
|
|
29
|
+
upsertAllWorkflows(): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { allWorkflows } from '../index';
|
|
3
|
+
export class NovuSyncService {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
if (!this.config.apiKey) {
|
|
7
|
+
throw new Error('NOVU_API_KEY is required');
|
|
8
|
+
}
|
|
9
|
+
if (!this.config.apiUrl) {
|
|
10
|
+
throw new Error('NOVU_API_URL is required');
|
|
11
|
+
}
|
|
12
|
+
this.client = axios.create({
|
|
13
|
+
baseURL: this.config.apiUrl,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `ApiKey ${this.config.apiKey}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Получить информацию о воркфлоу
|
|
22
|
+
*/
|
|
23
|
+
async getWorkflow(workflowId) {
|
|
24
|
+
try {
|
|
25
|
+
const response = await this.client.get(`/v2/workflows/${workflowId}`);
|
|
26
|
+
return response.data;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.response?.status === 404) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Создать новый воркфлоу
|
|
37
|
+
*/
|
|
38
|
+
async createWorkflow(data) {
|
|
39
|
+
try {
|
|
40
|
+
// Для создания НЕ передаем origin (как в testFramework2.ts)
|
|
41
|
+
const createData = { ...data };
|
|
42
|
+
delete createData.origin;
|
|
43
|
+
const response = await this.client.post('/v2/workflows', createData);
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(`Ошибка создания воркфлоу ${data.workflowId}:`, error.response?.data || error.message);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Обновить существующий воркфлоу
|
|
53
|
+
*/
|
|
54
|
+
async updateWorkflow(workflowId, data) {
|
|
55
|
+
try {
|
|
56
|
+
// Для обновления ВСЕГДА передаем origin: "external" (как в testFramework2.ts)
|
|
57
|
+
const updateData = { ...data, origin: 'novu-cloud' };
|
|
58
|
+
const response = await this.client.put(`/v2/workflows/${workflowId}`, updateData);
|
|
59
|
+
// console.log('response', response.data);
|
|
60
|
+
return response.data;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`Ошибка обновления воркфлоу ${workflowId}:`, error.response?.data || error.message);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Создать или обновить воркфлоу (upsert)
|
|
69
|
+
*/
|
|
70
|
+
async upsertWorkflow(workflow) {
|
|
71
|
+
try {
|
|
72
|
+
console.log(`Проверяем воркфлоу: ${workflow.workflowId}`);
|
|
73
|
+
const existingWorkflow = await this.getWorkflow(workflow.workflowId);
|
|
74
|
+
const novuData = {
|
|
75
|
+
name: workflow.name,
|
|
76
|
+
workflowId: workflow.workflowId,
|
|
77
|
+
description: workflow.description,
|
|
78
|
+
payloadSchema: workflow.payloadSchema,
|
|
79
|
+
steps: workflow.steps,
|
|
80
|
+
preferences: workflow.preferences,
|
|
81
|
+
tags: workflow.tags,
|
|
82
|
+
};
|
|
83
|
+
if (existingWorkflow) {
|
|
84
|
+
console.log(`Обновляем воркфлоу: ${workflow.workflowId}`);
|
|
85
|
+
return await this.updateWorkflow(workflow.workflowId, novuData);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`Создаём воркфлоу: ${workflow.workflowId}`);
|
|
89
|
+
return await this.createWorkflow(novuData);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`Ошибка upsert воркфлоу ${workflow.workflowId}:`, error.message);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Создать или обновить все воркфлоу
|
|
99
|
+
*/
|
|
100
|
+
async upsertAllWorkflows() {
|
|
101
|
+
console.log(`Начинаем upsert ${allWorkflows.length} воркфлоу...`);
|
|
102
|
+
const errors = [];
|
|
103
|
+
let successCount = 0;
|
|
104
|
+
for (const workflow of allWorkflows) {
|
|
105
|
+
try {
|
|
106
|
+
await this.upsertWorkflow(workflow);
|
|
107
|
+
console.log(`✓ Воркфлоу ${workflow.workflowId} успешно обработан`);
|
|
108
|
+
successCount++;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const errorMessage = `Ошибка обработки воркфлоу ${workflow.workflowId}: ${error.message}`;
|
|
112
|
+
console.error(`✗ ${errorMessage}`);
|
|
113
|
+
errors.push(errorMessage);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log(`\nРезультат синхронизации:`);
|
|
117
|
+
console.log(`✅ Успешно: ${successCount}`);
|
|
118
|
+
console.log(`❌ Ошибки: ${errors.length}`);
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
console.log(`\nСписок ошибок:`);
|
|
121
|
+
errors.forEach((error, index) => {
|
|
122
|
+
console.log(`${index + 1}. ${error}`);
|
|
123
|
+
});
|
|
124
|
+
throw new Error(`Синхронизация завершилась с ошибками: ${errors.length} из ${allWorkflows.length} воркфлоу`);
|
|
125
|
+
}
|
|
126
|
+
console.log('✅ Все воркфлоу синхронизированы успешно');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NovuSyncService } from './novu-sync.service';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { watch } from 'chokidar';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
dotenv.config();
|
|
9
|
+
// Конфигурация из переменных окружения
|
|
10
|
+
const config = {
|
|
11
|
+
apiKey: process.env.NOVU_API_KEY || '',
|
|
12
|
+
apiUrl: process.env.NOVU_API_URL || '',
|
|
13
|
+
};
|
|
14
|
+
async function runSync() {
|
|
15
|
+
console.log('🔄 Запуск синхронизации воркфлоу...');
|
|
16
|
+
try {
|
|
17
|
+
// Проверяем конфигурацию
|
|
18
|
+
if (!config.apiKey) {
|
|
19
|
+
throw new Error('❌ NOVU_API_KEY не установлен в переменных окружения');
|
|
20
|
+
}
|
|
21
|
+
if (!config.apiUrl) {
|
|
22
|
+
throw new Error('❌ NOVU_API_URL не установлен в переменных окружения');
|
|
23
|
+
}
|
|
24
|
+
const syncService = new NovuSyncService(config);
|
|
25
|
+
await syncService.upsertAllWorkflows();
|
|
26
|
+
console.log('✅ Синхронизация завершена успешно');
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('❌ Ошибка синхронизации:', error.message);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const isDev = args.includes('--dev') || process.env.NODE_ENV === 'development';
|
|
37
|
+
if (isDev) {
|
|
38
|
+
console.log('📡 Режим разработки: отслеживаем изменения...');
|
|
39
|
+
// Запускаем синхронизацию сразу
|
|
40
|
+
const initialSyncSuccess = await runSync();
|
|
41
|
+
if (!initialSyncSuccess) {
|
|
42
|
+
console.error('❌ Первоначальная синхронизация не удалась');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// Отслеживаем изменения в файлах воркфлоу и типов
|
|
46
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const workflowsPath = join(__dirname, '../workflows');
|
|
48
|
+
const typesPath = join(__dirname, '../types');
|
|
49
|
+
const watchPaths = [];
|
|
50
|
+
if (existsSync(workflowsPath)) {
|
|
51
|
+
watchPaths.push(workflowsPath);
|
|
52
|
+
}
|
|
53
|
+
if (existsSync(typesPath)) {
|
|
54
|
+
watchPaths.push(typesPath);
|
|
55
|
+
}
|
|
56
|
+
if (watchPaths.length > 0) {
|
|
57
|
+
console.log('👀 Отслеживаем изменения в:', watchPaths.join(', '));
|
|
58
|
+
let syncTimeout = null;
|
|
59
|
+
const watcher = watch(watchPaths, {
|
|
60
|
+
ignored: /node_modules/,
|
|
61
|
+
persistent: true,
|
|
62
|
+
ignoreInitial: true,
|
|
63
|
+
});
|
|
64
|
+
watcher.on('change', (path) => {
|
|
65
|
+
console.log(`📝 Изменен файл: ${path}`);
|
|
66
|
+
// Дебаунс для избежания множественных запусков
|
|
67
|
+
if (syncTimeout) {
|
|
68
|
+
clearTimeout(syncTimeout);
|
|
69
|
+
}
|
|
70
|
+
syncTimeout = setTimeout(async () => {
|
|
71
|
+
console.log('⏰ Запускаем синхронизацию...');
|
|
72
|
+
const success = await runSync();
|
|
73
|
+
if (!success) {
|
|
74
|
+
console.error('⚠️ Синхронизация не удалась, но продолжаем отслеживание...');
|
|
75
|
+
}
|
|
76
|
+
}, 1000);
|
|
77
|
+
});
|
|
78
|
+
watcher.on('add', (path) => {
|
|
79
|
+
console.log(`➕ Добавлен файл: ${path}`);
|
|
80
|
+
if (syncTimeout) {
|
|
81
|
+
clearTimeout(syncTimeout);
|
|
82
|
+
}
|
|
83
|
+
syncTimeout = setTimeout(async () => {
|
|
84
|
+
console.log('⏰ Запускаем синхронизацию...');
|
|
85
|
+
const success = await runSync();
|
|
86
|
+
if (!success) {
|
|
87
|
+
console.error('⚠️ Синхронизация не удалась, но продолжаем отслеживание...');
|
|
88
|
+
}
|
|
89
|
+
}, 1000);
|
|
90
|
+
});
|
|
91
|
+
watcher.on('unlink', (path) => {
|
|
92
|
+
console.log(`➖ Удален файл: ${path}`);
|
|
93
|
+
if (syncTimeout) {
|
|
94
|
+
clearTimeout(syncTimeout);
|
|
95
|
+
}
|
|
96
|
+
syncTimeout = setTimeout(async () => {
|
|
97
|
+
console.log('⏰ Запускаем синхронизацию...');
|
|
98
|
+
const success = await runSync();
|
|
99
|
+
if (!success) {
|
|
100
|
+
console.error('⚠️ Синхронизация не удалась, но продолжаем отслеживание...');
|
|
101
|
+
}
|
|
102
|
+
}, 1000);
|
|
103
|
+
});
|
|
104
|
+
console.log('💡 Нажмите Ctrl+C для выхода');
|
|
105
|
+
// Держим процесс живым
|
|
106
|
+
process.on('SIGINT', () => {
|
|
107
|
+
console.log('\n👋 Выход из режима разработки');
|
|
108
|
+
watcher.close();
|
|
109
|
+
process.exit(0);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log('⚠️ Папки для отслеживания не найдены');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Production режим - запускаем один раз
|
|
119
|
+
const success = await runSync();
|
|
120
|
+
process.exit(success ? 0 : 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Проверяем, запущен ли файл как основной модуль
|
|
124
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
125
|
+
main().catch((error) => {
|
|
126
|
+
console.error('❌ Критическая ошибка:', error);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export { runSync, NovuSyncService };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { UserRole } from '../workflows/role-based-workflows';
|
|
2
|
+
export interface UserWithRole {
|
|
3
|
+
isChairman: boolean;
|
|
4
|
+
isMember: boolean;
|
|
5
|
+
isUser?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function getUserRole(user: UserWithRole): UserRole;
|
|
8
|
+
export declare function isAuthenticatedUser(user: UserWithRole): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getUserRole = getUserRole;
|
|
4
|
+
exports.isAuthenticatedUser = isAuthenticatedUser;
|
|
5
|
+
// Функция для определения роли пользователя
|
|
6
|
+
function getUserRole(user) {
|
|
7
|
+
if (user.isChairman) {
|
|
8
|
+
return 'chairman';
|
|
9
|
+
}
|
|
10
|
+
if (user.isMember) {
|
|
11
|
+
return 'member';
|
|
12
|
+
}
|
|
13
|
+
return 'user';
|
|
14
|
+
}
|
|
15
|
+
// Функция для проверки является ли пользователь авторизованным
|
|
16
|
+
function isAuthenticatedUser(user) {
|
|
17
|
+
return user.isChairman || user.isMember || Boolean(user.isUser);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WorkflowBuilder } from '../../base/workflow-builder';
|
|
2
|
+
import { createEmailStep, createInAppStep, createPushStep } from '../../base/defaults';
|
|
3
|
+
import { incomingTransferPayloadSchema } from './types';
|
|
4
|
+
export const incomingTransferWorkflow = WorkflowBuilder
|
|
5
|
+
.create()
|
|
6
|
+
.name('Входящий перевод')
|
|
7
|
+
.workflowId('vkhodyashchiy-perevod')
|
|
8
|
+
.description('Уведомление о получении входящего перевода')
|
|
9
|
+
.payloadSchema(incomingTransferPayloadSchema)
|
|
10
|
+
.tags(['user', 'chairman', 'member']) // Доступно для всех ролей
|
|
11
|
+
.addSteps([
|
|
12
|
+
createEmailStep('incoming-transfer-email', 'Получен входящий перевод на сумму {{payload.quantity}}', 'Уведомляем вас о получении входящего перевода.<br><br><strong>Сумма перевода: {{payload.quantity}}</strong><br><br>Перевод успешно зачислен на ваш счет.'),
|
|
13
|
+
createInAppStep('incoming-transfer-notification', 'Входящий перевод', 'Получен входящий перевод на сумму {{payload.quantity}}'),
|
|
14
|
+
createPushStep('incoming-transfer-push', 'Входящий перевод', 'Получен перевод на сумму {{payload.quantity}}'),
|
|
15
|
+
])
|
|
16
|
+
.build();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BaseWorkflowPayload } from '../../types';
|
|
3
|
+
export declare const incomingTransferPayloadSchema: z.ZodObject<{
|
|
4
|
+
quantity: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
quantity: string;
|
|
7
|
+
}, {
|
|
8
|
+
quantity: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type IncomingTransferPayload = z.infer<typeof incomingTransferPayloadSchema>;
|
|
11
|
+
export interface IncomingTransferWorkflowPayload extends BaseWorkflowPayload, IncomingTransferPayload {
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { WorkflowBuilder } from '../../base/workflow-builder';
|
|
2
|
+
import { createEmailStep, createInAppStep, createPushStep } from '../../base/defaults';
|
|
3
|
+
import { newAgendaItemPayloadSchema } from './types';
|
|
4
|
+
export const newAgendaItemWorkflow = WorkflowBuilder
|
|
5
|
+
.create()
|
|
6
|
+
.name('Новый вопрос на повестке')
|
|
7
|
+
.workflowId('noviy-vopros-na-povestke')
|
|
8
|
+
.description('Уведомление о новом вопросе на повестке дня собрания совета для председателя и членов совета')
|
|
9
|
+
.payloadSchema(newAgendaItemPayloadSchema)
|
|
10
|
+
.tags(['chairman', 'member']) // Доступно только для председателя и членов
|
|
11
|
+
.addSteps([
|
|
12
|
+
createEmailStep('new-agenda-item-email', 'Новый вопрос на повестке: {{payload.itemTitle}}', 'Добавлен новый вопрос на повестку:<br><br><strong>{{payload.itemTitle}}</strong><br><br>{{payload.itemDescription}}<br><br>Автор: {{payload.authorName}} ({{payload.authorEmail}})<br><br>{{payload.meetingDate}}Дата заседания: {{payload.meetingDate}}<br>{{payload.meetingDate}}{{payload.meetingTitle}}Заседание: {{payload.meetingTitle}}{{payload.meetingTitle}}'),
|
|
13
|
+
createInAppStep('new-agenda-item-notification', 'Новый вопрос на повестке', 'Добавлен новый вопрос: {{payload.itemTitle}}<br>Автор: {{payload.authorName}}'),
|
|
14
|
+
createPushStep('new-agenda-item-push', 'Новый вопрос на повестке', 'Добавлен новый вопрос: {{payload.itemTitle}}'),
|
|
15
|
+
])
|
|
16
|
+
.build();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BaseWorkflowPayload } from '../../types';
|
|
3
|
+
export declare const newAgendaItemPayloadSchema: z.ZodObject<{
|
|
4
|
+
itemTitle: z.ZodString;
|
|
5
|
+
itemDescription: z.ZodString;
|
|
6
|
+
authorName: z.ZodString;
|
|
7
|
+
authorEmail: z.ZodString;
|
|
8
|
+
meetingDate: z.ZodOptional<z.ZodString>;
|
|
9
|
+
meetingTitle: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
itemTitle: string;
|
|
12
|
+
itemDescription: string;
|
|
13
|
+
authorName: string;
|
|
14
|
+
authorEmail: string;
|
|
15
|
+
meetingDate?: string | undefined;
|
|
16
|
+
meetingTitle?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
itemTitle: string;
|
|
19
|
+
itemDescription: string;
|
|
20
|
+
authorName: string;
|
|
21
|
+
authorEmail: string;
|
|
22
|
+
meetingDate?: string | undefined;
|
|
23
|
+
meetingTitle?: string | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export type NewAgendaItemPayload = z.infer<typeof newAgendaItemPayloadSchema>;
|
|
26
|
+
export interface NewAgendaItemWorkflowPayload extends BaseWorkflowPayload, NewAgendaItemPayload {
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Схема для new-agenda-item воркфлоу
|
|
3
|
+
export const newAgendaItemPayloadSchema = z.object({
|
|
4
|
+
itemTitle: z.string(),
|
|
5
|
+
itemDescription: z.string(),
|
|
6
|
+
authorName: z.string(),
|
|
7
|
+
authorEmail: z.string().email(),
|
|
8
|
+
meetingDate: z.string().optional(),
|
|
9
|
+
meetingTitle: z.string().optional(),
|
|
10
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WorkflowDefinition } from '../types';
|
|
2
|
+
export type UserRole = 'chairman' | 'member' | 'user';
|
|
3
|
+
export interface RoleBasedWorkflow {
|
|
4
|
+
workflow: WorkflowDefinition;
|
|
5
|
+
roles: UserRole[];
|
|
6
|
+
displayName: string;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const roleBasedWorkflows: RoleBasedWorkflow[];
|
|
10
|
+
export declare function getWorkflowsByRole(role: UserRole): RoleBasedWorkflow[];
|
|
11
|
+
export declare function getWorkflowsWithRoleAccess(role: UserRole): Array<RoleBasedWorkflow & {
|
|
12
|
+
hasAccess: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function hasWorkflowAccess(workflowId: string, role: UserRole): boolean;
|
|
15
|
+
export declare function getAvailableRoles(userRole: UserRole): UserRole[];
|
|
16
|
+
export declare function getRoleDisplayName(role: UserRole): string;
|
|
17
|
+
export declare const workflowsByRole: {
|
|
18
|
+
readonly chairman: RoleBasedWorkflow[];
|
|
19
|
+
readonly member: RoleBasedWorkflow[];
|
|
20
|
+
readonly user: RoleBasedWorkflow[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { welcomeWorkflow } from './welcome';
|
|
2
|
+
import { newAgendaItemWorkflow } from './new-agenda-item';
|
|
3
|
+
// Определение воркфлоу по ролям
|
|
4
|
+
export const roleBasedWorkflows = [
|
|
5
|
+
{
|
|
6
|
+
workflow: welcomeWorkflow,
|
|
7
|
+
roles: ['chairman', 'member', 'user'], // Все роли
|
|
8
|
+
displayName: 'Приветственные уведомления',
|
|
9
|
+
description: 'Уведомления при регистрации в системе',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
workflow: newAgendaItemWorkflow,
|
|
13
|
+
roles: ['chairman', 'member'], // Только председатель и члены совета
|
|
14
|
+
displayName: 'Новый вопрос на повестке',
|
|
15
|
+
description: 'Уведомления о новых вопросах на повестке заседания',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
// Функция для получения воркфлоу по роли пользователя
|
|
19
|
+
export function getWorkflowsByRole(role) {
|
|
20
|
+
return roleBasedWorkflows.filter(item => item.roles.includes(role));
|
|
21
|
+
}
|
|
22
|
+
// Функция для получения всех воркфлоу с указанием доступности для роли
|
|
23
|
+
export function getWorkflowsWithRoleAccess(role) {
|
|
24
|
+
return roleBasedWorkflows.map(item => ({
|
|
25
|
+
...item,
|
|
26
|
+
hasAccess: item.roles.includes(role),
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
// Функция для проверки доступа к воркфлоу
|
|
30
|
+
export function hasWorkflowAccess(workflowId, role) {
|
|
31
|
+
const workflowItem = roleBasedWorkflows.find(item => item.workflow.workflowId === workflowId);
|
|
32
|
+
return workflowItem ? workflowItem.roles.includes(role) : false;
|
|
33
|
+
}
|
|
34
|
+
// Функция для получения всех доступных ролей для пользователя
|
|
35
|
+
export function getAvailableRoles(userRole) {
|
|
36
|
+
switch (userRole) {
|
|
37
|
+
case 'chairman':
|
|
38
|
+
return ['user', 'member', 'chairman']; // Председатель видит все роли
|
|
39
|
+
case 'member':
|
|
40
|
+
return ['user', 'member']; // Член совета видит user и member
|
|
41
|
+
case 'user':
|
|
42
|
+
return ['user']; // Обычный пользователь видит только user
|
|
43
|
+
default:
|
|
44
|
+
return ['user'];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Функция для получения названия роли для отображения
|
|
48
|
+
export function getRoleDisplayName(role) {
|
|
49
|
+
switch (role) {
|
|
50
|
+
case 'chairman':
|
|
51
|
+
return 'Председатель';
|
|
52
|
+
case 'member':
|
|
53
|
+
return 'Член совета';
|
|
54
|
+
case 'user':
|
|
55
|
+
return 'Пользователь';
|
|
56
|
+
default:
|
|
57
|
+
return 'Пользователь';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Экспорт воркфлоу по ролям для удобного доступа
|
|
61
|
+
export const workflowsByRole = {
|
|
62
|
+
chairman: getWorkflowsByRole('chairman'),
|
|
63
|
+
member: getWorkflowsByRole('member'),
|
|
64
|
+
user: getWorkflowsByRole('user'),
|
|
65
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coopenomics/notifications",
|
|
3
|
+
"version": "2025.11.8",
|
|
4
|
+
"description": "Typesafe notification workflows library for Novu",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "dist/index.cjs",
|
|
8
|
+
"module": "dist/index.mjs",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"typesVersions": {
|
|
11
|
+
"*": {
|
|
12
|
+
"*": [
|
|
13
|
+
"./dist/*",
|
|
14
|
+
"./dist/index.d.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "unbuild",
|
|
20
|
+
"dev": "unbuild --watch",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"sync": "tsx src/sync/sync-runner.ts",
|
|
23
|
+
"sync:dev": "tsx src/sync/sync-runner.ts --dev",
|
|
24
|
+
"sync:watch": "tsx src/sync/sync-runner.ts --dev"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.7.7",
|
|
28
|
+
"dotenv": "^17.1.0",
|
|
29
|
+
"transliteration": "^2.3.5",
|
|
30
|
+
"zod": "^3.22.4",
|
|
31
|
+
"zod-to-json-schema": "^3.22.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/chokidar": "^2.1.3",
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"chokidar": "^3.6.0",
|
|
37
|
+
"tsx": "^4.0.0",
|
|
38
|
+
"typescript": "^5.0.0",
|
|
39
|
+
"unbuild": "^2.0.0"
|
|
40
|
+
},
|
|
41
|
+
"bin": {
|
|
42
|
+
"novu-sync": "./dist/sync/sync-runner.js"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.mjs",
|
|
48
|
+
"require": "./dist/index.cjs"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PreferencesConfig, ChannelsConfig, ChannelConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
// Базовая конфигурация канала
|
|
4
|
+
export const createChannelConfig = (enabled: boolean, readOnly = false): ChannelConfig => ({
|
|
5
|
+
enabled,
|
|
6
|
+
readOnly,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Базовая конфигурация каналов
|
|
10
|
+
export const createDefaultChannelsConfig = (): ChannelsConfig => ({
|
|
11
|
+
email: createChannelConfig(true),
|
|
12
|
+
sms: createChannelConfig(false),
|
|
13
|
+
in_app: createChannelConfig(true),
|
|
14
|
+
push: createChannelConfig(false),
|
|
15
|
+
chat: createChannelConfig(false),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Базовые preferences для воркфлоу
|
|
19
|
+
export const createDefaultPreferences = (): PreferencesConfig => ({
|
|
20
|
+
user: {
|
|
21
|
+
all: createChannelConfig(true),
|
|
22
|
+
channels: createDefaultChannelsConfig(),
|
|
23
|
+
},
|
|
24
|
+
workflow: {
|
|
25
|
+
all: createChannelConfig(true),
|
|
26
|
+
channels: createDefaultChannelsConfig(),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Вспомогательные функции для создания шагов
|
|
31
|
+
export const createEmailStep = (name: string, subject: string, body: string) => ({
|
|
32
|
+
name,
|
|
33
|
+
type: 'email' as const,
|
|
34
|
+
controlValues: {
|
|
35
|
+
subject,
|
|
36
|
+
body,
|
|
37
|
+
editorType: 'html' as const,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const createInAppStep = (name: string, subject: string, body: string, avatar?: string) => ({
|
|
42
|
+
name,
|
|
43
|
+
type: 'in_app' as const,
|
|
44
|
+
controlValues: {
|
|
45
|
+
subject,
|
|
46
|
+
body,
|
|
47
|
+
avatar: avatar || 'https://novu.coopenomics.world/images/bell.svg',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const createPushStep = (name: string, title: string, body: string) => ({
|
|
52
|
+
name,
|
|
53
|
+
type: 'push' as const,
|
|
54
|
+
controlValues: {
|
|
55
|
+
subject: title,
|
|
56
|
+
body,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const createSmsStep = (name: string, body: string) => ({
|
|
61
|
+
name,
|
|
62
|
+
type: 'sms' as const,
|
|
63
|
+
controlValues: {
|
|
64
|
+
body,
|
|
65
|
+
},
|
|
66
|
+
});
|