perf(connections): collapse pricing N+1 into a single bulk query

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.
This commit is contained in:
kovakmedya
2026-05-21 22:45:58 +03:00
parent 90abb398fa
commit 48361792f0
3 changed files with 43 additions and 26 deletions
+15 -23
View File
@@ -1,7 +1,10 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 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 { import {
listApprovedConnections, listApprovedConnections,
listPendingInbound, listPendingInbound,
@@ -37,29 +40,18 @@ export default async function ConnectionsPage() {
listPendingOutbound(ctx.tenantId, ctx.user.id), listPendingOutbound(ctx.tenantId, ctx.user.id),
]); ]);
// Lab side: load pricing rules per approved clinic so the dialog can // Single bulk query for pricing then group client-side. Avoids N+1 across
// surface existing rules. Clinic side leaves this empty — it's display-only // approved counterparts (was the main reason the save dialog felt slow —
// for them (see ConnectionsTable). // 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<string, ClinicPricing[]>(); const pricingByCounterpart = new Map<string, ClinicPricing[]>();
if (isLab) { for (const row of allPricing) {
const pricingLists = await Promise.all( const key = isLab ? row.clinicTenantId : row.labTenantId;
approved.map((c) => const bucket = pricingByCounterpart.get(key);
listClinicPricing(ctx.tenantId, c.clinicTenantId).then( if (bucket) bucket.push(row);
(rows) => [c.clinicTenantId, rows] as const, else pricingByCounterpart.set(key, [row]);
),
),
);
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);
} }
const pricingObject: Record<string, ClinicPricing[]> = Object.fromEntries( const pricingObject: Record<string, ClinicPricing[]> = Object.fromEntries(
pricingByCounterpart, pricingByCounterpart,
+3 -3
View File
@@ -124,7 +124,7 @@ export async function setClinicPricingAction(
if (row) { if (row) {
await tablesDB.updateRow(DATABASE_ID, TABLES.clinicPricing, row.$id, payload); await tablesDB.updateRow(DATABASE_ID, TABLES.clinicPricing, row.$id, payload);
await logAudit({ void logAudit({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
userId: ctx.user.id, userId: ctx.user.id,
action: "update", action: "update",
@@ -140,7 +140,7 @@ export async function setClinicPricingAction(
payload, payload,
pricingPermissions(ctx.tenantId, parsed.data.clinicTenantId), pricingPermissions(ctx.tenantId, parsed.data.clinicTenantId),
); );
await logAudit({ void logAudit({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
userId: ctx.user.id, userId: ctx.user.id,
action: "create", action: "create",
@@ -184,7 +184,7 @@ export async function clearClinicPricingAction(
return { ok: false, error: "Yetkiniz yok." }; return { ok: false, error: "Yetkiniz yok." };
} }
await tablesDB.deleteRow(DATABASE_ID, TABLES.clinicPricing, id); await tablesDB.deleteRow(DATABASE_ID, TABLES.clinicPricing, id);
await logAudit({ void logAudit({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
userId: ctx.user.id, userId: ctx.user.id,
action: "delete", action: "delete",
@@ -29,3 +29,28 @@ export async function listClinicPricingForClinic(
): Promise<ClinicPricing[]> { ): Promise<ClinicPricing[]> {
return listClinicPricing(labTenantId, clinicTenantId); return listClinicPricing(labTenantId, clinicTenantId);
} }
/** Single bulk query so /connections doesn't N+1 across approved counterparts. */
export async function listAllPricingForLab(
labTenantId: string,
): Promise<ClinicPricing[]> {
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<ClinicPricing[]> {
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[]);
}