@hed-hog/core 0.0.300 → 0.0.302

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 (40) hide show
  1. package/dist/ai/ai.service.d.ts +13 -2
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +104 -2
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +26 -9
  6. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +11 -5
  8. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +34 -10
  10. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.service.js +196 -69
  12. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/integration/services/integration-link.service.d.ts +5 -1
  18. package/dist/integration/services/integration-link.service.d.ts.map +1 -1
  19. package/dist/integration/services/integration-link.service.js +141 -53
  20. package/dist/integration/services/integration-link.service.js.map +1 -1
  21. package/dist/mail/mail.service.d.ts +9 -2
  22. package/dist/mail/mail.service.d.ts.map +1 -1
  23. package/dist/mail/mail.service.js +56 -4
  24. package/dist/mail/mail.service.js.map +1 -1
  25. package/dist/setting/setting.service.d.ts +6 -1
  26. package/dist/setting/setting.service.d.ts.map +1 -1
  27. package/dist/setting/setting.service.js +188 -15
  28. package/dist/setting/setting.service.js.map +1 -1
  29. package/hedhog/data/setting_group.yaml +28 -0
  30. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +305 -75
  31. package/hedhog/frontend/messages/en.json +15 -3
  32. package/hedhog/frontend/messages/pt.json +15 -3
  33. package/package.json +5 -5
  34. package/src/ai/ai.service.ts +129 -1
  35. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +9 -2
  36. package/src/dashboard/dashboard-core/dashboard-core.service.ts +276 -75
  37. package/src/index.ts +7 -6
  38. package/src/integration/services/integration-link.service.ts +190 -55
  39. package/src/mail/mail.service.ts +67 -3
  40. package/src/setting/setting.service.ts +222 -15
@@ -14,13 +14,81 @@ export interface CreateIntegrationLinkDto {
14
14
  }
15
15
 
16
16
  export interface IntegrationLinkPersistenceClient {
17
- integrationLink: PrismaService['integrationLink'];
17
+ integrationLink?: PrismaService['integrationLink'];
18
+ integration_link?: PrismaService['integration_link'];
18
19
  }
19
20
 
21
+ type IntegrationLinkRecord = {
22
+ id: number | string;
23
+ source_module?: string;
24
+ source_entity_type?: string;
25
+ source_entity_id?: string;
26
+ target_module?: string;
27
+ target_entity_type?: string;
28
+ target_entity_id?: string;
29
+ link_type?: LinkType | string;
30
+ created_at?: Date;
31
+ updated_at?: Date;
32
+ sourceModule?: string;
33
+ sourceEntityType?: string;
34
+ sourceEntityId?: string;
35
+ targetModule?: string;
36
+ targetEntityType?: string;
37
+ targetEntityId?: string;
38
+ linkType?: LinkType | string;
39
+ metadata?: Record<string, any> | null;
40
+ createdAt?: Date;
41
+ updatedAt?: Date;
42
+ };
43
+
20
44
  @Injectable()
21
45
  export class IntegrationLinkService {
22
46
  constructor(private readonly prisma: PrismaService) {}
23
47
 
48
+ private resolvePersistence(
49
+ persistenceClient?: IntegrationLinkPersistenceClient,
50
+ ) {
51
+ const client = (persistenceClient ?? this.prisma) as PrismaService &
52
+ IntegrationLinkPersistenceClient;
53
+ const delegate = client.integration_link ?? client.integrationLink;
54
+ const useSnakeCase = !!client.integration_link;
55
+
56
+ if (!delegate) {
57
+ throw new Error(
58
+ 'Integration link delegate is not available on the Prisma client.',
59
+ );
60
+ }
61
+
62
+ return { delegate, useSnakeCase };
63
+ }
64
+
65
+ private toDomainLink(record: IntegrationLinkRecord): IntegrationLink {
66
+ return {
67
+ id: String(record.id),
68
+ sourceModule: record.source_module ?? record.sourceModule ?? '',
69
+ sourceEntityType:
70
+ record.source_entity_type ?? record.sourceEntityType ?? '',
71
+ sourceEntityId: record.source_entity_id ?? record.sourceEntityId ?? '',
72
+ targetModule: record.target_module ?? record.targetModule ?? '',
73
+ targetEntityType:
74
+ record.target_entity_type ?? record.targetEntityType ?? '',
75
+ targetEntityId: record.target_entity_id ?? record.targetEntityId ?? '',
76
+ linkType: (record.link_type ?? record.linkType ?? LinkType.REFERENCE) as LinkType,
77
+ metadata: record.metadata ?? null,
78
+ createdAt: record.created_at ?? record.createdAt ?? new Date(),
79
+ updatedAt: record.updated_at ?? record.updatedAt ?? new Date(),
80
+ };
81
+ }
82
+
83
+ private toDatabaseId(linkId: string | number) {
84
+ if (typeof linkId === 'number') {
85
+ return linkId;
86
+ }
87
+
88
+ const numericId = Number(linkId);
89
+ return Number.isNaN(numericId) ? linkId : numericId;
90
+ }
91
+
24
92
  /**
25
93
  * Create a link between entities from different modules
26
94
  */
@@ -28,20 +96,33 @@ export class IntegrationLinkService {
28
96
  dto: CreateIntegrationLinkDto,
29
97
  persistenceClient?: IntegrationLinkPersistenceClient,
30
98
  ): Promise<IntegrationLink> {
31
- const client = persistenceClient ?? this.prisma;
32
-
33
- return client.integrationLink.create({
34
- data: {
35
- sourceModule: dto.sourceModule,
36
- sourceEntityType: dto.sourceEntityType,
37
- sourceEntityId: dto.sourceEntityId,
38
- targetModule: dto.targetModule,
39
- targetEntityType: dto.targetEntityType,
40
- targetEntityId: dto.targetEntityId,
41
- linkType: dto.linkType,
42
- metadata: dto.metadata || null,
43
- },
99
+ const { delegate, useSnakeCase } = this.resolvePersistence(persistenceClient);
100
+
101
+ const record = await delegate.create({
102
+ data: useSnakeCase
103
+ ? {
104
+ source_module: dto.sourceModule,
105
+ source_entity_type: dto.sourceEntityType,
106
+ source_entity_id: dto.sourceEntityId,
107
+ target_module: dto.targetModule,
108
+ target_entity_type: dto.targetEntityType,
109
+ target_entity_id: dto.targetEntityId,
110
+ link_type: dto.linkType,
111
+ metadata: dto.metadata || null,
112
+ }
113
+ : {
114
+ sourceModule: dto.sourceModule,
115
+ sourceEntityType: dto.sourceEntityType,
116
+ sourceEntityId: dto.sourceEntityId,
117
+ targetModule: dto.targetModule,
118
+ targetEntityType: dto.targetEntityType,
119
+ targetEntityId: dto.targetEntityId,
120
+ linkType: dto.linkType,
121
+ metadata: dto.metadata || null,
122
+ },
44
123
  });
124
+
125
+ return this.toDomainLink(record as IntegrationLinkRecord);
45
126
  }
46
127
 
47
128
  /**
@@ -52,13 +133,24 @@ export class IntegrationLinkService {
52
133
  sourceEntityType: string,
53
134
  sourceEntityId: string,
54
135
  ): Promise<IntegrationLink[]> {
55
- return this.prisma.integrationLink.findMany({
56
- where: {
57
- sourceModule,
58
- sourceEntityType,
59
- sourceEntityId,
60
- },
136
+ const { delegate, useSnakeCase } = this.resolvePersistence();
137
+ const records = await delegate.findMany({
138
+ where: useSnakeCase
139
+ ? {
140
+ source_module: sourceModule,
141
+ source_entity_type: sourceEntityType,
142
+ source_entity_id: sourceEntityId,
143
+ }
144
+ : {
145
+ sourceModule,
146
+ sourceEntityType,
147
+ sourceEntityId,
148
+ },
61
149
  });
150
+
151
+ return records.map((record: IntegrationLinkRecord) =>
152
+ this.toDomainLink(record),
153
+ );
62
154
  }
63
155
 
64
156
  /**
@@ -69,40 +161,62 @@ export class IntegrationLinkService {
69
161
  targetEntityType: string,
70
162
  targetEntityId: string,
71
163
  ): Promise<IntegrationLink[]> {
72
- return this.prisma.integrationLink.findMany({
73
- where: {
74
- targetModule,
75
- targetEntityType,
76
- targetEntityId,
77
- },
164
+ const { delegate, useSnakeCase } = this.resolvePersistence();
165
+ const records = await delegate.findMany({
166
+ where: useSnakeCase
167
+ ? {
168
+ target_module: targetModule,
169
+ target_entity_type: targetEntityType,
170
+ target_entity_id: targetEntityId,
171
+ }
172
+ : {
173
+ targetModule,
174
+ targetEntityType,
175
+ targetEntityId,
176
+ },
78
177
  });
178
+
179
+ return records.map((record: IntegrationLinkRecord) =>
180
+ this.toDomainLink(record),
181
+ );
79
182
  }
80
183
 
81
184
  /**
82
185
  * Find links of a specific type
83
186
  */
84
187
  async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]> {
85
- return this.prisma.integrationLink.findMany({
86
- where: { linkType },
188
+ const { delegate, useSnakeCase } = this.resolvePersistence();
189
+ const records = await delegate.findMany({
190
+ where: useSnakeCase ? { link_type: linkType } : { linkType },
87
191
  });
192
+
193
+ return records.map((record: IntegrationLinkRecord) =>
194
+ this.toDomainLink(record),
195
+ );
88
196
  }
89
197
 
90
198
  /**
91
199
  * Delete a link
92
200
  */
93
201
  async deleteLink(linkId: string): Promise<IntegrationLink> {
94
- return this.prisma.integrationLink.delete({
95
- where: { id: linkId },
202
+ const { delegate } = this.resolvePersistence();
203
+ const record = await delegate.delete({
204
+ where: { id: this.toDatabaseId(linkId) },
96
205
  });
206
+
207
+ return this.toDomainLink(record as IntegrationLinkRecord);
97
208
  }
98
209
 
99
210
  /**
100
211
  * Get link by ID
101
212
  */
102
213
  async getById(linkId: string): Promise<IntegrationLink | null> {
103
- return this.prisma.integrationLink.findUnique({
104
- where: { id: linkId },
214
+ const { delegate } = this.resolvePersistence();
215
+ const record = await delegate.findUnique({
216
+ where: { id: this.toDatabaseId(linkId) },
105
217
  });
218
+
219
+ return record ? this.toDomainLink(record as IntegrationLinkRecord) : null;
106
220
  }
107
221
 
108
222
  /**
@@ -116,39 +230,60 @@ export class IntegrationLinkService {
116
230
  entity2Type: string,
117
231
  entity2Id: string,
118
232
  ): Promise<IntegrationLink | null> {
119
- // Look for forward or reverse direction
120
- const link = await this.prisma.integrationLink.findFirst({
233
+ const { delegate, useSnakeCase } = this.resolvePersistence();
234
+
235
+ const record = await delegate.findFirst({
121
236
  where: {
122
- OR: [
123
- {
124
- sourceModule: module1,
125
- sourceEntityType: entity1Type,
126
- sourceEntityId: entity1Id,
127
- targetModule: module2,
128
- targetEntityType: entity2Type,
129
- targetEntityId: entity2Id,
130
- },
131
- {
132
- sourceModule: module2,
133
- sourceEntityType: entity2Type,
134
- sourceEntityId: entity2Id,
135
- targetModule: module1,
136
- targetEntityType: entity1Type,
137
- targetEntityId: entity1Id,
138
- },
139
- ],
237
+ OR: useSnakeCase
238
+ ? [
239
+ {
240
+ source_module: module1,
241
+ source_entity_type: entity1Type,
242
+ source_entity_id: entity1Id,
243
+ target_module: module2,
244
+ target_entity_type: entity2Type,
245
+ target_entity_id: entity2Id,
246
+ },
247
+ {
248
+ source_module: module2,
249
+ source_entity_type: entity2Type,
250
+ source_entity_id: entity2Id,
251
+ target_module: module1,
252
+ target_entity_type: entity1Type,
253
+ target_entity_id: entity1Id,
254
+ },
255
+ ]
256
+ : [
257
+ {
258
+ sourceModule: module1,
259
+ sourceEntityType: entity1Type,
260
+ sourceEntityId: entity1Id,
261
+ targetModule: module2,
262
+ targetEntityType: entity2Type,
263
+ targetEntityId: entity2Id,
264
+ },
265
+ {
266
+ sourceModule: module2,
267
+ sourceEntityType: entity2Type,
268
+ sourceEntityId: entity2Id,
269
+ targetModule: module1,
270
+ targetEntityType: entity1Type,
271
+ targetEntityId: entity1Id,
272
+ },
273
+ ],
140
274
  },
141
275
  });
142
276
 
143
- return link;
277
+ return record ? this.toDomainLink(record as IntegrationLinkRecord) : null;
144
278
  }
145
279
 
146
280
  /**
147
281
  * Count links from a module
148
282
  */
149
283
  async countFromModule(sourceModule: string): Promise<number> {
150
- return this.prisma.integrationLink.count({
151
- where: { sourceModule },
284
+ const { delegate, useSnakeCase } = this.resolvePersistence();
285
+ return delegate.count({
286
+ where: useSnakeCase ? { source_module: sourceModule } : { sourceModule },
152
287
  });
153
288
  }
154
289
  }
@@ -10,8 +10,10 @@ import {
10
10
  Injectable,
11
11
  Logger,
12
12
  NotFoundException,
13
+ OnModuleInit,
13
14
  } from '@nestjs/common';
14
15
  import * as Handlebars from 'handlebars';
16
+ import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
15
17
  import { SettingService } from '../setting/setting.service';
16
18
  import { CreateDTO } from './dto/create.dto';
17
19
  import { ImportDTO } from './dto/import.dto';
@@ -20,7 +22,7 @@ import { TestMailDTO } from './dto/test-mail.dto';
20
22
  import { UpdateDTO } from './dto/update.dto';
21
23
 
22
24
  @Injectable()
23
- export class MailService {
25
+ export class MailService implements OnModuleInit {
24
26
  private readonly logger = new Logger(MailService.name);
25
27
  private readonly modelName = 'mail';
26
28
 
@@ -33,8 +35,70 @@ export class MailService {
33
35
  private readonly localeService: LocaleService,
34
36
  @Inject(forwardRef(() => SettingService))
35
37
  private readonly setting: SettingService,
38
+ @Inject(forwardRef(() => IntegrationDeveloperApiService))
39
+ private readonly integrationApi: IntegrationDeveloperApiService,
36
40
  ) { }
37
41
 
42
+ onModuleInit(): void {
43
+ this.integrationApi.subscribe({
44
+ eventName: 'core.setting.changed',
45
+ consumerName: 'core.mail-config-validator',
46
+ priority: 0,
47
+ handler: async (event) => {
48
+ const slug = String(event.payload?.slug || '').trim();
49
+
50
+ if (!this.isMailConfigSlug(slug)) {
51
+ return;
52
+ }
53
+
54
+ await this.syncConfiguredMailFlag();
55
+ },
56
+ });
57
+
58
+ void this.syncConfiguredMailFlag();
59
+ }
60
+
61
+ private isMailConfigSlug(slug: string) {
62
+ return slug.startsWith('mail-');
63
+ }
64
+
65
+ private isSettingEnabled(value: unknown) {
66
+ return value === true || value === 'true';
67
+ }
68
+
69
+ private async syncConfiguredMailFlag() {
70
+ try {
71
+ const settings = await this.setting.getSettingValues([
72
+ 'mail-provider',
73
+ 'mail-from',
74
+ ]);
75
+
76
+ const provider = String(settings['mail-provider'] || '').trim();
77
+ const from = String(settings['mail-from'] || '').trim();
78
+
79
+ if (!provider || !from) {
80
+ await this.setting.setValue('configured-mail', 'false');
81
+ return;
82
+ }
83
+
84
+ await this.reloadConfig();
85
+
86
+ await this.mailMainService.send({
87
+ to: from,
88
+ from,
89
+ subject: '[HedHog] Mail configuration validation',
90
+ body: `This is an automatic validation email for the configured provider "${provider}".`,
91
+ });
92
+
93
+ await this.setting.setValue('configured-mail', 'true');
94
+ } catch (error) {
95
+ this.logger.warn(
96
+ `Mail configuration validation failed: ${error instanceof Error ? error.message : String(error)}`,
97
+ );
98
+ await this.setting.setValue('configured-mail', 'false');
99
+ }
100
+ }
101
+
38
102
  /**
39
103
  * Reload mail configuration from database and update MailMainService
40
104
  */
@@ -333,7 +397,7 @@ export class MailService {
333
397
  'mail-from'
334
398
  ]);
335
399
 
336
- if (!settings['configured-mail']) {
400
+ if (!this.isSettingEnabled(settings['configured-mail'])) {
337
401
  this.logger.warn('Mail service is not configured. Aborting sending email.');
338
402
  return;
339
403
  }
@@ -422,7 +486,7 @@ export class MailService {
422
486
  'mail-from'
423
487
  ]);
424
488
 
425
- if (!settings['configured-mail']) {
489
+ if (!this.isSettingEnabled(settings['configured-mail'])) {
426
490
  throw new BadRequestException('Mail service is not configured.');
427
491
  }
428
492