Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/backend/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { createHash } from 'crypto';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaReadReplicaService } from '../prisma/prisma-read-replica.service';
import { ClaimStatus } from '@prisma/client';
import {
GlobalStatsDto,
Expand Down Expand Up @@ -97,6 +98,7 @@

constructor(
private readonly prisma: PrismaService,
private readonly prismaRead: PrismaReadReplicaService,
private readonly redis: RedisService,
private readonly privacyService: PrivacyService,
private readonly metrics: MetricsService,
Expand Down Expand Up @@ -214,9 +216,7 @@
const { from, to, region, token } = query;
const { startDate, endDate } = this.resolveDateRange(from, to);

// Fetch all disbursed claims within the time window, including their
// parent campaign so we can read metadata.
const claims = await this.prisma.claim.findMany({
const claims = await this.prismaRead.invoke('claim.findMany', {

Check warning on line 219 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value

Check warning on line 219 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
where: {
status: ClaimStatus.disbursed,
createdAt: { gte: startDate, lte: endDate },
Expand All @@ -237,7 +237,7 @@
});

// Count active campaigns (optionally filtered by region / token).
const activeCampaigns = await this.prisma.campaign.count({
const activeCampaigns = await this.prismaRead.invoke('campaign.count', {

Check warning on line 240 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value

Check warning on line 240 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value
where: {
status: 'active',
...(region || token ? this.buildMetadataFilter(region, token) : {}),
Expand All @@ -254,14 +254,14 @@
const dateMap = new Map<string, { amount: number; count: number }>();

for (const claim of claims) {
const meta = (claim.campaign.metadata ?? {}) as CampaignMetadata;

Check warning on line 257 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .campaign on an `any` value

Check warning on line 257 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .campaign on an `any` value
const claimToken = meta.token ?? FALLBACK_TOKEN;
const claimRegion = meta.region ?? FALLBACK_REGION;
const claimAmount = Number(claim.amount);

Check warning on line 260 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .amount on an `any` value

Check warning on line 260 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .amount on an `any` value
const dateKey = claim.createdAt.toISOString().slice(0, 10);

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .slice on an `any` value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .createdAt on an `any` value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .slice on an `any` value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe member access .createdAt on an `any` value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe call of an `any` typed value

Check warning on line 261 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe assignment of an `any` value

totalAidDisbursed += claimAmount;
uniqueRecipients.add(claim.recipientRef);

Check warning on line 264 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe argument of type `any` assigned to a parameter of type `string`

Check warning on line 264 in app/backend/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unsafe argument of type `any` assigned to a parameter of type `string`

// Token breakdown
const tok = tokenMap.get(claimToken) ?? { amount: 0, count: 0 };
Expand Down
10 changes: 9 additions & 1 deletion app/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from './common/security/security.module';
import { RedisService } from '@liaoliaots/nestjs-redis';

async function bootstrap() {
export async function bootstrap() {
// Load environment variables
loadEnv();

Expand All @@ -34,6 +34,14 @@ async function bootstrap() {
app.enableShutdownHooks();

const configService = app.get(ConfigService);
// Validate CORS_ORIGINS in production
const nodeEnv = configService.get<string>('NODE_ENV', 'development');
if (nodeEnv === 'production') {
const corsOrigins = configService.get<string>('CORS_ORIGINS');
if (!corsOrigins) {
throw new Error('CORS_ORIGINS must be set in production');
}
}

// Security middleware (order matters)
app.use(createHelmetMiddleware(configService));
Expand Down
60 changes: 60 additions & 0 deletions app/backend/src/prisma/prisma-read-replica.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaService } from './prisma.service';

/**
* Service that routes read‑only Prisma operations to a read‑replica datasource.
* It delegates write operations to the primary {@link PrismaService}.
* Read‑only operations (find*, count*, aggregate*) are executed against a
* PrismaClient instantiated with `DATABASE_URL_READ`.
*/
@Injectable()
export class PrismaReadReplicaService {
private readonly logger = new Logger(PrismaReadReplicaService.name);
private readonly readClient: PrismaClient;

constructor(private readonly primary: PrismaService) {
const readUrl = process.env.DATABASE_URL_READ;
if (!readUrl) {
this.logger.warn('DATABASE_URL_READ is not set; read replica will fall back to primary.');
}
this.readClient = new PrismaClient({
datasources: { db: { url: readUrl || process.env.DATABASE_URL } },
});
}

async onModuleInit(): Promise<void> {
try {
await this.readClient.$connect();
this.logger.debug('Read‑replica Prisma client connected');
} catch (err) {
this.logger.error('Failed to connect read‑replica Prisma client', err as Error);
}
}

async onModuleDestroy(): Promise<void> {
try {
await this.readClient.$disconnect();
this.logger.debug('Read‑replica Prisma client disconnected');
} catch (err) {
this.logger.error('Failed to disconnect read‑replica Prisma client', err as Error);
}
}

/** Determine the appropriate client for a given method name. */
private getTarget(methodName: string): any {
const readPrefixes = ['find', 'count', 'aggregate'];
const isRead = readPrefixes.some((p) => methodName.startsWith(p));
return isRead ? this.readClient : this.primary;
}

/** Generic proxy to call Prisma methods on the correct client. */
public async invoke<T extends keyof PrismaClient>(method: T, ...args: any[]): Promise<any> {
const target = this.getTarget(method as string);
const fn = (target as any)[method];
if (typeof fn !== 'function') {
throw new Error(`Prisma method ${method} does not exist`);
}
return fn.apply(target, args);
}
}

Check failure on line 60 in app/backend/src/prisma/prisma-read-replica.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Async method 'invoke' has no 'await' expression

Check failure on line 60 in app/backend/src/prisma/prisma-read-replica.service.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Async method 'invoke' has no 'await' expression
5 changes: 3 additions & 2 deletions app/backend/src/prisma/prisma.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { PrismaReadReplicaService } from './prisma-read-replica.service';

@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
providers: [PrismaService, PrismaReadReplicaService],
exports: [PrismaService, PrismaReadReplicaService],
})
export class PrismaModule {}
Loading