@hed-hog/core 0.0.354 → 0.0.356

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 (167) hide show
  1. package/dist/ai/ai.service.d.ts +3 -12
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +34 -125
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/core.module.d.ts.map +1 -1
  6. package/dist/core.module.js +3 -3
  7. package/dist/core.module.js.map +1 -1
  8. package/dist/file/file.service.d.ts +1 -1
  9. package/dist/file/file.service.d.ts.map +1 -1
  10. package/dist/file/file.service.js +50 -64
  11. package/dist/file/file.service.js.map +1 -1
  12. package/dist/file/provider/provider.enum.d.ts +2 -1
  13. package/dist/file/provider/provider.enum.d.ts.map +1 -1
  14. package/dist/file/provider/provider.enum.js +1 -0
  15. package/dist/file/provider/provider.enum.js.map +1 -1
  16. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  17. package/dist/file/provider/provider.factory.js +3 -0
  18. package/dist/file/provider/provider.factory.js.map +1 -1
  19. package/dist/file/provider/spaces.provider.d.ts +27 -0
  20. package/dist/file/provider/spaces.provider.d.ts.map +1 -0
  21. package/dist/file/provider/spaces.provider.js +218 -0
  22. package/dist/file/provider/spaces.provider.js.map +1 -0
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/integration-profile/dto/create.dto.d.ts +9 -0
  28. package/dist/integration-profile/dto/create.dto.d.ts.map +1 -0
  29. package/dist/integration-profile/dto/create.dto.js +54 -0
  30. package/dist/integration-profile/dto/create.dto.js.map +1 -0
  31. package/dist/integration-profile/dto/export.dto.d.ts +5 -0
  32. package/dist/integration-profile/dto/export.dto.d.ts.map +1 -0
  33. package/dist/integration-profile/dto/export.dto.js +29 -0
  34. package/dist/integration-profile/dto/export.dto.js.map +1 -0
  35. package/dist/integration-profile/dto/import-confirm.dto.d.ts +17 -0
  36. package/dist/integration-profile/dto/import-confirm.dto.d.ts.map +1 -0
  37. package/dist/integration-profile/dto/import-confirm.dto.js +77 -0
  38. package/dist/integration-profile/dto/import-confirm.dto.js.map +1 -0
  39. package/dist/integration-profile/dto/update.dto.d.ts +6 -0
  40. package/dist/integration-profile/dto/update.dto.d.ts.map +1 -0
  41. package/dist/integration-profile/dto/update.dto.js +9 -0
  42. package/dist/integration-profile/dto/update.dto.js.map +1 -0
  43. package/dist/integration-profile/integration-profile.controller.d.ts +142 -0
  44. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -0
  45. package/dist/integration-profile/integration-profile.controller.js +165 -0
  46. package/dist/integration-profile/integration-profile.controller.js.map +1 -0
  47. package/dist/integration-profile/integration-profile.module.d.ts +3 -0
  48. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -0
  49. package/dist/integration-profile/integration-profile.module.js +29 -0
  50. package/dist/integration-profile/integration-profile.module.js.map +1 -0
  51. package/dist/integration-profile/integration-profile.service.d.ts +160 -0
  52. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -0
  53. package/dist/integration-profile/integration-profile.service.js +606 -0
  54. package/dist/integration-profile/integration-profile.service.js.map +1 -0
  55. package/dist/integration-profile/integration-profile.utils.d.ts +24 -0
  56. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -0
  57. package/dist/integration-profile/integration-profile.utils.js +147 -0
  58. package/dist/integration-profile/integration-profile.utils.js.map +1 -0
  59. package/dist/mail/mail.module.d.ts.map +1 -1
  60. package/dist/mail/mail.module.js +27 -19
  61. package/dist/mail/mail.module.js.map +1 -1
  62. package/dist/mail/mail.service.d.ts +3 -2
  63. package/dist/mail/mail.service.d.ts.map +1 -1
  64. package/dist/mail/mail.service.js +25 -47
  65. package/dist/mail/mail.service.js.map +1 -1
  66. package/dist/mcp/mcp.module.js +2 -2
  67. package/dist/mcp/mcp.module.js.map +1 -1
  68. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +3 -15
  69. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -1
  70. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +12 -94
  71. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -1
  72. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  73. package/dist/mcp-chat/mcp-chat.service.js +17 -16
  74. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  75. package/dist/setting/setting.service.d.ts.map +1 -1
  76. package/dist/setting/setting.service.js +0 -2
  77. package/dist/setting/setting.service.js.map +1 -1
  78. package/dist/webhook-integration/event-webhook.controller.d.ts +1 -0
  79. package/dist/webhook-integration/event-webhook.controller.d.ts.map +1 -1
  80. package/dist/webhook-integration/event-webhook.controller.js +10 -0
  81. package/dist/webhook-integration/event-webhook.controller.js.map +1 -1
  82. package/dist/webhook-integration/event-webhook.service.d.ts +2 -0
  83. package/dist/webhook-integration/event-webhook.service.d.ts.map +1 -1
  84. package/dist/webhook-integration/event-webhook.service.js +30 -0
  85. package/dist/webhook-integration/event-webhook.service.js.map +1 -1
  86. package/dist/webhook-integration/webhook-integration.controller.d.ts +1 -0
  87. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  88. package/dist/webhook-integration/webhook-integration.controller.js +10 -0
  89. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  90. package/dist/webhook-integration/webhook-integration.service.d.ts +2 -1
  91. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  92. package/dist/webhook-integration/webhook-integration.service.js +62 -44
  93. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  94. package/dist/whatsapp-profile/dto/create.dto.d.ts +11 -0
  95. package/dist/whatsapp-profile/dto/create.dto.d.ts.map +1 -0
  96. package/dist/whatsapp-profile/dto/create.dto.js +55 -0
  97. package/dist/whatsapp-profile/dto/create.dto.js.map +1 -0
  98. package/dist/whatsapp-profile/dto/update.dto.d.ts +6 -0
  99. package/dist/whatsapp-profile/dto/update.dto.d.ts.map +1 -0
  100. package/dist/whatsapp-profile/dto/update.dto.js +9 -0
  101. package/dist/whatsapp-profile/dto/update.dto.js.map +1 -0
  102. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts +54 -0
  103. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts.map +1 -0
  104. package/dist/whatsapp-profile/whatsapp-profile.controller.js +85 -0
  105. package/dist/whatsapp-profile/whatsapp-profile.controller.js.map +1 -0
  106. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts +3 -0
  107. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts.map +1 -0
  108. package/dist/whatsapp-profile/whatsapp-profile.module.js +26 -0
  109. package/dist/whatsapp-profile/whatsapp-profile.module.js.map +1 -0
  110. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts +63 -0
  111. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts.map +1 -0
  112. package/dist/whatsapp-profile/whatsapp-profile.service.js +138 -0
  113. package/dist/whatsapp-profile/whatsapp-profile.service.js.map +1 -0
  114. package/hedhog/data/file_provider.yaml +9 -1
  115. package/hedhog/data/integration_provider.yaml +132 -0
  116. package/hedhog/data/integration_type.yaml +29 -0
  117. package/hedhog/data/menu.yaml +15 -14
  118. package/hedhog/data/role.yaml +8 -0
  119. package/hedhog/data/route.yaml +66 -15
  120. package/hedhog/data/setting_group.yaml +30 -191
  121. package/hedhog/data/setting_subgroup.yaml +5 -13
  122. package/hedhog/data/whatsapp_profile.yaml +6 -0
  123. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +25 -20
  124. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +579 -204
  125. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +1662 -0
  126. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +233 -108
  127. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +3 -822
  128. package/hedhog/frontend/app/users/permissions.tsx.ejs +79 -2
  129. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +7 -2
  130. package/hedhog/frontend/messages/en.json +172 -0
  131. package/hedhog/frontend/messages/pt.json +172 -0
  132. package/hedhog/table/integration_profile.yaml +34 -0
  133. package/hedhog/table/integration_provider.yaml +23 -0
  134. package/hedhog/table/integration_type.yaml +19 -0
  135. package/hedhog/table/webhook_action.yaml +85 -85
  136. package/hedhog/table/whatsapp_profile.yaml +26 -0
  137. package/package.json +2 -2
  138. package/src/ai/ai.service.ts +35 -153
  139. package/src/core.module.ts +3 -3
  140. package/src/file/file.service.ts +60 -76
  141. package/src/file/provider/provider.enum.ts +1 -0
  142. package/src/file/provider/provider.factory.ts +3 -0
  143. package/src/file/provider/spaces.provider.ts +261 -0
  144. package/src/index.ts +6 -3
  145. package/src/integration-profile/dto/create.dto.ts +44 -0
  146. package/src/integration-profile/dto/export.dto.ts +15 -0
  147. package/src/integration-profile/dto/import-confirm.dto.ts +61 -0
  148. package/src/integration-profile/dto/update.dto.ts +4 -0
  149. package/src/integration-profile/integration-profile.controller.ts +129 -0
  150. package/src/integration-profile/integration-profile.module.ts +16 -0
  151. package/src/integration-profile/integration-profile.service.ts +788 -0
  152. package/src/integration-profile/integration-profile.utils.ts +156 -0
  153. package/src/mail/mail.module.ts +27 -18
  154. package/src/mail/mail.service.ts +31 -60
  155. package/src/mcp/mcp.module.ts +2 -2
  156. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +10 -69
  157. package/src/mcp-chat/mcp-chat.service.ts +23 -20
  158. package/src/setting/setting.service.ts +0 -2
  159. package/src/webhook-integration/event-webhook.controller.ts +55 -50
  160. package/src/webhook-integration/event-webhook.service.ts +422 -384
  161. package/src/webhook-integration/webhook-integration.controller.ts +5 -0
  162. package/src/webhook-integration/webhook-integration.service.ts +74 -46
  163. package/src/whatsapp-profile/dto/create.dto.ts +36 -0
  164. package/src/whatsapp-profile/dto/update.dto.ts +4 -0
  165. package/src/whatsapp-profile/whatsapp-profile.controller.ts +51 -0
  166. package/src/whatsapp-profile/whatsapp-profile.module.ts +13 -0
  167. package/src/whatsapp-profile/whatsapp-profile.service.ts +160 -0
@@ -1,384 +1,422 @@
1
- import { DeleteDTO } from '@hed-hog/api';
2
- import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
- import { PrismaService } from '@hed-hog/api-prisma';
4
- import { HttpService } from '@nestjs/axios';
5
- import {
6
- BadRequestException,
7
- Injectable,
8
- Logger,
9
- NotFoundException,
10
- OnModuleInit,
11
- } from '@nestjs/common';
12
- import { firstValueFrom } from 'rxjs';
13
- import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
14
- import { DomainEvent } from '../integration/types';
15
- import {
16
- normalizeJsonObject,
17
- renderTemplate,
18
- summarizePayload,
19
- } from './webhook-utils';
20
-
21
- @Injectable()
22
- export class EventWebhookService implements OnModuleInit {
23
- private readonly logger = new Logger(EventWebhookService.name);
24
- private readonly registeredEvents = new Set<string>();
25
-
26
- constructor(
27
- private readonly prisma: PrismaService,
28
- private readonly pagination: PaginationService,
29
- private readonly http: HttpService,
30
- private readonly integrationApi: IntegrationDeveloperApiService,
31
- ) {}
32
-
33
- private get webhooks() {
34
- return (this.prisma as any).event_webhook;
35
- }
36
-
37
- private get webhookEvents() {
38
- return (this.prisma as any).event_webhook_event;
39
- }
40
-
41
- private get catalog() {
42
- return (this.prisma as any).integration_event_catalog;
43
- }
44
-
45
- private get logs() {
46
- return (this.prisma as any).event_webhook_delivery_log;
47
- }
48
-
49
- async onModuleInit() {
50
- await this.registerConfiguredEvents();
51
- }
52
-
53
- async list(paginationParams: PaginationDTO) {
54
- const fields = ['slug', 'name', 'description', 'url', 'method', 'status'];
55
- const OR = this.prisma.createInsensitiveSearch(fields, paginationParams);
56
-
57
- return this.pagination.paginate(this.webhooks, paginationParams, {
58
- where: OR.length > 0 ? { OR } : {},
59
- include: {
60
- event_webhook_event: {
61
- include: {
62
- integration_event_catalog: true,
63
- },
64
- },
65
- },
66
- orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
67
- });
68
- }
69
-
70
- async get(id: number) {
71
- const record = await this.webhooks.findUnique({
72
- where: { id },
73
- include: {
74
- event_webhook_event: {
75
- include: {
76
- integration_event_catalog: true,
77
- },
78
- },
79
- },
80
- });
81
-
82
- if (!record) {
83
- throw new NotFoundException(`Event webhook with id ${id} not found.`);
84
- }
85
-
86
- return record;
87
- }
88
-
89
- async create(data: any) {
90
- await this.ensureSlugAvailable(data.slug);
91
-
92
- const record = await this.webhooks.create({
93
- data: {
94
- ...this.normalizeWebhook(data),
95
- event_webhook_event: {
96
- create: this.normalizeEventIds(data.event_ids).map((eventId) => ({
97
- integration_event_catalog_id: eventId,
98
- })),
99
- },
100
- },
101
- include: {
102
- event_webhook_event: {
103
- include: { integration_event_catalog: true },
104
- },
105
- },
106
- });
107
-
108
- await this.registerEventsFromWebhook(record);
109
- return record;
110
- }
111
-
112
- async update(id: number, data: any) {
113
- const existing = await this.get(id);
114
- if (data.slug && data.slug !== existing.slug) {
115
- await this.ensureSlugAvailable(data.slug);
116
- }
117
-
118
- const record = await this.webhooks.update({
119
- where: { id },
120
- data: this.normalizeWebhook(data, true),
121
- include: {
122
- event_webhook_event: {
123
- include: { integration_event_catalog: true },
124
- },
125
- },
126
- });
127
-
128
- if (data.event_ids !== undefined) {
129
- await this.webhookEvents.deleteMany({
130
- where: { event_webhook_id: id },
131
- });
132
-
133
- const eventIds = this.normalizeEventIds(data.event_ids);
134
- for (const eventId of eventIds) {
135
- await this.webhookEvents.create({
136
- data: {
137
- event_webhook_id: id,
138
- integration_event_catalog_id: eventId,
139
- },
140
- });
141
- }
142
- }
143
-
144
- const refreshed = await this.get(id);
145
- await this.registerEventsFromWebhook(refreshed);
146
- return refreshed;
147
- }
148
-
149
- async delete(data: DeleteDTO) {
150
- if (!data.ids?.length) {
151
- throw new BadRequestException('You must select at least one item to delete.');
152
- }
153
-
154
- return this.webhooks.deleteMany({
155
- where: { id: { in: data.ids } },
156
- });
157
- }
158
-
159
- async listLogs(webhookId: number, paginationParams: PaginationDTO) {
160
- await this.get(webhookId);
161
- return this.pagination.paginate(this.logs, paginationParams, {
162
- where: { event_webhook_id: webhookId },
163
- orderBy: [{ created_at: 'desc' }, { id: 'desc' }],
164
- });
165
- }
166
-
167
- async handleEvent(event: DomainEvent) {
168
- const webhooks = await this.findActiveWebhooksForEvent(event.eventName);
169
-
170
- for (const webhook of webhooks) {
171
- await this.deliverWebhook(webhook, event);
172
- }
173
- }
174
-
175
- async registerConfiguredEvents() {
176
- const rows = await this.catalog.findMany({
177
- where: {
178
- status: 'active',
179
- event_webhook_event: {
180
- some: {
181
- event_webhook: {
182
- status: 'active',
183
- },
184
- },
185
- },
186
- },
187
- select: { slug: true },
188
- });
189
-
190
- for (const row of rows) {
191
- this.registerEvent(row.slug);
192
- }
193
- }
194
-
195
- private async findActiveWebhooksForEvent(eventName: string) {
196
- return this.webhooks.findMany({
197
- where: {
198
- status: 'active',
199
- event_webhook_event: {
200
- some: {
201
- integration_event_catalog: {
202
- slug: eventName,
203
- status: 'active',
204
- },
205
- },
206
- },
207
- },
208
- include: {
209
- event_webhook_event: {
210
- include: {
211
- integration_event_catalog: true,
212
- },
213
- },
214
- },
215
- orderBy: [{ priority: 'desc' }, { id: 'asc' }],
216
- });
217
- }
218
-
219
- private async deliverWebhook(webhook: any, event: DomainEvent) {
220
- const startedAt = Date.now();
221
- let attemptCount = 0;
222
- let lastError: any = null;
223
- let httpStatus: number | null = null;
224
- let responseSummary: any = null;
225
-
226
- const context = {
227
- ...event,
228
- timestamp: event.timestamp?.toISOString?.() ?? event.timestamp,
229
- outboxEventId: (event as any).outboxEventId ?? null,
230
- };
231
-
232
- const url = renderTemplate(webhook.url, context) as string;
233
- const method = webhook.method || 'POST';
234
- const headers = renderTemplate(webhook.headers || {}, context) as Record<string, string>;
235
- const query = renderTemplate(webhook.query || {}, context) as Record<string, string>;
236
- const payload = renderTemplate(webhook.payload ?? event.payload ?? {}, context);
237
- const maxAttempts = Math.max(1, Number(webhook.retry_count || 0) + 1);
238
-
239
- for (attemptCount = 1; attemptCount <= maxAttempts; attemptCount++) {
240
- try {
241
- const result = await firstValueFrom(
242
- this.http.request({
243
- url,
244
- method,
245
- headers,
246
- params: query,
247
- data: ['GET', 'DELETE'].includes(method) ? undefined : payload,
248
- timeout: Number(webhook.timeout_ms || 10000),
249
- }),
250
- );
251
-
252
- httpStatus = result.status;
253
- responseSummary = summarizePayload(result.data);
254
- lastError = null;
255
- break;
256
- } catch (error: any) {
257
- lastError = error;
258
- httpStatus = error?.response?.status ?? null;
259
- responseSummary = summarizePayload(error?.response?.data);
260
- }
261
- }
262
-
263
- const success = !lastError;
264
- await this.logs.create({
265
- data: {
266
- event_webhook_id: webhook.id,
267
- integration_event_catalog_id:
268
- webhook.event_webhook_event?.[0]?.integration_event_catalog?.id ?? null,
269
- outbox_event_id: Number((event as any).outboxEventId || 0) || null,
270
- event_name: event.eventName,
271
- method,
272
- url,
273
- status: success ? 'success' : 'failed',
274
- http_status: httpStatus,
275
- duration_ms: Date.now() - startedAt,
276
- attempt_count: attemptCount,
277
- error_message: lastError
278
- ? lastError instanceof Error
279
- ? lastError.message
280
- : String(lastError)
281
- : null,
282
- request_summary: summarizePayload({
283
- headers,
284
- query,
285
- payload,
286
- }),
287
- response_summary: responseSummary,
288
- },
289
- });
290
-
291
- if (!success) {
292
- throw lastError;
293
- }
294
- }
295
-
296
- private async registerEventsFromWebhook(webhook: any) {
297
- const eventNames =
298
- webhook.event_webhook_event
299
- ?.map((row: any) => row.integration_event_catalog?.slug)
300
- .filter(Boolean) || [];
301
-
302
- for (const eventName of eventNames) {
303
- this.registerEvent(eventName);
304
- }
305
- }
306
-
307
- private registerEvent(eventName: string) {
308
- if (this.registeredEvents.has(eventName)) {
309
- return;
310
- }
311
-
312
- this.integrationApi.subscribe({
313
- eventName,
314
- consumerName: `core.event-webhook.${eventName}`,
315
- priority: -100,
316
- handler: async (event) => {
317
- await this.handleEvent(event);
318
- },
319
- });
320
-
321
- this.registeredEvents.add(eventName);
322
- this.logger.log(`Registered event webhook dispatcher for ${eventName}`);
323
- }
324
-
325
- private async ensureSlugAvailable(slug: string) {
326
- const existing = await this.webhooks.findUnique({
327
- where: { slug: String(slug || '').trim() },
328
- select: { id: true },
329
- });
330
-
331
- if (existing) {
332
- throw new BadRequestException(`Event webhook "${slug}" already exists.`);
333
- }
334
- }
335
-
336
- private normalizeWebhook(data: any, partial = false) {
337
- const normalized: Record<string, any> = {};
338
- const assign = (key: string, value: any) => {
339
- if (!partial || value !== undefined) {
340
- normalized[key] = value;
341
- }
342
- };
343
-
344
- assign('slug', String(data.slug ?? '').trim());
345
- assign('name', String(data.name ?? '').trim());
346
- assign('description', this.nullableString(data.description, partial));
347
- assign('status', data.status === 'inactive' ? 'inactive' : 'active');
348
- assign('priority', Number(data.priority ?? 0));
349
- assign('url', String(data.url ?? '').trim());
350
- assign('method', String(data.method || 'POST').toUpperCase());
351
- assign('headers', normalizeJsonObject(data.headers));
352
- assign('query', normalizeJsonObject(data.query));
353
- assign('payload', normalizeJsonObject(data.payload));
354
- assign('timeout_ms', Number(data.timeout_ms || 10000));
355
- assign('retry_count', Number(data.retry_count || 0));
356
-
357
- return normalized;
358
- }
359
-
360
- private normalizeEventIds(value: any) {
361
- if (!value) {
362
- return [];
363
- }
364
-
365
- const values = Array.isArray(value) ? value : String(value).split(',');
366
- return values
367
- .map((item) => Number(item))
368
- .filter((item) => Number.isFinite(item) && item > 0);
369
- }
370
-
371
- private nullableString(value: any, partial = false) {
372
- if (value === undefined) {
373
- return partial ? undefined : null;
374
- }
375
-
376
- if (value === null) {
377
- return null;
378
- }
379
-
380
- const normalized = String(value).trim();
381
- return normalized || null;
382
- }
383
- }
384
-
1
+ import { DeleteDTO } from '@hed-hog/api';
2
+ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
+ import { PrismaService } from '@hed-hog/api-prisma';
4
+ import { HttpService } from '@nestjs/axios';
5
+ import {
6
+ BadRequestException,
7
+ Injectable,
8
+ Logger,
9
+ NotFoundException,
10
+ OnModuleInit,
11
+ } from '@nestjs/common';
12
+ import { firstValueFrom } from 'rxjs';
13
+ import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
14
+ import { DomainEvent } from '../integration/types';
15
+ import {
16
+ normalizeJsonObject,
17
+ renderTemplate,
18
+ summarizePayload,
19
+ } from './webhook-utils';
20
+
21
+ @Injectable()
22
+ export class EventWebhookService implements OnModuleInit {
23
+ private readonly logger = new Logger(EventWebhookService.name);
24
+ private readonly registeredEvents = new Set<string>();
25
+
26
+ constructor(
27
+ private readonly prisma: PrismaService,
28
+ private readonly pagination: PaginationService,
29
+ private readonly http: HttpService,
30
+ private readonly integrationApi: IntegrationDeveloperApiService,
31
+ ) {}
32
+
33
+ private get webhooks() {
34
+ return (this.prisma as any).event_webhook;
35
+ }
36
+
37
+ private get webhookEvents() {
38
+ return (this.prisma as any).event_webhook_event;
39
+ }
40
+
41
+ private get catalog() {
42
+ return (this.prisma as any).integration_event_catalog;
43
+ }
44
+
45
+ private get logs() {
46
+ return (this.prisma as any).event_webhook_delivery_log;
47
+ }
48
+
49
+ async onModuleInit() {
50
+ await this.registerConfiguredEvents();
51
+ }
52
+
53
+ async list(paginationParams: PaginationDTO) {
54
+ const fields = ['slug', 'name', 'description', 'url', 'method', 'status'];
55
+ const OR = this.prisma.createInsensitiveSearch(fields, paginationParams);
56
+
57
+ return this.pagination.paginate(this.webhooks, paginationParams, {
58
+ where: OR.length > 0 ? { OR } : {},
59
+ include: {
60
+ event_webhook_event: {
61
+ include: {
62
+ integration_event_catalog: true,
63
+ },
64
+ },
65
+ },
66
+ orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
67
+ });
68
+ }
69
+
70
+ async get(id: number) {
71
+ const record = await this.webhooks.findUnique({
72
+ where: { id },
73
+ include: {
74
+ event_webhook_event: {
75
+ include: {
76
+ integration_event_catalog: true,
77
+ },
78
+ },
79
+ },
80
+ });
81
+
82
+ if (!record) {
83
+ throw new NotFoundException(`Event webhook with id ${id} not found.`);
84
+ }
85
+
86
+ return record;
87
+ }
88
+
89
+ async create(data: any) {
90
+ await this.ensureSlugAvailable(data.slug);
91
+
92
+ const record = await this.webhooks.create({
93
+ data: {
94
+ ...this.normalizeWebhook(data),
95
+ event_webhook_event: {
96
+ create: this.normalizeEventIds(data.event_ids).map((eventId) => ({
97
+ integration_event_catalog_id: eventId,
98
+ })),
99
+ },
100
+ },
101
+ include: {
102
+ event_webhook_event: {
103
+ include: { integration_event_catalog: true },
104
+ },
105
+ },
106
+ });
107
+
108
+ await this.registerEventsFromWebhook(record);
109
+ return record;
110
+ }
111
+
112
+ async update(id: number, data: any) {
113
+ const existing = await this.get(id);
114
+ if (data.slug && data.slug !== existing.slug) {
115
+ await this.ensureSlugAvailable(data.slug);
116
+ }
117
+
118
+ const record = await this.webhooks.update({
119
+ where: { id },
120
+ data: this.normalizeWebhook(data, true),
121
+ include: {
122
+ event_webhook_event: {
123
+ include: { integration_event_catalog: true },
124
+ },
125
+ },
126
+ });
127
+
128
+ if (data.event_ids !== undefined) {
129
+ await this.webhookEvents.deleteMany({
130
+ where: { event_webhook_id: id },
131
+ });
132
+
133
+ const eventIds = this.normalizeEventIds(data.event_ids);
134
+ for (const eventId of eventIds) {
135
+ await this.webhookEvents.create({
136
+ data: {
137
+ event_webhook_id: id,
138
+ integration_event_catalog_id: eventId,
139
+ },
140
+ });
141
+ }
142
+ }
143
+
144
+ const refreshed = await this.get(id);
145
+ await this.registerEventsFromWebhook(refreshed);
146
+ return refreshed;
147
+ }
148
+
149
+ async delete(data: DeleteDTO) {
150
+ if (!data.ids?.length) {
151
+ throw new BadRequestException('You must select at least one item to delete.');
152
+ }
153
+
154
+ return this.webhooks.deleteMany({
155
+ where: { id: { in: data.ids } },
156
+ });
157
+ }
158
+
159
+ async duplicate(id: number) {
160
+ const original = await this.get(id);
161
+
162
+ const newSlug = await this.generateUniqueSlug(original.slug);
163
+ const eventIds = (original.event_webhook_event ?? []).map(
164
+ (e: any) => e.integration_event_catalog_id,
165
+ );
166
+
167
+ const record = await this.webhooks.create({
168
+ data: {
169
+ ...this.normalizeWebhook({ ...original, slug: newSlug, name: `${original.name} (copy)` }),
170
+ event_webhook_event: {
171
+ create: eventIds.map((eventId: number) => ({
172
+ integration_event_catalog_id: eventId,
173
+ })),
174
+ },
175
+ },
176
+ include: {
177
+ event_webhook_event: { include: { integration_event_catalog: true } },
178
+ },
179
+ });
180
+
181
+ await this.registerEventsFromWebhook(record);
182
+ return record;
183
+ }
184
+
185
+ private async generateUniqueSlug(base: string): Promise<string> {
186
+ const candidate = `${base}-copy`;
187
+ const existing = await this.webhooks.findUnique({ where: { slug: candidate } });
188
+ if (!existing) return candidate;
189
+
190
+ for (let i = 2; ; i++) {
191
+ const next = `${base}-copy-${i}`;
192
+ const taken = await this.webhooks.findUnique({ where: { slug: next } });
193
+ if (!taken) return next;
194
+ }
195
+ }
196
+
197
+ async listLogs(webhookId: number, paginationParams: PaginationDTO) {
198
+ await this.get(webhookId);
199
+ return this.pagination.paginate(this.logs, paginationParams, {
200
+ where: { event_webhook_id: webhookId },
201
+ orderBy: [{ created_at: 'desc' }, { id: 'desc' }],
202
+ });
203
+ }
204
+
205
+ async handleEvent(event: DomainEvent) {
206
+ const webhooks = await this.findActiveWebhooksForEvent(event.eventName);
207
+
208
+ for (const webhook of webhooks) {
209
+ await this.deliverWebhook(webhook, event);
210
+ }
211
+ }
212
+
213
+ async registerConfiguredEvents() {
214
+ const rows = await this.catalog.findMany({
215
+ where: {
216
+ status: 'active',
217
+ event_webhook_event: {
218
+ some: {
219
+ event_webhook: {
220
+ status: 'active',
221
+ },
222
+ },
223
+ },
224
+ },
225
+ select: { slug: true },
226
+ });
227
+
228
+ for (const row of rows) {
229
+ this.registerEvent(row.slug);
230
+ }
231
+ }
232
+
233
+ private async findActiveWebhooksForEvent(eventName: string) {
234
+ return this.webhooks.findMany({
235
+ where: {
236
+ status: 'active',
237
+ event_webhook_event: {
238
+ some: {
239
+ integration_event_catalog: {
240
+ slug: eventName,
241
+ status: 'active',
242
+ },
243
+ },
244
+ },
245
+ },
246
+ include: {
247
+ event_webhook_event: {
248
+ include: {
249
+ integration_event_catalog: true,
250
+ },
251
+ },
252
+ },
253
+ orderBy: [{ priority: 'desc' }, { id: 'asc' }],
254
+ });
255
+ }
256
+
257
+ private async deliverWebhook(webhook: any, event: DomainEvent) {
258
+ const startedAt = Date.now();
259
+ let attemptCount = 0;
260
+ let lastError: any = null;
261
+ let httpStatus: number | null = null;
262
+ let responseSummary: any = null;
263
+
264
+ const context = {
265
+ ...event,
266
+ timestamp: event.timestamp?.toISOString?.() ?? event.timestamp,
267
+ outboxEventId: (event as any).outboxEventId ?? null,
268
+ };
269
+
270
+ const url = renderTemplate(webhook.url, context) as string;
271
+ const method = webhook.method || 'POST';
272
+ const headers = renderTemplate(webhook.headers || {}, context) as Record<string, string>;
273
+ const query = renderTemplate(webhook.query || {}, context) as Record<string, string>;
274
+ const payload = renderTemplate(webhook.payload ?? event.payload ?? {}, context);
275
+ const maxAttempts = Math.max(1, Number(webhook.retry_count || 0) + 1);
276
+
277
+ for (attemptCount = 1; attemptCount <= maxAttempts; attemptCount++) {
278
+ try {
279
+ const result = await firstValueFrom(
280
+ this.http.request({
281
+ url,
282
+ method,
283
+ headers,
284
+ params: query,
285
+ data: ['GET', 'DELETE'].includes(method) ? undefined : payload,
286
+ timeout: Number(webhook.timeout_ms || 10000),
287
+ }),
288
+ );
289
+
290
+ httpStatus = result.status;
291
+ responseSummary = summarizePayload(result.data);
292
+ lastError = null;
293
+ break;
294
+ } catch (error: any) {
295
+ lastError = error;
296
+ httpStatus = error?.response?.status ?? null;
297
+ responseSummary = summarizePayload(error?.response?.data);
298
+ }
299
+ }
300
+
301
+ const success = !lastError;
302
+ await this.logs.create({
303
+ data: {
304
+ event_webhook_id: webhook.id,
305
+ integration_event_catalog_id:
306
+ webhook.event_webhook_event?.[0]?.integration_event_catalog?.id ?? null,
307
+ outbox_event_id: Number((event as any).outboxEventId || 0) || null,
308
+ event_name: event.eventName,
309
+ method,
310
+ url,
311
+ status: success ? 'success' : 'failed',
312
+ http_status: httpStatus,
313
+ duration_ms: Date.now() - startedAt,
314
+ attempt_count: attemptCount,
315
+ error_message: lastError
316
+ ? lastError instanceof Error
317
+ ? lastError.message
318
+ : String(lastError)
319
+ : null,
320
+ request_summary: summarizePayload({
321
+ headers,
322
+ query,
323
+ payload,
324
+ }),
325
+ response_summary: responseSummary,
326
+ },
327
+ });
328
+
329
+ if (!success) {
330
+ throw lastError;
331
+ }
332
+ }
333
+
334
+ private async registerEventsFromWebhook(webhook: any) {
335
+ const eventNames =
336
+ webhook.event_webhook_event
337
+ ?.map((row: any) => row.integration_event_catalog?.slug)
338
+ .filter(Boolean) || [];
339
+
340
+ for (const eventName of eventNames) {
341
+ this.registerEvent(eventName);
342
+ }
343
+ }
344
+
345
+ private registerEvent(eventName: string) {
346
+ if (this.registeredEvents.has(eventName)) {
347
+ return;
348
+ }
349
+
350
+ this.integrationApi.subscribe({
351
+ eventName,
352
+ consumerName: `core.event-webhook.${eventName}`,
353
+ priority: -100,
354
+ handler: async (event) => {
355
+ await this.handleEvent(event);
356
+ },
357
+ });
358
+
359
+ this.registeredEvents.add(eventName);
360
+ this.logger.log(`Registered event webhook dispatcher for ${eventName}`);
361
+ }
362
+
363
+ private async ensureSlugAvailable(slug: string) {
364
+ const existing = await this.webhooks.findUnique({
365
+ where: { slug: String(slug || '').trim() },
366
+ select: { id: true },
367
+ });
368
+
369
+ if (existing) {
370
+ throw new BadRequestException(`Event webhook "${slug}" already exists.`);
371
+ }
372
+ }
373
+
374
+ private normalizeWebhook(data: any, partial = false) {
375
+ const normalized: Record<string, any> = {};
376
+ const assign = (key: string, value: any) => {
377
+ if (!partial || value !== undefined) {
378
+ normalized[key] = value;
379
+ }
380
+ };
381
+
382
+ assign('slug', String(data.slug ?? '').trim());
383
+ assign('name', String(data.name ?? '').trim());
384
+ assign('description', this.nullableString(data.description, partial));
385
+ assign('status', data.status === 'inactive' ? 'inactive' : 'active');
386
+ assign('priority', Number(data.priority ?? 0));
387
+ assign('url', String(data.url ?? '').trim());
388
+ assign('method', String(data.method || 'POST').toUpperCase());
389
+ assign('headers', normalizeJsonObject(data.headers));
390
+ assign('query', normalizeJsonObject(data.query));
391
+ assign('payload', normalizeJsonObject(data.payload));
392
+ assign('timeout_ms', Number(data.timeout_ms || 10000));
393
+ assign('retry_count', Number(data.retry_count || 0));
394
+
395
+ return normalized;
396
+ }
397
+
398
+ private normalizeEventIds(value: any) {
399
+ if (!value) {
400
+ return [];
401
+ }
402
+
403
+ const values = Array.isArray(value) ? value : String(value).split(',');
404
+ return values
405
+ .map((item) => Number(item))
406
+ .filter((item) => Number.isFinite(item) && item > 0);
407
+ }
408
+
409
+ private nullableString(value: any, partial = false) {
410
+ if (value === undefined) {
411
+ return partial ? undefined : null;
412
+ }
413
+
414
+ if (value === null) {
415
+ return null;
416
+ }
417
+
418
+ const normalized = String(value).trim();
419
+ return normalized || null;
420
+ }
421
+ }
422
+