perf(medications): batch the intake CSV import duplicate probe

The CSV import loop ran a findUnique duplicate check before every
single create — up to 2000 sequential round trips on a 1000-row
import. Resolve takenAt + the idempotency key for every row first,
then read every key's existence in one indexed query before the write
loop; create and the per-row inventory consumption stay unchanged.

Also runs prettier over the batch-route test files touched in the
prior two commits.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 00:37:24 +02:00
1 parent 2c6699f682
commit e3dfe9f400
7 files changed
+91 -39

No files matched your search

@@ -190,15 +190,15 @@ describe("GET /api/insights/cards", () => {
pulseRows.push({ value: i < 3 ? 300 : 68, measuredAt: at });
}
vi.mocked(prisma.measurement.findMany).mockImplementation(
(async (args: { where?: { type?: unknown } }) => {
// `readDayMeanSeries`'s live-fallback probes a single `type`
// equality filter; the manual WEIGHT/BP query below probes an
// `{ in: [...] }` list — the two calls are distinguishable by shape.
if (args?.where?.type === "PULSE") return pulseRows;
return [];
}) as never,
);
vi.mocked(prisma.measurement.findMany).mockImplementation((async (args: {
where?: { type?: unknown };
}) => {
// `readDayMeanSeries`'s live-fallback probes a single `type`
// equality filter; the manual WEIGHT/BP query below probes an
// `{ in: [...] }` list — the two calls are distinguishable by shape.
if (args?.where?.type === "PULSE") return pulseRows;
return [];
}) as never);
const res = await callGet(makeReq());
expect(res.status).toBe(200);
@@ -216,13 +216,12 @@ describe("GET /api/insights/cards", () => {
.mocked(prisma.measurement.findMany)
.mock.calls.find(
(call) =>
typeof (call[0] as { where?: { type?: { in?: unknown[] } } })
?.where?.type === "object",
typeof (call[0] as { where?: { type?: { in?: unknown[] } } })?.where
?.type === "object",
);
expect(manualCall).toBeDefined();
const typeIn = (
manualCall?.[0] as { where: { type: { in: string[] } } }
).where.type.in;
const typeIn = (manualCall?.[0] as { where: { type: { in: string[] } } })
.where.type.in;
expect(typeIn).not.toContain("PULSE");
});
@@ -3,6 +3,10 @@
* /api/medications/[id]/intake/import. Preserves the
* `medication.intake.import.invalid_format` errorCode meta so the CSV
* import client UI can branch on the prefix semantics.
*
* v1.29 perf fix — the per-row `findUnique` duplicate probe was replaced by
* one indexed `findMany` existence read over every row's idempotencyKey
* before the write loop. `create` + `consumeForIntake` stay per-row.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
@@ -13,7 +17,7 @@ vi.mock("@/lib/db", () => ({
findUnique: vi.fn(),
},
medicationIntakeEvent: {
findUnique: vi.fn(),
findMany: vi.fn(),
create: vi.fn(),
},
auditLog: { create: vi.fn() },
@@ -75,6 +79,12 @@ function postReq(body: unknown): NextRequest {
const ROUTE_CTX = { params: Promise.resolve({ id: "m1" }) };
/** Mirrors the route's own dedup-key derivation for a zaehler-less row. */
function keyFor(medId: string, datum: string, uhrzeit: string): string {
const takenAt = new Date(`${datum}T${uhrzeit}`);
return `import-${medId}-${takenAt.getTime()}`;
}
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
@@ -172,10 +182,9 @@ describe("POST /api/medications/[id]/intake/import — 422 multi-issue (v1.4.43
describe("POST /api/medications/[id]/intake/import — inventory consumption", () => {
beforeEach(() => {
// Fresh imports: no pre-existing dedup row, and create echoes an id.
vi.mocked(prisma.medicationIntakeEvent.findUnique).mockResolvedValue(
null as never,
);
// Fresh imports: the batched existence read finds no prior rows, and
// create echoes an id.
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue([]);
let n = 0;
vi.mocked(prisma.medicationIntakeEvent.create).mockImplementation(
(async () => ({ id: `evt-${++n}`, takenAt: new Date() })) as never,
@@ -200,16 +209,24 @@ describe("POST /api/medications/[id]/intake/import — inventory consumption", (
expect(call?.userId).toBe("user-1");
expect(call?.eventId).toBe("evt-1");
// The three-row import reads existence in ONE indexed query, not one
// findUnique probe per row.
expect(prisma.medicationIntakeEvent.findMany).toHaveBeenCalledTimes(1);
const findManyArg = vi.mocked(prisma.medicationIntakeEvent.findMany).mock
.calls[0]?.[0] as { where: { idempotencyKey: { in: string[] } } };
expect(findManyArg.where.idempotencyKey.in).toHaveLength(3);
// (#22) — the whole import queues exactly ONE cross-device sync
// fan-out, not one per imported row.
expect(queueMedicationIntakeSync).toHaveBeenCalledTimes(1);
});
it("does not consume for duplicate (already-imported) rows", async () => {
// First entry is a duplicate (existing dedup row), second is fresh.
vi.mocked(prisma.medicationIntakeEvent.findUnique)
.mockResolvedValueOnce({ id: "existing" } as never)
.mockResolvedValueOnce(null as never);
// First entry's key is already present — the batched existence read
// reports it, the second entry's key is absent (fresh).
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue([
{ idempotencyKey: keyFor("m1", "2026-01-01", "07:00:00") },
] as never);
const res = await POST(
postReq([
{ datum: "2026-01-01", uhrzeit: "07:00:00" },
@@ -99,6 +99,20 @@ export const POST = apiHandler(
// so the rollup pass fires once per day rather than once per row.
const touchedDays = new Set<string>();
// Perf — a CSV import can carry up to 1000 rows; the pre-fix loop ran a
// `findUnique` duplicate probe before every single `create`, up to 2000
// sequential round trips. Resolve `takenAt` + the dedup key for every
// row first, then read every key's existence in ONE indexed query
// (`idempotencyKey` carries its own unique index) before the write
// loop. `create` + `consumeForIntake` (inventory decrement) stay
// per-row — each has a real side effect keyed off the freshly-created
// event's id, so they aren't safely batchable without a deeper change
// to the inventory-consumption path.
interface PreparedIntake {
takenAt: Date;
idempotencyKey: string;
}
const prepared: PreparedIntake[] = [];
for (const entry of entries) {
// Parse as local datetime first; if invalid, fall back to CET offset.
let takenAt = new Date(`${entry.datum}T${entry.uhrzeit}`);
@@ -115,10 +129,26 @@ export const POST = apiHandler(
? `import-${id}-${String(entry.zaehler)}`
: `import-${id}-${takenAt.getTime()}`;
const existing = await prisma.medicationIntakeEvent.findUnique({
where: { idempotencyKey },
});
if (existing) {
prepared.push({ takenAt, idempotencyKey });
}
const existingRows =
prepared.length > 0
? await prisma.medicationIntakeEvent.findMany({
where: {
idempotencyKey: { in: prepared.map((p) => p.idempotencyKey) },
},
select: { idempotencyKey: true },
})
: [];
const existingKeys = new Set(
existingRows
.map((r) => r.idempotencyKey)
.filter((k): k is string => k !== null),
);
for (const { takenAt, idempotencyKey } of prepared) {
if (existingKeys.has(idempotencyKey)) {
skippedDuplicates++;
continue;
}
@@ -321,7 +321,8 @@ describe("POST /api/mood-entries/bulk — structured tagKeys (v1.12.0)", () => {
};
expect(json.data.inserted).toBe(1);
expect(prisma.moodEntry.findMany).toHaveBeenCalledTimes(1);
const findManyArg = vi.mocked(prisma.moodEntry.findMany).mock.calls[0]?.[0] as {
const findManyArg = vi.mocked(prisma.moodEntry.findMany).mock
.calls[0]?.[0] as {
where: { userId: string; OR: Array<Record<string, unknown>> };
};
expect(findManyArg.where.OR).toContainEqual({
@@ -349,7 +350,8 @@ describe("POST /api/mood-entries/bulk — structured tagKeys (v1.12.0)", () => {
const json = (await res.json()) as {
data: { entries: Array<{ externalId?: string }> };
};
const findManyArg = vi.mocked(prisma.moodEntry.findMany).mock.calls[0]?.[0] as {
const findManyArg = vi.mocked(prisma.moodEntry.findMany).mock
.calls[0]?.[0] as {
where: { OR: Array<Record<string, unknown>> };
};
expect(findManyArg.where.OR).toContainEqual({
+11 -4
View File
@@ -209,7 +209,10 @@ async function postBulk(request: NextRequest): Promise<Response> {
> = entries.map((entry) =>
entry.externalId
? { source: entry.source, externalId: entry.externalId }
: { date: moodDateKey(entry.moodLoggedAt, tz), moodLoggedAt: entry.moodLoggedAt },
: {
date: moodDateKey(entry.moodLoggedAt, tz),
moodLoggedAt: entry.moodLoggedAt,
},
);
const existingRows =
orClauses.length > 0
@@ -231,7 +234,10 @@ async function postBulk(request: NextRequest): Promise<Response> {
if (row.externalId !== null) {
existingByExternalKey.set(`${row.source}::${row.externalId}`, row);
}
existingByDateKey.set(`${row.date}::${row.moodLoggedAt.toISOString()}`, row);
existingByDateKey.set(
`${row.date}::${row.moodLoggedAt.toISOString()}`,
row,
);
}
for (let i = 0; i < entries.length; i++) {
@@ -271,8 +277,9 @@ async function postBulk(request: NextRequest): Promise<Response> {
const existing =
(entry.externalId
? existingByExternalKey.get(`${resolvedSource}::${entry.externalId}`)
: existingByDateKey.get(`${date}::${entry.moodLoggedAt.toISOString()}`)) ??
null;
: existingByDateKey.get(
`${date}::${entry.moodLoggedAt.toISOString()}`,
)) ?? null;
// Tombstone suppression: a soft-deleted match stays deleted. The
// user-facing DELETE route flips `deletedAt` so the `/api/sync/changes`
@@ -113,9 +113,8 @@ beforeEach(() => {
vi.mocked(prisma.nutrientIntakeDay.update).mockResolvedValue({} as never);
// Real Prisma resolves every promise in the `$transaction([...])` array;
// the mock does the same so the update path actually runs in these tests.
vi.mocked(prisma.$transaction).mockImplementation(
(async (ops: unknown[]) => Promise.all(ops)) as never,
);
vi.mocked(prisma.$transaction).mockImplementation((async (ops: unknown[]) =>
Promise.all(ops)) as never);
});
describe("POST /api/nutrients/batch — module gate", () => {
+1 -3
View File
@@ -35,9 +35,7 @@ vi.mock("@/lib/rollups/measurement-coverage", () => ({
}));
vi.mock("@/lib/insights/derived/baseline", () => ({
readDayMeanSeries: vi
.fn()
.mockResolvedValue({ points: [], source: "none" }),
readDayMeanSeries: vi.fn().mockResolvedValue({ points: [], source: "none" }),
}));
vi.mock("@/lib/analytics/intraday-pulse-io", () => ({