Files
lisglosips/packages/database/src/caller-analytics-store.ts
T

53 lines
4.2 KiB
TypeScript

import { Prisma, PrismaClient } from '@prisma/client';
import { analyticsHash, foldAnalytics, projectAnalytics, COUNT_KEYS, type AnalyticsCall, type AnalyticsEvent, type AnalyticsProjection } from './caller-analytics-model.js';
type Tx = Prisma.TransactionClient;
const json = <T>(value: T | string): T => typeof value === 'string' ? JSON.parse(value) as T : value;
export class CallerAnalyticsStore {
constructor(private readonly db: PrismaClient) {}
async consume(event: AnalyticsEvent): Promise<'processed' | 'duplicate'> {
// The legacy router puts diagnostic markers in customer_id for unauthenticated traffic.
// Such traffic is outside the business population and must not create analysis calls.
if (!await this.db.customer.findUnique({ where: { id: event.customerId }, select: { id: true } })) return 'duplicate';
return this.db.$transaction(async tx => {
const callKey = analyticsHash(event.callUid);
await tx.$executeRaw`INSERT IGNORE INTO caller_analysis_calls (id, payload, updated_at) VALUES (${callKey}, NULL, NOW(3))`;
const rows = await tx.$queryRaw<Array<{ payload: AnalyticsCall | string | null }>>`SELECT payload FROM caller_analysis_calls WHERE id=${callKey} FOR UPDATE`;
const eventKey = analyticsHash(event.eventId);
const previous = await tx.$queryRaw<Array<{ id: string }>>`SELECT id FROM caller_analysis_events WHERE id=${eventKey}`;
if (previous.length) return 'duplicate';
const old = rows[0].payload ? json<AnalyticsCall>(rows[0].payload) : null;
const next = foldAnalytics(old, event);
const before = new Map((old ? projectAnalytics(old) : []).map(row => [row.id, row]));
for (const row of projectAnalytics(next)) {
const prior = before.get(row.id);
if (prior) await this.bucket(tx, prior, -1);
await this.bucket(tx, row, 1);
await this.state(tx, row);
}
await tx.$executeRaw`UPDATE caller_analysis_calls SET payload=${JSON.stringify(next)}, updated_at=NOW(3) WHERE id=${callKey}`;
await tx.$executeRaw`INSERT INTO caller_analysis_events (id, call_key, occurred_at, payload, created_at) VALUES (${eventKey}, ${callKey}, ${new Date(event.at)}, ${JSON.stringify(event)}, NOW(3))`;
return 'processed';
}, { timeout: 15000 });
}
private async state(tx: Tx, row: AnalyticsProjection) {
await tx.$executeRaw`INSERT INTO caller_analysis_states
(id, call_key, view, started_at, customer_id, customer_gateway_id, vendor_id, vendor_gateway_id, caller, callee, city, carrier, counts, payload, updated_at)
VALUES (${row.id}, ${row.callKey}, ${row.view}, ${new Date(row.startedAt)}, ${row.customerId}, ${row.customerGatewayId}, ${row.vendorId}, ${row.vendorGatewayId}, ${row.caller}, ${row.callee}, ${row.city}, ${row.carrier}, ${JSON.stringify(Object.fromEntries(COUNT_KEYS.map(k => [k, row[k]])))}, ${JSON.stringify(row.payload)}, NOW(3))
ON DUPLICATE KEY UPDATE started_at=VALUES(started_at), customer_gateway_id=VALUES(customer_gateway_id), vendor_id=VALUES(vendor_id), vendor_gateway_id=VALUES(vendor_gateway_id), caller=VALUES(caller), callee=VALUES(callee), city=VALUES(city), carrier=VALUES(carrier), counts=VALUES(counts), payload=VALUES(payload), updated_at=NOW(3)`;
}
private async bucket(tx: Tx, row: AnalyticsProjection, sign: number) {
const minute = new Date(Math.floor(row.startedAt / 60000) * 60000);
const id = analyticsHash([minute.toISOString(), row.view, row.customerId, row.customerGatewayId, row.vendorId, row.vendorGatewayId, row.caller, row.city, row.carrier]);
const delta = JSON.stringify(Object.fromEntries(COUNT_KEYS.map(k => [k, sign * row[k]])));
const additions = Prisma.join(COUNT_KEYS.flatMap(k => [Prisma.sql`${`$.${k}`}`, Prisma.sql`CAST(JSON_UNQUOTE(JSON_EXTRACT(counts, ${`$.${k}`})) AS SIGNED) + ${sign * row[k]}`]));
await tx.$executeRaw(Prisma.sql`INSERT INTO caller_analysis_minute_buckets
(id, started_at, view, customer_id, customer_gateway_id, vendor_id, vendor_gateway_id, caller, city, carrier, counts)
VALUES (${id}, ${minute}, ${row.view}, ${row.customerId}, ${row.customerGatewayId}, ${row.vendorId}, ${row.vendorGatewayId}, ${row.caller}, ${row.city}, ${row.carrier}, ${delta})
ON DUPLICATE KEY UPDATE counts=JSON_OBJECT(${additions})`);
}
}