-
Notifications
You must be signed in to change notification settings - Fork 19
Issue #243540 feat: Email notification using SenGrid #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
130b2cb
f72d034
e625528
71e8ea5
a78414d
b6ef453
fff30b3
dda2006
1a70558
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,8 @@ import { NotificationLog } from "../entity/notificationLogs.entity"; | |||||||||
| import { NotificationService } from "../notification.service"; | ||||||||||
| import { LoggerUtil } from "src/common/logger/LoggerUtil"; | ||||||||||
| import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; | ||||||||||
| import { ConfigService } from "@nestjs/config"; | ||||||||||
| import * as sgMail from '@sendgrid/mail'; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Interface for raw email data | ||||||||||
|
|
@@ -24,10 +26,44 @@ export interface RawEmailData { | |||||||||
|
|
||||||||||
| @Injectable() | ||||||||||
| export class EmailAdapter implements NotificationServiceInterface { | ||||||||||
| private provider: 'smtp' | 'sendgrid' = 'smtp'; | ||||||||||
| private isConfigured = false; | ||||||||||
| constructor( | ||||||||||
| @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService | ||||||||||
| ) { } | ||||||||||
| @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService, | ||||||||||
| private readonly configService: ConfigService | ||||||||||
| ) { | ||||||||||
| this.provider = (this.configService.get('EMAIL_PROVIDER') || 'smtp') as 'smtp' | 'sendgrid'; | ||||||||||
| if (this.provider === 'smtp' && this.hasSmtpConfig()) { | ||||||||||
| this.isConfigured = true; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (this.provider === 'sendgrid' && this.hasSendGridConfig()) { | ||||||||||
| sgMail.setApiKey(this.configService.get('SENDGRID_API_KEY')); | ||||||||||
| this.isConfigured = true; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!this.isConfigured) { | ||||||||||
| console.warn('EmailAdapter not configured: Missing required settings.'); | ||||||||||
| LoggerUtil.error(ERROR_MESSAGES.EMAIL_ADAPTER_NOT_CONFIGURED); | ||||||||||
|
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Inconsistent logging approach Using both -console.warn('EmailAdapter not configured: Missing required settings.');
-LoggerUtil.error(ERROR_MESSAGES.EMAIL_ADAPTER_NOT_CONFIGURED);
+LoggerUtil.warn('EmailAdapter not configured: Missing required settings.');
+LoggerUtil.error(ERROR_MESSAGES.EMAIL_ADAPTER_NOT_CONFIGURED);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
| } | ||||||||||
|
Comment on lines
+46
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add guard check for adapter configuration The adapter continues to work even when not configured properly. Consider adding a guard check in the send methods. Would you like me to generate code that adds initialization checks to all public methods to prevent operations when the adapter is not properly configured? 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
|
|
||||||||||
| private hasSmtpConfig(): boolean { | ||||||||||
| return ( | ||||||||||
| !!this.configService.get('EMAIL_HOST') && | ||||||||||
| !!this.configService.get('EMAIL_PORT') && | ||||||||||
| !!this.configService.get('EMAIL_USER') && | ||||||||||
| !!this.configService.get('EMAIL_PASS') | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private hasSendGridConfig(): boolean { | ||||||||||
| return !!( | ||||||||||
| this.configService.get('SENDGRID_API_KEY') && | ||||||||||
| this.configService.get('EMAIL_FROM') | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| /** | ||||||||||
| * Sends notifications using template-based approach | ||||||||||
| * @param notificationDataArray Array of notification data objects | ||||||||||
|
|
@@ -155,7 +191,7 @@ export class EmailAdapter implements NotificationServiceInterface { | |||||||||
| email: { | ||||||||||
| providers: [ | ||||||||||
| { | ||||||||||
| type: process.env.EMAIL_TYPE, | ||||||||||
| type: process.env.EMAIL_PROVIDER || 'smtp', | ||||||||||
| host: process.env.EMAIL_HOST, | ||||||||||
| port: process.env.EMAIL_PORT, | ||||||||||
| secure: false, | ||||||||||
|
|
@@ -186,17 +222,34 @@ export class EmailAdapter implements NotificationServiceInterface { | |||||||||
| */ | ||||||||||
| async send(notificationData) { | ||||||||||
| const notificationLogs = this.createNotificationLog(notificationData, notificationData.subject, notificationData.key, notificationData.body, notificationData.recipient); | ||||||||||
| let result; | ||||||||||
| try { | ||||||||||
| const emailConfig = this.getEmailConfig(notificationData.context); | ||||||||||
| const notifmeSdk = new NotifmeSdk(emailConfig); | ||||||||||
| const result = await notifmeSdk.send({ | ||||||||||
| email: { | ||||||||||
| from: emailConfig.email.from, | ||||||||||
| if (this.provider === 'smtp') { | ||||||||||
| const emailConfig = this.getEmailConfig(notificationData.context); | ||||||||||
| const notifmeSdk = new NotifmeSdk(emailConfig); | ||||||||||
| result = await notifmeSdk.send({ | ||||||||||
| email: { | ||||||||||
| from: emailConfig.email.from, | ||||||||||
| to: notificationData.recipient, | ||||||||||
| subject: notificationData.subject, | ||||||||||
| html: notificationData.body, | ||||||||||
| }, | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| else if (this.provider === 'sendgrid') { | ||||||||||
| const msg = { | ||||||||||
| to: notificationData.recipient, | ||||||||||
| from: this.configService.get('EMAIL_FROM'), | ||||||||||
| subject: notificationData.subject, | ||||||||||
| html: notificationData.body, | ||||||||||
| }, | ||||||||||
| }); | ||||||||||
| }; | ||||||||||
| const sgResponse = await sgMail.send(msg); | ||||||||||
| result = { | ||||||||||
| status: sgResponse[0]?.statusCode === 202 ? 'success' : 'error', | ||||||||||
| id: sgResponse[0]?.headers['x-message-id'] || null, | ||||||||||
| errors: sgResponse[0]?.statusCode !== 202 ? sgResponse[0]?.body : undefined, | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| if (result.status === 'success') { | ||||||||||
| notificationLogs.status = true; | ||||||||||
| await this.notificationServices.saveNotificationLogs(notificationLogs); | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,29 +7,47 @@ import { NotificationLog } from "../entity/notificationLogs.entity"; | |||||||||||||||||||||||||||||||
| import { NotificationService } from "../notification.service"; | ||||||||||||||||||||||||||||||||
| import { ERROR_MESSAGES, SUCCESS_MESSAGES } from "src/common/utils/constant.util"; | ||||||||||||||||||||||||||||||||
| import { LoggerUtil } from "src/common/logger/LoggerUtil"; | ||||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||||
| export class PushAdapter implements NotificationServiceInterface { | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainMissing @Injectable() decorator The 🏁 Script executed: #!/bin/bash
# Check if PushAdapter is being injected anywhere in the codebase
ast-grep --pattern 'PushAdapter' | head -20
rg -A 5 "PushAdapter" --type tsLength of output: 6813 Add NestJS requires classes registered in module • File: import { ConfigService } from '@nestjs/config';
+import { Injectable, Inject, forwardRef } from '@nestjs/common';
+@Injectable()
export class PushAdapter implements NotificationServiceInterface {
private readonly fcmurl: string;
private isInitialized = false;
constructor(
@Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService,
private readonly configService: ConfigService
) { … }Ensure you import and apply 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| private readonly fcmurl: string; | ||||||||||||||||||||||||||||||||
| private isInitialized = false; | ||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||
| @Inject(forwardRef(() => NotificationService)) private readonly notificationServices: NotificationService, | ||||||||||||||||||||||||||||||||
| private readonly configService: ConfigService | ||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||
| this.fcmurl = this.configService.get('FCM_URL'); | ||||||||||||||||||||||||||||||||
| if (this.hasRequiredConfig()) { | ||||||||||||||||||||||||||||||||
| //Initialize Firebase Admin SDK with environment variables | ||||||||||||||||||||||||||||||||
| const serviceAccount = { | ||||||||||||||||||||||||||||||||
| projectId: this.configService.get('FIREBASE_PROJECT_ID'), | ||||||||||||||||||||||||||||||||
| clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'), | ||||||||||||||||||||||||||||||||
| privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), // Replace escaped newlines | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Initialize Firebase Admin SDK with environment variables | ||||||||||||||||||||||||||||||||
| const serviceAccount = { | ||||||||||||||||||||||||||||||||
| projectId: this.configService.get('FIREBASE_PROJECT_ID'), | ||||||||||||||||||||||||||||||||
| clientEmail: this.configService.get('FIREBASE_CLIENT_EMAIL'), | ||||||||||||||||||||||||||||||||
| privateKey: this.configService.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'), // Replace escaped newlines | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (admin.apps.length === 0) { | ||||||||||||||||||||||||||||||||
| admin.initializeApp({ | ||||||||||||||||||||||||||||||||
| credential: admin.credential.cert(serviceAccount), | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| if (admin.apps.length === 0) { | ||||||||||||||||||||||||||||||||
| admin.initializeApp({ | ||||||||||||||||||||||||||||||||
| credential: admin.credential.cert(serviceAccount), | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| this.isInitialized = true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||||||
| console.warn('PushAdapter not initialized: Missing Firebase config.'); | ||||||||||||||||||||||||||||||||
| LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Inconsistent logging approach Using both -console.warn('PushAdapter not initialized: Missing Firebase config.');
-LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED);
+LoggerUtil.warn('PushAdapter not initialized: Missing Firebase config.');
+LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private hasRequiredConfig(): boolean { | ||||||||||||||||||||||||||||||||
| return !!( | ||||||||||||||||||||||||||||||||
| this.configService.get('FIREBASE_PROJECT_ID') && | ||||||||||||||||||||||||||||||||
| this.configService.get('FIREBASE_CLIENT_EMAIL') && | ||||||||||||||||||||||||||||||||
| this.configService.get('FIREBASE_PRIVATE_KEY') | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| async sendNotification(notificationDataArray) { | ||||||||||||||||||||||||||||||||
| if (!this.isInitialized) { | ||||||||||||||||||||||||||||||||
| LoggerUtil.error(ERROR_MESSAGES.PUSH_ADAPTER_NOT_INITIALIZED); | ||||||||||||||||||||||||||||||||
| throw new Error('PushAdapter is not initialized. Check Firebase configuration.'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const results = []; | ||||||||||||||||||||||||||||||||
| for (const notificationData of notificationDataArray) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,13 +28,12 @@ export class NotificationActionTemplates { | |
| @Column({ type: 'uuid' }) | ||
| createdBy: string; | ||
|
|
||
| @Column() | ||
| @Column({ nullable: true }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainLGTM! Schema changes add appropriate flexibility. Making the Ensure that a database migration is created for this schema change to avoid runtime issues: Also applies to: 34-34 🏁 Script executed: #!/bin/bash
# Description: Check if database migration files exist for this schema change
# Expected: Migration files should exist for nullable column changes
echo "Checking for recent migration files..."
fd -t f -e ts -e js migration | head -10
echo "Checking for nullable column changes in migrations..."
rg -A 3 -B 3 "nullable.*true" --type tsLength of output: 7478 Add missing database migration for nullable columns The schema changes to make Action items:
Files needing attention:
🤖 Prompt for AI Agents |
||
| image: string; | ||
|
|
||
| @Column() | ||
| @Column({ nullable: true }) | ||
| link: string; | ||
|
|
||
|
|
||
| @Column({ type: 'uuid', nullable: true }) | ||
| updatedBy: string; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version mismatch between runtime and dev dependencies
NestJS runtime packages are upgraded to v10, but dev dependencies (@nestjs/cli, @nestjs/schematics) remain at v8. This version mismatch could cause compatibility issues during development and testing.
Update dev dependencies to match:
📝 Committable suggestion
🤖 Prompt for AI Agents