From 48361792f061c668dd87dbd571ba1f2dac71e222 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 21 May 2026 22:45:58 +0300 Subject: [PATCH] perf(connections): collapse pricing N+1 into a single bulk query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /connections page was firing one listClinicPricing call per approved counterpart inside Promise.all. That meant a lab with 10 clinics paid 10 sequential Appwrite roundtrips on every load, and worse, every time the ClinicPricingDialog saved a row revalidatePath('/connections') ran the whole fan-out again — saving a single discount felt like the request had hung. Replaced the per-peer query with listAllPricingForLab / listAllPricingForClinic (single Query.equal on the side-specific column, limit 500) and group the result into a Map client-side. One roundtrip regardless of how many connections you have. Also flipped the audit-log calls in setClinicPricingAction / clearClinicPricingAction from 'await logAudit(...)' to 'void logAudit(...)' — audit is best-effort by design and never blocks the user-facing mutation; awaiting it doubled the perceived latency for nothing. --- src/app/(dashboard)/connections/page.tsx | 38 +++++++++------------- src/lib/appwrite/clinic-pricing-actions.ts | 6 ++-- src/lib/appwrite/clinic-pricing-queries.ts | 25 ++++++++++++++ 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/app/(dashboard)/connections/page.tsx b/src/app/(dashboard)/connections/page.tsx index 6c1cf2c..7c696f8 100644 --- a/src/app/(dashboard)/connections/page.tsx +++ b/src/app/(dashboard)/connections/page.tsx @@ -1,7 +1,10 @@ import { redirect } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { listClinicPricing } from "@/lib/appwrite/clinic-pricing-queries"; +import { + listAllPricingForClinic, + listAllPricingForLab, +} from "@/lib/appwrite/clinic-pricing-queries"; import { listApprovedConnections, listPendingInbound, @@ -37,29 +40,18 @@ export default async function ConnectionsPage() { listPendingOutbound(ctx.tenantId, ctx.user.id), ]); - // Lab side: load pricing rules per approved clinic so the dialog can - // surface existing rules. Clinic side leaves this empty — it's display-only - // for them (see ConnectionsTable). + // Single bulk query for pricing then group client-side. Avoids N+1 across + // approved counterparts (was the main reason the save dialog felt slow — + // every revalidatePath of /connections fanned out one query per peer). + const allPricing = isLab + ? await listAllPricingForLab(ctx.tenantId) + : await listAllPricingForClinic(ctx.tenantId); const pricingByCounterpart = new Map(); - if (isLab) { - const pricingLists = await Promise.all( - approved.map((c) => - listClinicPricing(ctx.tenantId, c.clinicTenantId).then( - (rows) => [c.clinicTenantId, rows] as const, - ), - ), - ); - for (const [id, rows] of pricingLists) pricingByCounterpart.set(id, rows); - } else { - // Clinic side: only its own pricing rules from each lab, read-only - const pricingLists = await Promise.all( - approved.map((c) => - listClinicPricing(c.labTenantId, ctx.tenantId).then( - (rows) => [c.labTenantId, rows] as const, - ), - ), - ); - for (const [id, rows] of pricingLists) pricingByCounterpart.set(id, rows); + for (const row of allPricing) { + const key = isLab ? row.clinicTenantId : row.labTenantId; + const bucket = pricingByCounterpart.get(key); + if (bucket) bucket.push(row); + else pricingByCounterpart.set(key, [row]); } const pricingObject: Record = Object.fromEntries( pricingByCounterpart, diff --git a/src/lib/appwrite/clinic-pricing-actions.ts b/src/lib/appwrite/clinic-pricing-actions.ts index d8f9c9e..a13705a 100644 --- a/src/lib/appwrite/clinic-pricing-actions.ts +++ b/src/lib/appwrite/clinic-pricing-actions.ts @@ -124,7 +124,7 @@ export async function setClinicPricingAction( if (row) { await tablesDB.updateRow(DATABASE_ID, TABLES.clinicPricing, row.$id, payload); - await logAudit({ + void logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", @@ -140,7 +140,7 @@ export async function setClinicPricingAction( payload, pricingPermissions(ctx.tenantId, parsed.data.clinicTenantId), ); - await logAudit({ + void logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "create", @@ -184,7 +184,7 @@ export async function clearClinicPricingAction( return { ok: false, error: "Yetkiniz yok." }; } await tablesDB.deleteRow(DATABASE_ID, TABLES.clinicPricing, id); - await logAudit({ + void logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "delete", diff --git a/src/lib/appwrite/clinic-pricing-queries.ts b/src/lib/appwrite/clinic-pricing-queries.ts index 63e93cc..5121f2f 100644 --- a/src/lib/appwrite/clinic-pricing-queries.ts +++ b/src/lib/appwrite/clinic-pricing-queries.ts @@ -29,3 +29,28 @@ export async function listClinicPricingForClinic( ): Promise { return listClinicPricing(labTenantId, clinicTenantId); } + +/** Single bulk query so /connections doesn't N+1 across approved counterparts. */ +export async function listAllPricingForLab( + labTenantId: string, +): Promise { + const { tablesDB } = createAdminClient(); + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.clinicPricing, + queries: [Query.equal("labTenantId", labTenantId), Query.limit(500)], + }); + return toPlain(result.rows as unknown as ClinicPricing[]); +} + +export async function listAllPricingForClinic( + clinicTenantId: string, +): Promise { + const { tablesDB } = createAdminClient(); + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.clinicPricing, + queries: [Query.equal("clinicTenantId", clinicTenantId), Query.limit(500)], + }); + return toPlain(result.rows as unknown as ClinicPricing[]); +}