From 3bfb60a168fcc92abdd8fd07bca75cc9615a42b4 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Tue, 25 Nov 2025 19:47:57 +0530 Subject: [PATCH 01/23] feat: alert via email while ledgerId gets set to null Signed-off-by: Krishna Waske --- libs/common/src/dtos/email.dto.ts | 26 ++++---- .../migration.sql | 21 ++++++ .../src/prisma-service.module.ts | 5 +- .../src/prisma-service.service.ts | 65 ++++++++++++++++++- package.json | 2 +- pnpm-lock.yaml | 7 +- 6 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20251125062851_add_ledger_null_trigger/migration.sql diff --git a/libs/common/src/dtos/email.dto.ts b/libs/common/src/dtos/email.dto.ts index e395da884..58bc44cd2 100644 --- a/libs/common/src/dtos/email.dto.ts +++ b/libs/common/src/dtos/email.dto.ts @@ -1,17 +1,17 @@ export class EmailDto { - emailFrom: string; - emailTo: string; - emailSubject: string; - emailText: string; - emailHtml: string; - emailAttachments?: AttachmentJSON[]; + emailFrom: string; + emailTo: string | string[]; + emailSubject: string; + emailText: string; + emailHtml: string; + emailAttachments?: AttachmentJSON[]; } interface AttachmentJSON { - content: string; - filename: string; - contentType: string; - type?: string; - disposition?: string; - content_id?: string; - } \ No newline at end of file + content: string; + filename: string; + contentType: string; + type?: string; + disposition?: string; + content_id?: string; +} diff --git a/libs/prisma-service/prisma/migrations/20251125062851_add_ledger_null_trigger/migration.sql b/libs/prisma-service/prisma/migrations/20251125062851_add_ledger_null_trigger/migration.sql new file mode 100644 index 000000000..8c0e1073b --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251125062851_add_ledger_null_trigger/migration.sql @@ -0,0 +1,21 @@ +-- Create the function +CREATE OR REPLACE FUNCTION alert_ledger_null() +RETURNS trigger AS $$ +BEGIN + IF NEW."ledgerId" IS NULL THEN + PERFORM pg_notify('ledger_null', json_build_object( + 'agentId', NEW.id, + 'orgId', NEW."orgId", + 'timestamp', now() + )::text); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +CREATE TRIGGER ledger_null_trigger +AFTER UPDATE ON org_agents +FOR EACH ROW +WHEN (NEW."ledgerId" IS NULL AND OLD."ledgerId" IS NOT NULL) +EXECUTE FUNCTION alert_ledger_null(); diff --git a/libs/prisma-service/src/prisma-service.module.ts b/libs/prisma-service/src/prisma-service.module.ts index 2441ae4ad..612e93094 100644 --- a/libs/prisma-service/src/prisma-service.module.ts +++ b/libs/prisma-service/src/prisma-service.module.ts @@ -1,8 +1,11 @@ import { Logger, Module } from '@nestjs/common'; import { PrismaService } from './prisma-service.service'; +import { CommonModule } from '@credebl/common'; +import { EmailService } from '@credebl/common/email.service'; @Module({ - providers: [PrismaService, Logger], + imports: [CommonModule], + providers: [PrismaService, Logger, EmailService], exports: [PrismaService] }) export class PrismaServiceModule {} diff --git a/libs/prisma-service/src/prisma-service.service.ts b/libs/prisma-service/src/prisma-service.service.ts index 1a828eefd..a0b8e1694 100644 --- a/libs/prisma-service/src/prisma-service.service.ts +++ b/libs/prisma-service/src/prisma-service.service.ts @@ -1,11 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { EmailService } from '@credebl/common/email.service'; import { INestApplication, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Prisma, PrismaClient } from '@prisma/client'; +import { Client as PgClient } from 'pg'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { private readonly logger = new Logger('PrismaService'); private prismaLogs: string[]; + private pg: PgClient; + private lastAlertTime: number | null = null; + private readonly emailService = new EmailService(); private enable = (type: string): boolean => this.prismaLogs.includes(type); @@ -22,9 +27,12 @@ export class PrismaService extends PrismaClient implements OnModuleInit { .toLowerCase() .split(',') .map((l) => l.trim()); + + this.pg = new PgClient({ + connectionString: process.env.DATABASE_URL + }); } - UserDevicesRepository: any; async onModuleInit(): Promise { await this.$connect(); if (this.enable('error')) { @@ -48,8 +56,63 @@ export class PrismaService extends PrismaClient implements OnModuleInit { this.logger.debug(`${e.timestamp}ms`, '(Prisma Query) Timestamp:'); }); } + + await this.pg.connect(); + + this.pg.on('notification', async (msg) => { + if ('ledger_null' === msg.channel) { + await this.handleLedgerAlert(); + } + }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async enableShutdownHooks(app: INestApplication): Promise {} + + async onModuleDestroy(): Promise { + await this.pg?.end(); + } + + private async handleLedgerAlert(): Promise { + // Step 1: Count total records + const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); + const total = Number(totalRes.rows[0].count); + + // Step 2: Count NULL ledgerId records + const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); + const nullCount = Number(nullRes.rows[0].count); + + // Step 3: Calculate % + const percent = (nullCount / total) * 100; + + // Condition: > 30% + if (30 >= percent) { + return; + } + + // Avoid spamming: send only once every 2 hours + const now = Date.now(); + if (this.lastAlertTime && now - this.lastAlertTime < 2 * 60 * 60 * 1000) { + return; + } + this.lastAlertTime = now; + + const alertEmails = + process.env.DB_ALERT_EMAILS?.split(',') + .map((e) => e.trim()) + .filter((e) => 0 < e.length) || []; + + // Step 4: Send Email + await this.emailService.sendEmail({ + emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, + emailTo: alertEmails, + emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', + emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, + emailHtml: `

ALERT: ${percent.toFixed( + 2 + )}% of org_agents have ledgerId = NULL.

` + }); + + this.logger.log('ALERT EMAIL SENT, ledgerId WAS SET TO NULL'); + } } diff --git a/package.json b/package.json index 418a81be2..c67063f19 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "passport-local": "^1.0.0", "path": "^0.12.7", "pdfkit": "^0.13.0", - "pg": "^8.11.2", + "pg": "^8.16.3", "puppeteer": "^21.5.0", "qrcode": "^1.5.3", "qs": "^6.11.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be044baf..f3575872d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,7 +318,7 @@ importers: specifier: ^0.13.0 version: 0.13.0 pg: - specifier: ^8.11.2 + specifier: ^8.16.3 version: 8.16.3 puppeteer: specifier: ^21.5.0 @@ -1789,6 +1789,7 @@ packages: '@smithy/core@3.18.0': resolution: {integrity: sha512-vGSDXOJFZgOPTatSI1ly7Gwyy/d/R9zh2TO3y0JZ0uut5qQ88p9IaWaZYIWSSqtdekNM4CGok/JppxbAff4KcQ==} engines: {node: '>=18.0.0'} + deprecated: Please upgrade your lockfile to use the latest 3.x version of @smithy/core for various fixes, see https://github.com/smithy-lang/smithy-typescript/blob/main/packages/core/CHANGELOG.md '@smithy/credential-provider-imds@4.2.5': resolution: {integrity: sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==} @@ -5451,12 +5452,12 @@ packages: puppeteer@21.0.1: resolution: {integrity: sha512-KTjmSdPZ6bMkq3EbAzAUhcB3gMDXvdwd6912rxG9hNtjwRJzHSA568vh6vIbO2WQeNmozRdt1LtiUMLSWfeMrg==} engines: {node: '>=16.3.0'} - deprecated: < 22.8.2 is no longer supported + deprecated: < 24.10.2 is no longer supported puppeteer@21.11.0: resolution: {integrity: sha512-9jTHuYe22TD3sNxy0nEIzC7ZrlRnDgeX3xPkbS7PnbdwYjl2o/z/YuCrRBwezdKpbTDTJ4VqIggzNyeRcKq3cg==} engines: {node: '>=16.13.2'} - deprecated: < 22.8.2 is no longer supported + deprecated: < 24.10.2 is no longer supported hasBin: true pure-rand@6.1.0: From c9beeb3939e483728e4342e600ccbff6b46216e5 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Tue, 25 Nov 2025 19:49:25 +0530 Subject: [PATCH 02/23] feat: alert via email while ledgerId gets set to null Signed-off-by: Krishna Waske --- .env.demo | 1 + .env.sample | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.env.demo b/.env.demo index c9cd8c829..2cf41aeb2 100644 --- a/.env.demo +++ b/.env.demo @@ -223,3 +223,4 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Default set to error PRISMA_LOGS = error +# DB_ALERT_EMAILS= \ No newline at end of file diff --git a/.env.sample b/.env.sample index cb422b084..4ac06addc 100644 --- a/.env.sample +++ b/.env.sample @@ -242,3 +242,6 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Ideally should have only error or warn enabled. Having query enabled can add a lot of unwanted logging for all types of queries being run # PRISMA_LOGS = error,warn,query + +#Comma separated emails that needs to be alerted in case the 'ledgerId' is set to null +# DB_ALERT_EMAILS= \ No newline at end of file From 3e6f9ebef2d99e3bc5bcf0e6b936e1c3a4c0002d Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Wed, 26 Nov 2025 11:05:45 +0530 Subject: [PATCH 03/23] fix: add pg query listen Signed-off-by: Krishna Waske --- libs/prisma-service/src/prisma-service.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/prisma-service/src/prisma-service.service.ts b/libs/prisma-service/src/prisma-service.service.ts index a0b8e1694..c9d3d9efb 100644 --- a/libs/prisma-service/src/prisma-service.service.ts +++ b/libs/prisma-service/src/prisma-service.service.ts @@ -59,6 +59,9 @@ export class PrismaService extends PrismaClient implements OnModuleInit { await this.pg.connect(); + // Listen to the notification channel + await this.pg.query('LISTEN ledger_null'); + this.pg.on('notification', async (msg) => { if ('ledger_null' === msg.channel) { await this.handleLedgerAlert(); From cb3bbaa2a86a9c2b647831ece3bd64b624aeb3b2 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:18:23 +0530 Subject: [PATCH 04/23] fix: multiple event listner handle Signed-off-by: Krishna Waske --- .../src/utilities/utilities.service.ts | 69 +++++++++- apps/ledger/src/ledger.module.ts | 20 ++- apps/utility/src/utilities.controller.ts | 14 +++ apps/utility/src/utilities.service.ts | 118 +++++++++++------- libs/common/src/resend-helper-file.ts | 2 +- libs/org-roles/src/org-roles.module.ts | 5 +- .../src/prisma-service.module.ts | 3 +- .../src/prisma-service.service.ts | 76 ++--------- 8 files changed, 174 insertions(+), 133 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 58f3f5cd7..6657bdaf1 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -3,14 +3,81 @@ import { BaseService } from 'libs/service/base.service'; import { StoreObjectDto, UtilitiesDto } from './dtos/shortening-url.dto'; import { NATSClient } from '@credebl/common/NATSClient'; import { ClientProxy } from '@nestjs/microservices'; +import { Client as PgClient } from 'pg'; @Injectable() export class UtilitiesService extends BaseService { + private pg: PgClient; + constructor( @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, private readonly natsClient: NATSClient ) { - super('OrganizationService'); + super('UtilitiesService'); + this.pg = new PgClient({ + connectionString: process.env.DATABASE_URL + }); + } + + async onModuleInit(): Promise { + await this.pg.connect(); + + // Listen to the notification channel + await this.pg.query('LISTEN ledger_null'); + + // NATS is not available → skip silently + this.pg.on('notification', async (msg) => { + if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLocaleLowerCase()) { + // in case it is not enabled, return + return; + } + + if ('ledger_null' === msg.channel) { + // Step 1: Count total records + const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); + const total = Number(totalRes.rows[0].count); + + // Step 2: Count NULL ledgerId records + const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); + const nullCount = Number(nullRes.rows[0].count); + + // Step 3: Calculate % + const percent = (nullCount / total) * 100; + + // Condition: > 30% + if (30 >= percent) { + return; + } + + const alertEmails = + process.env.DB_ALERT_EMAILS?.split(',') + .map((e) => e.trim()) + .filter((e) => 0 < e.length) || []; + + const emailDto = { + emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, + emailTo: alertEmails, + emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', + emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, + emailHtml: `

ALERT: ${percent.toFixed( + 2 + )}% of org_agents have ledgerId = NULL.

` + }; + + try { + const result = await this.natsClient.sendNatsMessage(this.serviceProxy, 'alert-db-ledgerId-null', { + emailDto + }); + this.logger.debug('Received result', JSON.stringify(result, null, 2)); + } catch (err) { + this.logger.error(err?.message ?? 'Some error occurred while sending prisma ledgerId alert email'); + } + } + }); + } + + async onModuleDestroy(): Promise { + await this.pg?.end(); } async createShorteningUrl(shorteningUrlDto: UtilitiesDto): Promise { diff --git a/apps/ledger/src/ledger.module.ts b/apps/ledger/src/ledger.module.ts index ed5daa043..cdb5779d2 100644 --- a/apps/ledger/src/ledger.module.ts +++ b/apps/ledger/src/ledger.module.ts @@ -2,7 +2,7 @@ import { Logger, Module } from '@nestjs/common'; import { LedgerController } from './ledger.controller'; import { LedgerService } from './ledger.service'; import { SchemaModule } from './schema/schema.module'; -import { PrismaService } from '@credebl/prisma-service'; +import { PrismaServiceModule } from '@credebl/prisma-service'; import { CredentialDefinitionModule } from './credential-definition/credential-definition.module'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { LedgerRepository } from './repositories/ledger.repository'; @@ -16,23 +16,21 @@ import { ContextInterceptorModule } from '@credebl/context/contextInterceptorMod @Module({ imports: [ GlobalConfigModule, - LoggerModule, PlatformConfig, ContextInterceptorModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, ClientsModule.register([ { name: 'NATS_CLIENT', transport: Transport.NATS, options: getNatsOptions(CommonConstants.LEDGER_SERVICE, process.env.LEDGER_NKEY_SEED) - } ]), - SchemaModule, CredentialDefinitionModule + SchemaModule, + CredentialDefinitionModule, + PrismaServiceModule ], controllers: [LedgerController], - providers: [ - LedgerService, - PrismaService, - LedgerRepository, - Logger - ] + providers: [LedgerService, LedgerRepository, Logger] }) -export class LedgerModule { } +export class LedgerModule {} diff --git a/apps/utility/src/utilities.controller.ts b/apps/utility/src/utilities.controller.ts index f9199625d..68a2775fc 100644 --- a/apps/utility/src/utilities.controller.ts +++ b/apps/utility/src/utilities.controller.ts @@ -2,6 +2,7 @@ import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { UtilitiesService } from './utilities.service'; import { IShorteningUrlData } from '../interfaces/shortening-url.interface'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; @Controller() export class UtilitiesController { @@ -30,4 +31,17 @@ export class UtilitiesController { throw new Error('Error occured in Utility Microservices Controller'); } } + + @MessagePattern({ cmd: 'alert-db-ledgerId-null' }) + async handleLedgerAlert(payload: { emailDto: EmailDto }): Promise { + try { + this.logger.debug('Received msg in alert-db-service'); + const result = await this.utilitiesService.handleLedgerAlert(payload.emailDto); + this.logger.debug('Received result in alert-db-service'); + return result; + } catch (error) { + this.logger.error(error); + throw error; + } + } } diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index ae299f334..acba05db1 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -4,60 +4,84 @@ import { UtilitiesRepository } from './utilities.repository'; import { AwsService } from '@credebl/aws'; import { S3 } from 'aws-sdk'; import { v4 as uuidv4 } from 'uuid'; +import { EmailService } from '@credebl/common/email.service'; +import { EmailDto } from '@credebl/common/dtos/email.dto'; @Injectable() export class UtilitiesService { - constructor( - private readonly logger: Logger, - private readonly utilitiesRepository: UtilitiesRepository, - private readonly awsService: AwsService - ) { } - - async createAndStoreShorteningUrl(payload): Promise { - try { - const { credentialId, schemaId, credDefId, invitationUrl, attributes } = payload; - const invitationPayload = { - referenceId: credentialId, - invitationPayload: { - schemaId, - credDefId, - invitationUrl, - attributes - } - }; - await this.utilitiesRepository.saveShorteningUrl(invitationPayload); - return `${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}/invitation/qr-code/${credentialId}`; - } catch (error) { - this.logger.error(`[createAndStoreShorteningUrl] - error in create shortening url: ${JSON.stringify(error)}`); - throw new RpcException(error); + private lastAlertTime: number | null = null; + + constructor( + private readonly logger: Logger, + private readonly utilitiesRepository: UtilitiesRepository, + private readonly awsService: AwsService, + private readonly emailService: EmailService + ) {} + + async createAndStoreShorteningUrl(payload): Promise { + try { + const { credentialId, schemaId, credDefId, invitationUrl, attributes } = payload; + const invitationPayload = { + referenceId: credentialId, + invitationPayload: { + schemaId, + credDefId, + invitationUrl, + attributes } + }; + await this.utilitiesRepository.saveShorteningUrl(invitationPayload); + return `${process.env.API_GATEWAY_PROTOCOL}://${process.env.API_ENDPOINT}/invitation/qr-code/${credentialId}`; + } catch (error) { + this.logger.error(`[createAndStoreShorteningUrl] - error in create shortening url: ${JSON.stringify(error)}`); + throw new RpcException(error); } + } - async getShorteningUrl(referenceId: string): Promise { - try { - const getShorteningUrl = await this.utilitiesRepository.getShorteningUrl(referenceId); - - const getInvitationUrl = { - referenceId: getShorteningUrl.referenceId, - invitationPayload: getShorteningUrl.invitationPayload - }; - - return getInvitationUrl; - } catch (error) { - this.logger.error(`[getShorteningUrl] - error in get shortening url: ${JSON.stringify(error)}`); - throw new RpcException(error); - } + async getShorteningUrl(referenceId: string): Promise { + try { + const getShorteningUrl = await this.utilitiesRepository.getShorteningUrl(referenceId); + + const getInvitationUrl = { + referenceId: getShorteningUrl.referenceId, + invitationPayload: getShorteningUrl.invitationPayload + }; + + return getInvitationUrl; + } catch (error) { + this.logger.error(`[getShorteningUrl] - error in get shortening url: ${JSON.stringify(error)}`); + throw new RpcException(error); } + } - async storeObject(payload: {persistent: boolean, storeObj: unknown}): Promise { - try { - const uuid = uuidv4(); - const uploadResult:S3.ManagedUpload.SendData = await this.awsService.storeObject(payload.persistent, uuid, payload.storeObj); - const url: string = `${process.env.SHORTENED_URL_DOMAIN}/${uploadResult.Key}`; - return url; - } catch (error) { - this.logger.error(error); - throw new Error('An error occurred while uploading data to S3. Error::::::'); - } + async storeObject(payload: { persistent: boolean; storeObj: unknown }): Promise { + try { + const uuid = uuidv4(); + const uploadResult: S3.ManagedUpload.SendData = await this.awsService.storeObject( + payload.persistent, + uuid, + payload.storeObj + ); + const url: string = `${process.env.SHORTENED_URL_DOMAIN}/${uploadResult.Key}`; + return url; + } catch (error) { + this.logger.error(error); + throw new Error('An error occurred while uploading data to S3. Error::::::'); + } + } + + async handleLedgerAlert(emailDto: EmailDto): Promise { + // Avoid spamming: send only once every 2 hours + const now = Date.now(); + if (this.lastAlertTime && now - this.lastAlertTime < 2 * 60 * 60 * 1000) { + this.logger.log(`ALERT EMAIL ALREADY SENT at ${this.lastAlertTime}, ledgerId WAS SET TO NULL`); + return; } + this.lastAlertTime = now; + + // Send Email + await this.emailService.sendEmail(emailDto); + + this.logger.log('ALERT EMAIL SENT, ledgerId WAS SET TO NULL'); + } } diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 2beccbb0c..7eb4f6fd5 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -10,7 +10,7 @@ const apiKey = process.env.RESEND_API_KEY; if (!apiKey) { throw new Error('Missing RESEND_API_KEY in environment variables.'); } -const resend = new Resend(process.env.RESEND_API_KEY); +const resend = new Resend(apiKey); export const sendWithResend = async (emailDto: EmailDto): Promise => { try { diff --git a/libs/org-roles/src/org-roles.module.ts b/libs/org-roles/src/org-roles.module.ts index a53dff126..e2757c8b4 100644 --- a/libs/org-roles/src/org-roles.module.ts +++ b/libs/org-roles/src/org-roles.module.ts @@ -1,11 +1,12 @@ -import { PrismaService } from '@credebl/prisma-service'; +import { PrismaServiceModule } from '@credebl/prisma-service'; import { Logger } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { OrgRolesRepository } from '../repositories'; import { OrgRolesService } from './org-roles.service'; @Module({ - providers: [OrgRolesService, OrgRolesRepository, Logger, PrismaService], + imports: [PrismaServiceModule], + providers: [OrgRolesService, OrgRolesRepository, Logger], exports: [OrgRolesService] }) export class OrgRolesModule {} diff --git a/libs/prisma-service/src/prisma-service.module.ts b/libs/prisma-service/src/prisma-service.module.ts index 612e93094..c839f6d86 100644 --- a/libs/prisma-service/src/prisma-service.module.ts +++ b/libs/prisma-service/src/prisma-service.module.ts @@ -1,11 +1,10 @@ import { Logger, Module } from '@nestjs/common'; import { PrismaService } from './prisma-service.service'; import { CommonModule } from '@credebl/common'; -import { EmailService } from '@credebl/common/email.service'; @Module({ imports: [CommonModule], - providers: [PrismaService, Logger, EmailService], + providers: [PrismaService, Logger], exports: [PrismaService] }) export class PrismaServiceModule {} diff --git a/libs/prisma-service/src/prisma-service.service.ts b/libs/prisma-service/src/prisma-service.service.ts index c9d3d9efb..04e9b64d8 100644 --- a/libs/prisma-service/src/prisma-service.service.ts +++ b/libs/prisma-service/src/prisma-service.service.ts @@ -1,20 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EmailService } from '@credebl/common/email.service'; -import { INestApplication, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { INestApplication, Inject, Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; import { Prisma, PrismaClient } from '@prisma/client'; -import { Client as PgClient } from 'pg'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { private readonly logger = new Logger('PrismaService'); private prismaLogs: string[]; - private pg: PgClient; - private lastAlertTime: number | null = null; - private readonly emailService = new EmailService(); private enable = (type: string): boolean => this.prismaLogs.includes(type); - constructor() { + constructor( + @Optional() @Inject('NATS_CLIENT') private readonly prismaProxy: ClientProxy | null, + @Optional() private readonly natsClient: NATSClient + ) { super({ log: [ { level: 'query', emit: 'event' }, @@ -27,10 +27,6 @@ export class PrismaService extends PrismaClient implements OnModuleInit { .toLowerCase() .split(',') .map((l) => l.trim()); - - this.pg = new PgClient({ - connectionString: process.env.DATABASE_URL - }); } async onModuleInit(): Promise { @@ -56,66 +52,8 @@ export class PrismaService extends PrismaClient implements OnModuleInit { this.logger.debug(`${e.timestamp}ms`, '(Prisma Query) Timestamp:'); }); } - - await this.pg.connect(); - - // Listen to the notification channel - await this.pg.query('LISTEN ledger_null'); - - this.pg.on('notification', async (msg) => { - if ('ledger_null' === msg.channel) { - await this.handleLedgerAlert(); - } - }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async enableShutdownHooks(app: INestApplication): Promise {} - - async onModuleDestroy(): Promise { - await this.pg?.end(); - } - - private async handleLedgerAlert(): Promise { - // Step 1: Count total records - const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); - const total = Number(totalRes.rows[0].count); - - // Step 2: Count NULL ledgerId records - const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); - const nullCount = Number(nullRes.rows[0].count); - - // Step 3: Calculate % - const percent = (nullCount / total) * 100; - - // Condition: > 30% - if (30 >= percent) { - return; - } - - // Avoid spamming: send only once every 2 hours - const now = Date.now(); - if (this.lastAlertTime && now - this.lastAlertTime < 2 * 60 * 60 * 1000) { - return; - } - this.lastAlertTime = now; - - const alertEmails = - process.env.DB_ALERT_EMAILS?.split(',') - .map((e) => e.trim()) - .filter((e) => 0 < e.length) || []; - - // Step 4: Send Email - await this.emailService.sendEmail({ - emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, - emailTo: alertEmails, - emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', - emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, - emailHtml: `

ALERT: ${percent.toFixed( - 2 - )}% of org_agents have ledgerId = NULL.

` - }); - - this.logger.log('ALERT EMAIL SENT, ledgerId WAS SET TO NULL'); - } } From 41543fd141915e3c236cc2debc7523dbf50c5956 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:28:55 +0530 Subject: [PATCH 05/23] fix: log enabling of alerts Signed-off-by: Krishna Waske --- apps/api-gateway/src/main.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index e92468ce8..58182fceb 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -119,6 +119,14 @@ async function bootstrap(): Promise { ); app.useGlobalInterceptors(new NatsInterceptor()); await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); - Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`); + Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`, 'Success'); + + if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLocaleLowerCase()) { + // in case it is enabled, log that + Logger.log( + "We have enabled DB alert for 'ledger_null' instances. This would send email in case the 'ledger_id' column in 'org_agents' table is set to null", + 'DB alert enabled' + ); + } } bootstrap(); From f1bba47936bb44d0019758a002068fbe1f267693 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:31:05 +0530 Subject: [PATCH 06/23] fix: add env demo and sample events Signed-off-by: Krishna Waske --- .env.demo | 1 + .env.sample | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.env.demo b/.env.demo index 2cf41aeb2..49d5ef30a 100644 --- a/.env.demo +++ b/.env.demo @@ -223,4 +223,5 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Default set to error PRISMA_LOGS = error +# DB_ALERT_ENABLE= # DB_ALERT_EMAILS= \ No newline at end of file diff --git a/.env.sample b/.env.sample index 4ac06addc..626d9a264 100644 --- a/.env.sample +++ b/.env.sample @@ -243,5 +243,7 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Ideally should have only error or warn enabled. Having query enabled can add a lot of unwanted logging for all types of queries being run # PRISMA_LOGS = error,warn,query -#Comma separated emails that needs to be alerted in case the 'ledgerId' is set to null -# DB_ALERT_EMAILS= \ No newline at end of file +# Comma separated emails that needs to be alerted in case the 'ledgerId' is set to null +# DB_ALERT_EMAILS= +# Boolean: to enable/disable db alerts. This needs the 'utility' microservice +# DB_ALERT_ENABLE= \ No newline at end of file From b0513b4d1093fdc1b180d7b811ed6704eebd9719 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:34:03 +0530 Subject: [PATCH 07/23] fix: remove unwanted imports Signed-off-by: Krishna Waske --- libs/prisma-service/src/prisma-service.service.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libs/prisma-service/src/prisma-service.service.ts b/libs/prisma-service/src/prisma-service.service.ts index 04e9b64d8..f0a78eeeb 100644 --- a/libs/prisma-service/src/prisma-service.service.ts +++ b/libs/prisma-service/src/prisma-service.service.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { NATSClient } from '@credebl/common/NATSClient'; -import { INestApplication, Inject, Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common'; -import { ClientProxy } from '@nestjs/microservices'; +import { INestApplication, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Prisma, PrismaClient } from '@prisma/client'; @Injectable() @@ -11,10 +9,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit { private enable = (type: string): boolean => this.prismaLogs.includes(type); - constructor( - @Optional() @Inject('NATS_CLIENT') private readonly prismaProxy: ClientProxy | null, - @Optional() private readonly natsClient: NATSClient - ) { + constructor() { super({ log: [ { level: 'query', emit: 'event' }, From 7f75778851e6ee644ea31621664076efe3e06178 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:37:53 +0530 Subject: [PATCH 08/23] fix: change toLocaleLowercase to toLowerCase Signed-off-by: Krishna Waske --- apps/api-gateway/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 58182fceb..6a57d5a32 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -121,7 +121,7 @@ async function bootstrap(): Promise { await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`, 'Success'); - if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLocaleLowerCase()) { + if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) { // in case it is enabled, log that Logger.log( "We have enabled DB alert for 'ledger_null' instances. This would send email in case the 'ledger_id' column in 'org_agents' table is set to null", From a86e787800f36e8385ab9226a3d3bf87d4feb62b Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:40:50 +0530 Subject: [PATCH 09/23] fix: gracefully handle pg connect Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 6657bdaf1..c1d289753 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -20,10 +20,14 @@ export class UtilitiesService extends BaseService { } async onModuleInit(): Promise { - await this.pg.connect(); - - // Listen to the notification channel - await this.pg.query('LISTEN ledger_null'); + try { + await this.pg.connect(); + await this.pg.query('LISTEN ledger_null'); + this.logger.log('PostgreSQL notification listener connected'); + } catch (err) { + this.logger.error(`Failed to connect PostgreSQL listener: ${err?.message}`); + throw err; + } // NATS is not available → skip silently this.pg.on('notification', async (msg) => { From 1a56a1421b88a88ea46d3192621b45c79cbe9338 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:43:36 +0530 Subject: [PATCH 10/23] fix: increase try catch scope Signed-off-by: Krishna Waske --- .../src/utilities/utilities.service.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index c1d289753..39713a710 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -37,44 +37,44 @@ export class UtilitiesService extends BaseService { } if ('ledger_null' === msg.channel) { - // Step 1: Count total records - const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); - const total = Number(totalRes.rows[0].count); + try { + // Step 1: Count total records + const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); + const total = Number(totalRes.rows[0].count); - // Step 2: Count NULL ledgerId records - const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); - const nullCount = Number(nullRes.rows[0].count); + // Step 2: Count NULL ledgerId records + const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); + const nullCount = Number(nullRes.rows[0].count); - // Step 3: Calculate % - const percent = (nullCount / total) * 100; + // Step 3: Calculate % + const percent = (nullCount / total) * 100; - // Condition: > 30% - if (30 >= percent) { - return; - } + // Condition: > 30% + if (30 >= percent) { + return; + } - const alertEmails = - process.env.DB_ALERT_EMAILS?.split(',') - .map((e) => e.trim()) - .filter((e) => 0 < e.length) || []; + const alertEmails = + process.env.DB_ALERT_EMAILS?.split(',') + .map((e) => e.trim()) + .filter((e) => 0 < e.length) || []; - const emailDto = { - emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, - emailTo: alertEmails, - emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', - emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, - emailHtml: `

ALERT: ${percent.toFixed( - 2 - )}% of org_agents have ledgerId = NULL.

` - }; + const emailDto = { + emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, + emailTo: alertEmails, + emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', + emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, + emailHtml: `

ALERT: ${percent.toFixed( + 2 + )}% of org_agents have ledgerId = NULL.

` + }; - try { const result = await this.natsClient.sendNatsMessage(this.serviceProxy, 'alert-db-ledgerId-null', { emailDto }); this.logger.debug('Received result', JSON.stringify(result, null, 2)); } catch (err) { - this.logger.error(err?.message ?? 'Some error occurred while sending prisma ledgerId alert email'); + this.logger.error(err?.message ?? 'Error in ledgerId alert handler'); } } }); From 0c0fd05be3e9b89de5176704ec4917f12f9e06c8 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:46:08 +0530 Subject: [PATCH 11/23] fix: handle missing env variables gracefully Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 39713a710..5c7359d5e 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -59,6 +59,17 @@ export class UtilitiesService extends BaseService { .map((e) => e.trim()) .filter((e) => 0 < e.length) || []; + if (0 === alertEmails.length) { + this.logger.warn('DB_ALERT_EMAILS is empty, skipping alert'); + return; + } + + // TODO: Check if the to email is actually this or we need to take it from DB + if (!process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL) { + this.logger.warn('PUBLIC_PLATFORM_SUPPORT_EMAIL not configured, skipping alert'); + return; + } + const emailDto = { emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, emailTo: alertEmails, From 699dd5b061c59f4f3b5321da05b207db0e86fb12 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:47:00 +0530 Subject: [PATCH 12/23] fix: handle positive scenario properly Signed-off-by: Krishna Waske --- apps/utility/src/utilities.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index acba05db1..a3c5c553a 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -77,10 +77,10 @@ export class UtilitiesService { this.logger.log(`ALERT EMAIL ALREADY SENT at ${this.lastAlertTime}, ledgerId WAS SET TO NULL`); return; } - this.lastAlertTime = now; // Send Email await this.emailService.sendEmail(emailDto); + this.lastAlertTime = now; this.logger.log('ALERT EMAIL SENT, ledgerId WAS SET TO NULL'); } From ca1d597d42af6321e236462283ff958e1689142d Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:51:21 +0530 Subject: [PATCH 13/23] fix: handle positive scenario properly Signed-off-by: Krishna Waske --- .../src/utilities/utilities.service.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 5c7359d5e..a3b3acc22 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -14,13 +14,21 @@ export class UtilitiesService extends BaseService { private readonly natsClient: NATSClient ) { super('UtilitiesService'); - this.pg = new PgClient({ - connectionString: process.env.DATABASE_URL - }); + if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase() && !process.env.DATABASE_URL) { + throw new Error('DATABASE_URL environment variable is required'); + } else { + this.pg = new PgClient({ + connectionString: process.env.DATABASE_URL + }); + } } async onModuleInit(): Promise { try { + if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) { + // in case it is not enabled, return + return; + } await this.pg.connect(); await this.pg.query('LISTEN ledger_null'); this.logger.log('PostgreSQL notification listener connected'); @@ -29,7 +37,6 @@ export class UtilitiesService extends BaseService { throw err; } - // NATS is not available → skip silently this.pg.on('notification', async (msg) => { if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLocaleLowerCase()) { // in case it is not enabled, return From 92317b94dd970ec7882408eb91925fa09f4a48cc Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 27 Nov 2025 19:52:56 +0530 Subject: [PATCH 14/23] fix: handle empty org_agents table in alerts Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index a3b3acc22..9988ca919 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -49,6 +49,12 @@ export class UtilitiesService extends BaseService { const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); const total = Number(totalRes.rows[0].count); + // If the org_agents table has no records, total will be 0, causing a division by zero resulting in Infinity or NaN + if (0 === total) { + this.logger.debug('No org_agents records found, skipping alert check'); + return; + } + // Step 2: Count NULL ledgerId records const nullRes = await this.pg.query('SELECT COUNT(*) FROM org_agents WHERE "ledgerId" IS NULL'); const nullCount = Number(nullRes.rows[0].count); From a1228b0207e7dd6dabea8a69dc97a96aa3abedef Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 28 Nov 2025 15:08:16 +0530 Subject: [PATCH 15/23] fix: handle retry logic and multiple notifications for send email Signed-off-by: Krishna Waske --- .../src/utilities/utilities.service.ts | 13 +++- apps/utility/src/utilities.service.ts | 63 ++++++++++++++++--- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 9988ca919..7e07677f0 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -8,6 +8,7 @@ import { Client as PgClient } from 'pg'; @Injectable() export class UtilitiesService extends BaseService { private pg: PgClient; + private isSendingNatsAlert = false; constructor( @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, @@ -45,6 +46,13 @@ export class UtilitiesService extends BaseService { if ('ledger_null' === msg.channel) { try { + if (this.isSendingNatsAlert) { + this.logger.warn('Skipping duplicate NATS alert send...'); + return; + } + + this.isSendingNatsAlert = true; + // Step 1: Count total records const totalRes = await this.pg.query('SELECT COUNT(*) FROM org_agents'); const total = Number(totalRes.rows[0].count); @@ -73,7 +81,10 @@ export class UtilitiesService extends BaseService { .filter((e) => 0 < e.length) || []; if (0 === alertEmails.length) { - this.logger.warn('DB_ALERT_EMAILS is empty, skipping alert'); + this.logger.error( + `DB_ALERT_EMAILS is empty, skipping alert. There is a ${percent}% records are set to null for 'ledgerId' in 'org_agents' table`, + 'DB alert' + ); return; } diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index a3c5c553a..356bbe6eb 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { RpcException } from '@nestjs/microservices'; import { UtilitiesRepository } from './utilities.repository'; import { AwsService } from '@credebl/aws'; @@ -6,17 +6,20 @@ import { S3 } from 'aws-sdk'; import { v4 as uuidv4 } from 'uuid'; import { EmailService } from '@credebl/common/email.service'; import { EmailDto } from '@credebl/common/dtos/email.dto'; +import { BaseService } from 'libs/service/base.service'; @Injectable() -export class UtilitiesService { +export class UtilitiesService extends BaseService { private lastAlertTime: number | null = null; + private isSendingAlert = false; // Prevent concurrent retries constructor( - private readonly logger: Logger, private readonly utilitiesRepository: UtilitiesRepository, private readonly awsService: AwsService, private readonly emailService: EmailService - ) {} + ) { + super('UtilitiesService'); + } async createAndStoreShorteningUrl(payload): Promise { try { @@ -71,17 +74,57 @@ export class UtilitiesService { } async handleLedgerAlert(emailDto: EmailDto): Promise { - // Avoid spamming: send only once every 2 hours const now = Date.now(); + + // 1. Avoid more than once every 2 hours if (this.lastAlertTime && now - this.lastAlertTime < 2 * 60 * 60 * 1000) { - this.logger.log(`ALERT EMAIL ALREADY SENT at ${this.lastAlertTime}, ledgerId WAS SET TO NULL`); + this.logger.log(`ALERT EMAIL ALREADY SENT at ${new Date(this.lastAlertTime).toISOString()}`); return; } - // Send Email - await this.emailService.sendEmail(emailDto); - this.lastAlertTime = now; + // 2. If a retry flow is already in progress, do NOT start another + if (this.isSendingAlert) { + this.logger.log('Alert email sending already in progress, skipping...'); + return; + } + + // 3. Start async retry flow — do not block the caller + this.isSendingAlert = true; + this.sendWithRetry(emailDto).finally(() => { + this.isSendingAlert = false; + }); + + // immediate return + } + + private async sendWithRetry(emailDto: EmailDto, retries = 3, delayMs = 3000): Promise { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const result = await this.emailService.sendEmail(emailDto); + + if (true !== result) { + throw new Error('Email not sent'); + } + + // Success + this.lastAlertTime = Date.now(); + this.logger.log(`ALERT EMAIL SENT SUCCESSFULLY (attempt ${attempt})`); + return; + } catch (err) { + this.logger.error( + `Email send failed (attempt ${attempt} of ${retries})`, + err instanceof Error ? err.stack : err + ); - this.logger.log('ALERT EMAIL SENT, ledgerId WAS SET TO NULL'); + // If last attempt → throw + if (attempt === retries) { + this.logger.error('All email retry attempts failed.'); + return; + } + + // Wait before retrying + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } } } From 0e8dff231cbfd9bb637775963f82262e2e335664 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 28 Nov 2025 15:14:02 +0530 Subject: [PATCH 16/23] fix: make pg obj readonly Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 7e07677f0..c6ec65a06 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -7,7 +7,7 @@ import { Client as PgClient } from 'pg'; @Injectable() export class UtilitiesService extends BaseService { - private pg: PgClient; + private readonly pg: PgClient; private isSendingNatsAlert = false; constructor( From fe698d069a906b89b809efb1740e5eb27e645701 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 28 Nov 2025 16:01:50 +0530 Subject: [PATCH 17/23] finally reset flag Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index c6ec65a06..f55878cda 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -110,6 +110,9 @@ export class UtilitiesService extends BaseService { this.logger.debug('Received result', JSON.stringify(result, null, 2)); } catch (err) { this.logger.error(err?.message ?? 'Error in ledgerId alert handler'); + } finally { + // Once its done, reset the flag + this.isSendingNatsAlert = false; } } }); From 8d17f7886ed0aac3502b7075493cea5d32f7b725 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 28 Nov 2025 16:06:58 +0530 Subject: [PATCH 18/23] fix: Only initialize PgClient when DB_ALERT_ENABLE is true Signed-off-by: Krishna Waske --- .../api-gateway/src/utilities/utilities.service.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index f55878cda..a4d8af669 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -15,12 +15,14 @@ export class UtilitiesService extends BaseService { private readonly natsClient: NATSClient ) { super('UtilitiesService'); - if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase() && !process.env.DATABASE_URL) { - throw new Error('DATABASE_URL environment variable is required'); - } else { - this.pg = new PgClient({ - connectionString: process.env.DATABASE_URL - }); + if ('true' === process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) { + if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL environment variable is required'); + } else { + this.pg = new PgClient({ + connectionString: process.env.DATABASE_URL + }); + } } } From 08940e3ca07517354692b575378fe002f0df5d9b Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 28 Nov 2025 16:19:36 +0530 Subject: [PATCH 19/23] fix: toLowerLocaleCase to toLowerCase Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index a4d8af669..dd923e6a8 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -41,7 +41,7 @@ export class UtilitiesService extends BaseService { } this.pg.on('notification', async (msg) => { - if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLocaleLowerCase()) { + if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) { // in case it is not enabled, return return; } From d10796023b245974f822fc98f481ab6fc8b0751e Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Mon, 1 Dec 2025 11:43:50 +0530 Subject: [PATCH 20/23] fix: TODOs regarding email from platformconfig Signed-off-by: Krishna Waske --- .../src/utilities/utilities.service.ts | 8 +- apps/utility/src/utilities.repository.ts | 97 ++++++++++--------- apps/utility/src/utilities.service.ts | 12 ++- 3 files changed, 63 insertions(+), 54 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index dd923e6a8..53b458514 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -90,14 +90,8 @@ export class UtilitiesService extends BaseService { return; } - // TODO: Check if the to email is actually this or we need to take it from DB - if (!process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL) { - this.logger.warn('PUBLIC_PLATFORM_SUPPORT_EMAIL not configured, skipping alert'); - return; - } - const emailDto = { - emailFrom: process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL, + emailFrom: '', emailTo: alertEmails, emailSubject: '[ALERT] More than 30% org_agents ledgerId is NULL', emailText: `ALERT: ${percent.toFixed(2)}% of org_agents records currently have ledgerId = NULL.`, diff --git a/apps/utility/src/utilities.repository.ts b/apps/utility/src/utilities.repository.ts index 7727d1b8e..a6a193337 100644 --- a/apps/utility/src/utilities.repository.ts +++ b/apps/utility/src/utilities.repository.ts @@ -1,51 +1,60 @@ -import { PrismaService } from "@credebl/prisma-service"; -import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from '@credebl/prisma-service'; +import { Injectable, Logger } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { shortening_url } from "@prisma/client"; +import { platform_config, shortening_url } from '@prisma/client'; @Injectable() export class UtilitiesRepository { - constructor( - private readonly prisma: PrismaService, - private readonly logger: Logger - ) { } - - async saveShorteningUrl( - payload - ): Promise { - - try { - - const { referenceId, invitationPayload } = payload; - const storeShorteningUrl = await this.prisma.shortening_url.upsert({ - where: { referenceId }, - update: { invitationPayload }, - create: { referenceId, invitationPayload } - }); - - this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in saveShorteningUrl: ${error} `); - throw error; - } + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async saveShorteningUrl(payload): Promise { + try { + const { referenceId, invitationPayload } = payload; + const storeShorteningUrl = await this.prisma.shortening_url.upsert({ + where: { referenceId }, + update: { invitationPayload }, + create: { referenceId, invitationPayload } + }); + + this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in saveShorteningUrl: ${error} `); + throw error; } - - // eslint-disable-next-line camelcase - async getShorteningUrl(referenceId): Promise { - try { - - const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ - where: { - referenceId - } - }); - - this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in getShorteningUrl: ${error} `); - throw error; + } + + // eslint-disable-next-line camelcase + async getShorteningUrl(referenceId): Promise { + try { + const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ + where: { + referenceId } + }); + + this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in getShorteningUrl: ${error} `); + throw error; + } + } + + /** + * Get platform config details + * @returns + */ + // eslint-disable-next-line camelcase + async getPlatformConfigDetails(): Promise { + try { + return this.prisma.platform_config.findFirst(); + } catch (error) { + this.logger.error(`[getPlatformConfigDetails] - error: ${JSON.stringify(error)}`); + throw error; } -} \ No newline at end of file + } +} diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index 356bbe6eb..e118a8c7a 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { RpcException } from '@nestjs/microservices'; import { UtilitiesRepository } from './utilities.repository'; import { AwsService } from '@credebl/aws'; @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { EmailService } from '@credebl/common/email.service'; import { EmailDto } from '@credebl/common/dtos/email.dto'; import { BaseService } from 'libs/service/base.service'; +import { ResponseMessages } from '@credebl/common/response-messages'; @Injectable() export class UtilitiesService extends BaseService { @@ -88,13 +89,18 @@ export class UtilitiesService extends BaseService { return; } + const platformConfigData = await this.utilitiesRepository.getPlatformConfigDetails(); + if (!platformConfigData) { + throw new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound); + } + + emailDto.emailFrom = platformConfigData?.emailFrom; + // 3. Start async retry flow — do not block the caller this.isSendingAlert = true; this.sendWithRetry(emailDto).finally(() => { this.isSendingAlert = false; }); - - // immediate return } private async sendWithRetry(emailDto: EmailDto, retries = 3, delayMs = 3000): Promise { From 8c82fc6c52786e027163832ace26c1839e7df54a Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Mon, 1 Dec 2025 11:49:01 +0530 Subject: [PATCH 21/23] fix: cosmetic changes and missing env variable add in sample and demo Signed-off-by: Krishna Waske --- .env.demo | 4 +++- .env.sample | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env.demo b/.env.demo index 49d5ef30a..984d1b2b2 100644 --- a/.env.demo +++ b/.env.demo @@ -223,5 +223,7 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Default set to error PRISMA_LOGS = error +# HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true + # DB_ALERT_ENABLE= -# DB_ALERT_EMAILS= \ No newline at end of file +# DB_ALERT_EMAILS= diff --git a/.env.sample b/.env.sample index 626d9a264..3a2450ebe 100644 --- a/.env.sample +++ b/.env.sample @@ -243,7 +243,10 @@ RESEND_API_KEY=re_xxxxxxxxxx # Prisma log type. Ideally should have only error or warn enabled. Having query enabled can add a lot of unwanted logging for all types of queries being run # PRISMA_LOGS = error,warn,query -# Comma separated emails that needs to be alerted in case the 'ledgerId' is set to null +# Default is true too, if nothing is passed +# HIDE_EXPERIMENTAL_OIDC_CONTROLLERS= + +# Comma separated emails that need to be alerted in case the 'ledgerId' is set to null # DB_ALERT_EMAILS= # Boolean: to enable/disable db alerts. This needs the 'utility' microservice -# DB_ALERT_ENABLE= \ No newline at end of file +# DB_ALERT_ENABLE= From ab52155d8f0d97928bae589284f540028788ff7d Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Mon, 1 Dec 2025 11:51:35 +0530 Subject: [PATCH 22/23] fix: minor code rabbit changes Signed-off-by: Krishna Waske --- apps/utility/src/utilities.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/utility/src/utilities.service.ts b/apps/utility/src/utilities.service.ts index e118a8c7a..f6180bf9f 100644 --- a/apps/utility/src/utilities.service.ts +++ b/apps/utility/src/utilities.service.ts @@ -46,6 +46,10 @@ export class UtilitiesService extends BaseService { try { const getShorteningUrl = await this.utilitiesRepository.getShorteningUrl(referenceId); + if (!getShorteningUrl) { + throw new NotFoundException(`Shortening URL not found for referenceId: ${referenceId}`); + } + const getInvitationUrl = { referenceId: getShorteningUrl.referenceId, invitationPayload: getShorteningUrl.invitationPayload @@ -70,7 +74,9 @@ export class UtilitiesService extends BaseService { return url; } catch (error) { this.logger.error(error); - throw new Error('An error occurred while uploading data to S3. Error::::::'); + throw new Error( + `An error occurred while uploading data to S3: ${error instanceof Error ? error?.message : error}` + ); } } From d34e79295bee3a53f5966f2047f46ada8f41dad1 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Mon, 1 Dec 2025 12:31:57 +0530 Subject: [PATCH 23/23] fix: percentage threshold from common constants Signed-off-by: Krishna Waske --- apps/api-gateway/src/utilities/utilities.service.ts | 6 ++++-- libs/common/src/common.constant.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/api-gateway/src/utilities/utilities.service.ts b/apps/api-gateway/src/utilities/utilities.service.ts index 53b458514..d90b0f4e5 100644 --- a/apps/api-gateway/src/utilities/utilities.service.ts +++ b/apps/api-gateway/src/utilities/utilities.service.ts @@ -4,6 +4,7 @@ import { StoreObjectDto, UtilitiesDto } from './dtos/shortening-url.dto'; import { NATSClient } from '@credebl/common/NATSClient'; import { ClientProxy } from '@nestjs/microservices'; import { Client as PgClient } from 'pg'; +import { CommonConstants } from '@credebl/common/common.constant'; @Injectable() export class UtilitiesService extends BaseService { @@ -26,6 +27,7 @@ export class UtilitiesService extends BaseService { } } + // TODO: I think it would be better, if we add all the event listening and email sending logic in a common library instead of it being scattered across here and the utility microservice async onModuleInit(): Promise { try { if ('true' !== process.env.DB_ALERT_ENABLE?.trim()?.toLowerCase()) { @@ -72,8 +74,8 @@ export class UtilitiesService extends BaseService { // Step 3: Calculate % const percent = (nullCount / total) * 100; - // Condition: > 30% - if (30 >= percent) { + // Condition: > 30% for now + if (CommonConstants.AFFECTED_RECORDS_THRESHOLD_PERCENTAGE_FOR_DB_ALERT >= percent) { return; } diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 7de2755a3..62cd4c7f2 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -383,7 +383,9 @@ export enum CommonConstants { GET_VERIFIED_PROOF = 'get-verified-proof', GET_QUESTION_ANSWER_RECORD = 'get-question-answer-record', SEND_QUESTION = 'send-question', - SEND_BASIC_MESSAGE = 'send-basic-message' + SEND_BASIC_MESSAGE = 'send-basic-message', + + AFFECTED_RECORDS_THRESHOLD_PERCENTAGE_FOR_DB_ALERT = 30 } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/;