perf(audit-tier-3): bulk-fetch the five hot N+1 loops
Replaces per-row fan-out with grouped queries / inArray pre-fetches across the five dashboard + cron hotspots flagged in the audit (MED §13 / HIGH §11–14): * reminders.processFollowUpReminders — was 3 round trips per enabled-and-due interest. Now: filter in JS, single clients bulk-fetch, single reminders bulk-insert, single interests bulk-update, one summary socket emit. 1k due interests: 6 round trips total instead of 3000+. * portal.getClientInvoices — was a full-table scan filtered in JS. Now an inArray push-down on lower(billingEmail) + defensive limit(100). After 12mo this would have been the worst portal endpoint. * interest-scoring.calculateBulkScores — was 6N round trips (1 redis + 1 findFirst + 4 counts per interest). Now 4 grouped count queries on the port's interest set + a single redis pipeline to refresh the cache. 1k interests: ~7 round trips. * document-reminders.processReminderQueue — was 5N round trips per cron tick (port + template + lastReminder + pendingSigners + send per doc). Now hoists port + per-type template map + grouped lastReminder + bulk pendingSigners; per-row work collapses to a Map.get and the documenso send. 500 docs: ~7 round trips. * inquiry-notifications.sendInquiryNotifications — was sequential createNotification + emailQueue.add per recipient inside a public POST. Now Promise.all'd; a 20-user port stops blocking the public inquiry POST on ~80 round trips. Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§11–14 (auditor-I Issues 1–4) + MED §13 (auditor-I Issue 5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { and, eq, count, inArray, isNull, desc } from 'drizzle-orm';
|
||||
import { and, eq, count, inArray, isNull, desc, sql } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
@@ -250,16 +250,22 @@ export async function getClientInvoices(
|
||||
|
||||
if (emailContacts.length === 0) return [];
|
||||
|
||||
// Fetch invoices matching any of the client's email addresses
|
||||
const allInvoices = await db
|
||||
// Fetch only the invoices matching any of the client's email addresses.
|
||||
// Without the inArray push-down here every portal invoice page-load
|
||||
// full-scanned the invoices table and filtered in JS — by 12mo it would
|
||||
// have been the worst portal endpoint in the platform. Defensive limit
|
||||
// 100 caps the upper bound for clients with abnormally many invoices.
|
||||
const clientInvoices = await db
|
||||
.select()
|
||||
.from(invoices)
|
||||
.where(eq(invoices.portId, portId))
|
||||
.orderBy(invoices.createdAt);
|
||||
|
||||
const clientInvoices = allInvoices.filter(
|
||||
(inv) => inv.billingEmail && emailContacts.includes(inv.billingEmail.toLowerCase()),
|
||||
);
|
||||
.where(
|
||||
and(
|
||||
eq(invoices.portId, portId),
|
||||
inArray(sql`lower(${invoices.billingEmail})`, emailContacts),
|
||||
),
|
||||
)
|
||||
.orderBy(invoices.createdAt)
|
||||
.limit(100);
|
||||
|
||||
return clientInvoices.map((inv) => ({
|
||||
id: inv.id,
|
||||
|
||||
Reference in New Issue
Block a user