@coopenomics/notifications 2025.11.8-3

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.
Files changed (43) hide show
  1. package/.cursor/rules/notifications.mdc +20 -0
  2. package/.env-example +2 -0
  3. package/README.md +218 -0
  4. package/build.config.ts +13 -0
  5. package/dist/index.cjs +3027 -0
  6. package/dist/index.d.cts +792 -0
  7. package/dist/index.d.mts +792 -0
  8. package/dist/index.d.ts +792 -0
  9. package/dist/index.mjs +3016 -0
  10. package/package.json +55 -0
  11. package/src/base/defaults.ts +66 -0
  12. package/src/base/workflow-builder.ts +128 -0
  13. package/src/index.ts +9 -0
  14. package/src/sync/README.md +54 -0
  15. package/src/sync/novu-sync.service.ts +246 -0
  16. package/src/sync/sync-runner.ts +154 -0
  17. package/src/types/index.ts +99 -0
  18. package/src/utils/index.ts +1 -0
  19. package/src/utils/slugify/builtinReplacements.ts +2065 -0
  20. package/src/utils/slugify/slugify.ts +284 -0
  21. package/src/utils/slugify/transliterate.ts +48 -0
  22. package/src/workflows/approval-request/index.ts +52 -0
  23. package/src/workflows/approval-response/index.ts +54 -0
  24. package/src/workflows/decision-approved/index.ts +50 -0
  25. package/src/workflows/email-verification/index.ts +35 -0
  26. package/src/workflows/incoming-transfer/index.ts +43 -0
  27. package/src/workflows/index.ts +74 -0
  28. package/src/workflows/invite/index.ts +34 -0
  29. package/src/workflows/meet-ended/index.ts +51 -0
  30. package/src/workflows/meet-initial/index.ts +53 -0
  31. package/src/workflows/meet-reminder-end/index.ts +52 -0
  32. package/src/workflows/meet-reminder-start/index.ts +51 -0
  33. package/src/workflows/meet-restart/index.ts +53 -0
  34. package/src/workflows/meet-started/index.ts +51 -0
  35. package/src/workflows/new-agenda-item/index.ts +51 -0
  36. package/src/workflows/new-deposit-payment-request/index.ts +50 -0
  37. package/src/workflows/new-initial-payment-request/index.ts +50 -0
  38. package/src/workflows/payment-cancelled/index.ts +51 -0
  39. package/src/workflows/payment-paid/index.ts +50 -0
  40. package/src/workflows/reset-key/index.ts +36 -0
  41. package/src/workflows/server-provisioned/index.ts +45 -0
  42. package/src/workflows/welcome/index.ts +43 -0
  43. package/tsconfig.json +18 -0
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@coopenomics/notifications",
3
+ "version": "2025.11.8-3",
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
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "typesVersions": {
14
+ "*": {
15
+ "*": [
16
+ "./dist/*",
17
+ "./dist/index.d.ts"
18
+ ]
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "unbuild",
23
+ "dev": "unbuild --watch",
24
+ "clean": "rm -rf dist",
25
+ "sync": "tsx src/sync/sync-runner.ts",
26
+ "sync:dev": "tsx src/sync/sync-runner.ts --dev",
27
+ "sync:watch": "tsx src/sync/sync-runner.ts --dev"
28
+ },
29
+ "dependencies": {
30
+ "axios": "^1.7.7",
31
+ "dotenv": "^17.1.0",
32
+ "transliteration": "^2.3.5",
33
+ "zod": "^3.22.4",
34
+ "zod-to-json-schema": "^3.22.5"
35
+ },
36
+ "devDependencies": {
37
+ "@types/chokidar": "^2.1.3",
38
+ "@types/node": "^20.0.0",
39
+ "chokidar": "^3.6.0",
40
+ "tsx": "^4.0.0",
41
+ "typescript": "^5.0.0",
42
+ "unbuild": "^2.0.0"
43
+ },
44
+ "bin": {
45
+ "novu-sync": "./dist/sync/sync-runner.js"
46
+ },
47
+ "exports": {
48
+ ".": {
49
+ "types": "./dist/index.d.ts",
50
+ "import": "./dist/index.mjs",
51
+ "require": "./dist/index.cjs"
52
+ }
53
+ },
54
+ "gitHead": "0937d087709c65b1de50b5b549f46ecbf5b42ba3"
55
+ }
@@ -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
+ });
@@ -0,0 +1,128 @@
1
+ import { z } from 'zod';
2
+ import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import {
4
+ WorkflowDefinition,
5
+ BaseWorkflowPayload,
6
+ WorkflowStep,
7
+ PayloadSchema,
8
+ NovuWorkflowData,
9
+ NovuOrigin
10
+ } from '../types';
11
+ import { createDefaultPreferences } from './defaults';
12
+
13
+ export class WorkflowBuilder<T extends BaseWorkflowPayload> {
14
+ private _name: string = '';
15
+ private _workflowId: string = '';
16
+ private _description?: string;
17
+ private _steps: WorkflowStep[] = [];
18
+ private _payloadZodSchema?: z.ZodSchema<T>;
19
+ private _origin?: NovuOrigin;
20
+ private _tags?: string[];
21
+
22
+ static create<T extends BaseWorkflowPayload>(): WorkflowBuilder<T> {
23
+ return new WorkflowBuilder<T>();
24
+ }
25
+
26
+ name(name: string): this {
27
+ this._name = name;
28
+ return this;
29
+ }
30
+
31
+ workflowId(id: string): this {
32
+ this._workflowId = id;
33
+ return this;
34
+ }
35
+
36
+ description(description: string): this {
37
+ this._description = description;
38
+ return this;
39
+ }
40
+
41
+ origin(origin: NovuOrigin): this {
42
+ this._origin = origin;
43
+ return this;
44
+ }
45
+
46
+ tags(tags: string[]): this {
47
+ this._tags = tags;
48
+ return this;
49
+ }
50
+
51
+ addStep(step: WorkflowStep): this {
52
+ this._steps.push(step);
53
+ return this;
54
+ }
55
+
56
+ addSteps(steps: WorkflowStep[]): this {
57
+ this._steps.push(...steps);
58
+ return this;
59
+ }
60
+
61
+ payloadSchema(schema: z.ZodSchema<T>): this {
62
+ this._payloadZodSchema = schema;
63
+ return this;
64
+ }
65
+
66
+ build(): WorkflowDefinition<T> {
67
+ if (!this._name) throw new Error('Workflow name is required');
68
+ if (!this._workflowId) throw new Error('Workflow ID is required');
69
+ if (!this._payloadZodSchema) throw new Error('Payload schema is required');
70
+
71
+ // Преобразуем Zod схему в JSON Schema для Novu
72
+ const jsonSchema = zodToJsonSchema(this._payloadZodSchema);
73
+
74
+ // Правильная типизация: zodToJsonSchema может вернуть разные типы схем
75
+ const payloadSchema: PayloadSchema = {
76
+ type: (jsonSchema as any).type || 'object',
77
+ properties: (jsonSchema as any).properties || {},
78
+ required: Array.isArray((jsonSchema as any).required) ? (jsonSchema as any).required : [],
79
+ };
80
+
81
+ const workflow: WorkflowDefinition<T> = {
82
+ name: this._name,
83
+ workflowId: this._workflowId,
84
+ description: this._description,
85
+ payloadSchema,
86
+ steps: this._steps,
87
+ preferences: createDefaultPreferences(),
88
+ payloadZodSchema: this._payloadZodSchema,
89
+ };
90
+
91
+ // Добавляем origin только если он указан
92
+ if (this._origin) {
93
+ workflow.origin = this._origin;
94
+ }
95
+
96
+ // Добавляем tags только если они указаны
97
+ if (this._tags) {
98
+ workflow.tags = this._tags;
99
+ }
100
+
101
+ return workflow;
102
+ }
103
+
104
+ // Конвертация в формат для Novu API
105
+ toNovuData(): NovuWorkflowData {
106
+ const workflow = this.build();
107
+ const novuData: NovuWorkflowData = {
108
+ name: workflow.name,
109
+ workflowId: workflow.workflowId,
110
+ description: workflow.description,
111
+ payloadSchema: workflow.payloadSchema,
112
+ steps: workflow.steps,
113
+ preferences: workflow.preferences,
114
+ };
115
+
116
+ // Добавляем origin только если он указан
117
+ if (workflow.origin) {
118
+ novuData.origin = workflow.origin;
119
+ }
120
+
121
+ // Добавляем tags только если они указаны
122
+ if (workflow.tags) {
123
+ novuData.tags = workflow.tags;
124
+ }
125
+
126
+ return novuData;
127
+ }
128
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Экспортируем типы
2
+ export * as Types from './types';
3
+
4
+ // Экспортируем базовые утилиты
5
+ export * from './base/defaults';
6
+ export { WorkflowBuilder } from './base/workflow-builder';
7
+
8
+ // Экспортируем все воркфлоу
9
+ export * as Workflows from './workflows';
@@ -0,0 +1,54 @@
1
+ # Синхронизация Novu Workflows
2
+
3
+ Эта папка содержит серверную логику для синхронизации воркфлоу с Novu API.
4
+
5
+ ## Файлы
6
+
7
+ - `novu-sync.service.ts` - Сервис для работы с Novu API
8
+ - `sync-runner.ts` - Скрипт для запуска синхронизации
9
+ - `README.md` - Этот файл
10
+
11
+ ## Использование
12
+
13
+ ### Production режим (один раз)
14
+ ```bash
15
+ pnpm run sync
16
+ ```
17
+
18
+ ### Development режим (watch)
19
+ ```bash
20
+ pnpm run sync:dev
21
+ # или
22
+ pnpm run sync:watch
23
+ ```
24
+
25
+ ## Переменные окружения
26
+
27
+ Требуются следующие переменные:
28
+ - `NOVU_API_KEY` - API ключ для Novu
29
+ - `NOVU_API_URL` - URL Novu API (по умолчанию: https://api.novu.co)
30
+
31
+ ## Режимы работы
32
+
33
+ ### Production
34
+ - Запускается один раз
35
+ - Синхронизирует все воркфлоу
36
+ - Завершается после выполнения
37
+
38
+ ### Development
39
+ - Запускается и остается активным
40
+ - Отслеживает изменения в папках `workflows/` и `types/`
41
+ - Автоматически перезапускает синхронизацию при изменениях
42
+ - Дебаунс 1 секунда для избежания множественных запусков
43
+
44
+ ## Как работает
45
+
46
+ 1. Загружает все воркфлоу из `../workflows/index.ts`
47
+ 2. Для каждого воркфлоу:
48
+ - Проверяет существование в Novu API
49
+ - Создает новый или обновляет существующий
50
+ 3. Логирует результаты операций
51
+
52
+ ## Важно
53
+
54
+ ⚠️ Этот модуль предназначен только для серверного использования и не должен импортироваться на фронтенде!
@@ -0,0 +1,246 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import {
3
+ Types,
4
+ Workflows
5
+ } from '../index';
6
+
7
+ export interface NovuSyncConfig {
8
+ apiKey: string;
9
+ apiUrl: string;
10
+ }
11
+
12
+ export class NovuSyncService {
13
+ private readonly client: AxiosInstance;
14
+ private readonly config: NovuSyncConfig;
15
+
16
+ constructor(config: NovuSyncConfig) {
17
+ this.config = config;
18
+
19
+ if (!this.config.apiKey) {
20
+ throw new Error('NOVU_API_KEY is required');
21
+ }
22
+
23
+ if (!this.config.apiUrl) {
24
+ throw new Error('NOVU_API_URL is required');
25
+ }
26
+ this.client = axios.create({
27
+ baseURL: this.config.apiUrl,
28
+ headers: {
29
+ 'Authorization': `ApiKey ${this.config.apiKey}`,
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Получить информацию о воркфлоу
37
+ */
38
+ async getWorkflow(workflowId: string): Promise<any> {
39
+ try {
40
+ const response = await this.client.get(`/v2/workflows/${workflowId}`);
41
+ return response.data;
42
+ } catch (error: any) {
43
+ if (error.response?.status === 404) {
44
+ return null;
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Получить список всех воркфлоу
52
+ */
53
+ async getAllWorkflows(): Promise<any[]> {
54
+ try {
55
+ const response = await this.client.get('/v2/workflows', {
56
+ params: {
57
+ limit: 10000
58
+ }
59
+ });
60
+
61
+ return response.data.data.workflows || [];
62
+ } catch (error: any) {
63
+ console.error('Ошибка получения списка воркфлоу:', console.dir(error.response?.data || error.message, {depth: null}));
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Создать новый воркфлоу
70
+ */
71
+ async createWorkflow(data: Types.NovuWorkflowData): Promise<any> {
72
+ try {
73
+ // Для создания НЕ передаем origin (как в testFramework2.ts)
74
+ const createData = { ...data };
75
+
76
+ delete createData.origin;
77
+
78
+ const response = await this.client.post('/v2/workflows', createData);
79
+ return response.data;
80
+ } catch (error: any) {
81
+ console.error(`Ошибка создания воркфлоу ${data.workflowId}:`, error.response?.data || error.message);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Обновить существующий воркфлоу
88
+ */
89
+ async updateWorkflow(workflowId: string, data: Types.NovuWorkflowData): Promise<any> {
90
+ try {
91
+ // Для обновления ВСЕГДА передаем origin: "external" (как в testFramework2.ts)
92
+ const updateData = { ...data, origin: 'novu-cloud' as const };
93
+ const response = await this.client.put(`/v2/workflows/${workflowId}`, updateData);
94
+ // console.log('response', response.data);
95
+ return response.data;
96
+ } catch (error: any) {
97
+ console.error(`Ошибка обновления воркфлоу ${workflowId}:`, error.response?.data || error.message);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Удалить воркфлоу по ID
104
+ */
105
+ async deleteWorkflow(workflowId: string): Promise<void> {
106
+ try {
107
+ await this.client.delete(`/v2/workflows/${workflowId}`);
108
+ console.log(`Удален воркфлоу: ${workflowId}`);
109
+ } catch (error: any) {
110
+ if (error.response?.status === 404) {
111
+ console.log(`Воркфлоу ${workflowId} не найден (уже удален)`);
112
+ return;
113
+ }
114
+ console.error(`Ошибка удаления воркфлоу ${workflowId}:`, error.response?.data || error.message);
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Создать или обновить воркфлоу (upsert)
121
+ */
122
+ async upsertWorkflow(workflow: Types.WorkflowDefinition): Promise<any> {
123
+ try {
124
+ console.log(`Проверяем воркфлоу: ${workflow.workflowId}`);
125
+
126
+ const existingWorkflow = await this.getWorkflow(workflow.workflowId);
127
+
128
+ const novuData: Types.NovuWorkflowData = {
129
+ name: workflow.name,
130
+ workflowId: workflow.workflowId,
131
+ description: workflow.description,
132
+ payloadSchema: workflow.payloadSchema,
133
+ steps: workflow.steps,
134
+ preferences: workflow.preferences,
135
+ tags: workflow.tags,
136
+ };
137
+
138
+ if (existingWorkflow) {
139
+ console.log(`Обновляем воркфлоу: ${workflow.workflowId}`);
140
+ return await this.updateWorkflow(workflow.workflowId, novuData);
141
+ } else {
142
+ console.log(`Создаём воркфлоу: ${workflow.workflowId}`);
143
+ return await this.createWorkflow(novuData);
144
+ }
145
+ } catch (error: any) {
146
+ console.error(`Ошибка upsert воркфлоу ${workflow.workflowId}:`, error.message);
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Удалить все существующие воркфлоу
153
+ */
154
+ async deleteAllWorkflows(): Promise<void> {
155
+ console.log('Получаем список всех воркфлоу для удаления...');
156
+
157
+ try {
158
+ const workflows = await this.getAllWorkflows();
159
+ console.log(`Найдено ${workflows.length} воркфлоу для удаления`);
160
+
161
+ if (workflows.length === 0) {
162
+ console.log('Нет воркфлоу для удаления');
163
+ return;
164
+ }
165
+
166
+ const errors: string[] = [];
167
+ let deletedCount = 0;
168
+
169
+ for (const workflow of workflows) {
170
+ try {
171
+ await this.deleteWorkflow(workflow.workflowId || workflow._id);
172
+ deletedCount++;
173
+ } catch (error: any) {
174
+ const errorMessage = `Ошибка удаления воркфлоу ${workflow.workflowId || workflow._id}: ${error.message}`;
175
+ console.error(`✗ ${errorMessage}`);
176
+ errors.push(errorMessage);
177
+ }
178
+ }
179
+
180
+ console.log(`\nРезультат удаления:`);
181
+ console.log(`✅ Удалено: ${deletedCount}`);
182
+ console.log(`❌ Ошибки: ${errors.length}`);
183
+
184
+ if (errors.length > 0) {
185
+ console.log(`\nСписок ошибок удаления:`);
186
+ errors.forEach((error, index) => {
187
+ console.log(`${index + 1}. ${error}`);
188
+ });
189
+ throw new Error(`Удаление завершилось с ошибками: ${errors.length} из ${workflows.length} воркфлоу`);
190
+ }
191
+
192
+ console.log('✅ Все существующие воркфлоу удалены успешно');
193
+ } catch (error: any) {
194
+ console.error('❌ Критическая ошибка при удалении воркфлоу:', error.message);
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Создать или обновить все воркфлоу (с предварительным удалением существующих)
201
+ */
202
+ async upsertAllWorkflows(): Promise<void> {
203
+ console.log('🚀 Начинаем полную синхронизацию воркфлоу...');
204
+
205
+ // Шаг 1: Удаляем все существующие воркфлоу
206
+ // try {
207
+ // await this.deleteAllWorkflows();
208
+ // } catch (error: any) {
209
+ // console.error('❌ Ошибка при удалении существующих воркфлоу:', error.message);
210
+ // throw error;
211
+ // }
212
+
213
+ // Шаг 2: Создаем новые воркфлоу
214
+ console.log(`\n📝 Начинаем создание ${Workflows.allWorkflows.length} новых воркфлоу...`);
215
+
216
+ const errors: string[] = [];
217
+ let successCount = 0;
218
+
219
+ for (const workflow of Workflows.allWorkflows) {
220
+ try {
221
+ await this.upsertWorkflow(workflow);
222
+ console.log(`✓ Воркфлоу ${workflow.workflowId} успешно создан`);
223
+ successCount++;
224
+ } catch (error: any) {
225
+ const errorMessage = `Ошибка создания воркфлоу ${workflow.workflowId}: ${error.message}`;
226
+ console.error(`✗ ${errorMessage}`);
227
+ errors.push(errorMessage);
228
+ }
229
+ }
230
+
231
+ console.log(`\nРезультат синхронизации:`);
232
+ console.log(`✅ Успешно создано: ${successCount}`);
233
+ console.log(`❌ Ошибки: ${errors.length}`);
234
+
235
+ if (errors.length > 0) {
236
+ console.log(`\nСписок ошибок:`);
237
+ errors.forEach((error, index) => {
238
+ console.log(`${index + 1}. ${error}`);
239
+ });
240
+
241
+ throw new Error(`Синхронизация завершилась с ошибками: ${errors.length} из ${Workflows.allWorkflows.length} воркфлоу`);
242
+ }
243
+
244
+ console.log('✅ Все воркфлоу синхронизированы успешно');
245
+ }
246
+ }