mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(workouts): wire batch route through user source priority
The batch handler now looks up `User.sourcePriorityJson` once at the start of the request and threads it into the write-time canonical-row picker. A user with a customised Settings → Sources ladder keeps their preferred row across the ingest pipeline; the route picks up the v1.4.43 helper's user-aware path with one indexed findUnique per batch.
This commit is contained in:
1 parent
d1be9f0177
commit
0fbe68a313
2 files changed
+86
No files matched your search
@@ -21,6 +21,9 @@ vi.mock("@/lib/db", () => ({
|
||||
workoutRoute: {
|
||||
createMany: vi.fn(),
|
||||
},
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(async (fn: unknown) => {
|
||||
// Same shape as the real Prisma client — invoke the supplied
|
||||
// callback with the mocked client itself so `tx.workout.createMany`
|
||||
@@ -120,6 +123,12 @@ beforeEach(() => {
|
||||
vi.mocked(prisma.workout.createMany).mockResolvedValue({ count: 0 });
|
||||
vi.mocked(prisma.workout.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workoutRoute.createMany).mockResolvedValue({ count: 0 });
|
||||
// v1.4.43 W9 — default to no per-user source-priority override so
|
||||
// the write-time picker walks the canonical default ladder. Tests
|
||||
// that exercise a custom ladder override this in-line.
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
sourcePriorityJson: null,
|
||||
} as never);
|
||||
});
|
||||
|
||||
describe("POST /api/workouts/batch — auth + size + rate-limit", () => {
|
||||
@@ -377,6 +386,69 @@ describe("POST /api/workouts/batch — nested route attachment", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/workouts/batch — user source-priority (v1.4.43 W9)", () => {
|
||||
it("looks up the user's source-priority blob exactly once per batch", async () => {
|
||||
vi.mocked(prisma.workout.createMany).mockResolvedValue({ count: 1 });
|
||||
const res = await POST(
|
||||
makeRequest({
|
||||
workouts: [
|
||||
validWorkout("uuid-prio-1"),
|
||||
validWorkout("uuid-prio-2", {
|
||||
startedAt: "2026-05-14T08:30:00.000Z",
|
||||
endedAt: "2026-05-14T09:15:00.000Z",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user-1" },
|
||||
select: { sourcePriorityJson: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("threads a custom MANUAL>APPLE_HEALTH ladder so the user's preferred row survives the write-time dedup", async () => {
|
||||
// User has flipped the default in Settings. Two rows for the same
|
||||
// logical run inside the 90 s write-time window — MANUAL must
|
||||
// win because the user told us so.
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
sourcePriorityJson: {
|
||||
steps: ["MANUAL", "APPLE_HEALTH", "WITHINGS"],
|
||||
},
|
||||
} as never);
|
||||
vi.mocked(prisma.workout.createMany).mockResolvedValue({ count: 1 });
|
||||
|
||||
const res = await POST(
|
||||
makeRequest({
|
||||
workouts: [
|
||||
validWorkout("uuid-apple", { source: "APPLE_HEALTH" }),
|
||||
validWorkout("uuid-manual", {
|
||||
source: "MANUAL",
|
||||
startedAt: "2026-05-14T06:31:00.000Z",
|
||||
endedAt: "2026-05-14T07:16:00.000Z",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
data: {
|
||||
inserted: number;
|
||||
duplicates: number;
|
||||
entries: Array<{ index: number; status: string }>;
|
||||
};
|
||||
};
|
||||
// One survivor (MANUAL), one dropped twin (APPLE_HEALTH).
|
||||
expect(body.data.inserted).toBe(1);
|
||||
expect(body.data.duplicates).toBe(1);
|
||||
// The Apple row at index 0 is the duplicate; the Manual row at
|
||||
// index 1 is the survivor.
|
||||
expect(body.data.entries[0]?.status).toBe("duplicate");
|
||||
expect(body.data.entries[1]?.status).toBe("inserted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/workouts/batch — PR detection enqueue (v1.4.25 W16c)", () => {
|
||||
it("enqueues PR detection after a successful batch with silent=false", async () => {
|
||||
vi.mocked(prisma.workout.createMany).mockResolvedValue({ count: 1 });
|
||||
|
||||
@@ -119,6 +119,19 @@ export const POST = apiHandler(withIdempotency<[NextRequest]>(postBatch));
|
||||
async function postBatch(request: NextRequest): Promise<Response> {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
// v1.4.43 W9 — resolve the per-user source-priority blob once at
|
||||
// the start of the handler so the write-time canonical-row picker
|
||||
// walks the SAME ladder the read-time picker walks. Without this
|
||||
// lookup a user who customised Settings → Sources (e.g. MANUAL >
|
||||
// APPLE_HEALTH) would see their preferred row dropped at write-
|
||||
// time before the read-time picker ever ran. One indexed
|
||||
// findUnique per batch — well under the cost of letting the
|
||||
// duplicate persist and carrying it forward on every read.
|
||||
const userPriority = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { sourcePriorityJson: true },
|
||||
});
|
||||
|
||||
// Content-Length pre-flight. The body cap exists to bound the
|
||||
// request-pipeline cost BEFORE Node has to read the full payload —
|
||||
// a misbehaving client shipping a 50 MB body shouldn't tie up the
|
||||
@@ -287,6 +300,7 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
// falls through to input order via the `index` field below.
|
||||
index: p.index,
|
||||
})),
|
||||
userPriority?.sourcePriorityJson ?? null,
|
||||
);
|
||||
const survivingIndices = new Set(canonicalPrepared.map((r) => r.index ?? -1));
|
||||
const droppedByWriteDedup: number[] = [];
|
||||
|
||||
Reference in new issue
Block a user