mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(mood): return tag keys from create and update
The list GET surfaces tagKeys per entry but the create and update responses omitted it, so a client hydrating from the response body showed an empty tag set until the next refetch. Read the persisted link keys back inside the write transaction and include them in both response bodies.
This commit is contained in:
1 parent
d5580917cc
commit
78d3478ad2
3 files changed
+102
-36
No files matched your search
@@ -139,21 +139,33 @@ export const PUT = apiHandler(
|
||||
// too — otherwise a client retry on the 5xx double-applies the edit
|
||||
// (and the `syncVersion` increment). The tx client is threaded through
|
||||
// the helper so both writes commit (or abort) together.
|
||||
const entry = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.moodEntry.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
const { entry, persistedTagKeys } = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const updated = await tx.moodEntry.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// Full replacement of the structured-tag link set when `tagKeys`
|
||||
// is present in the body. `null` clears every link; an omitted
|
||||
// field leaves the links untouched.
|
||||
if (data.tagKeys !== undefined) {
|
||||
await replaceTagLinks(id, data.tagKeys ?? [], tx);
|
||||
}
|
||||
// Full replacement of the structured-tag link set when `tagKeys`
|
||||
// is present in the body. `null` clears every link; an omitted
|
||||
// field leaves the links untouched.
|
||||
if (data.tagKeys !== undefined) {
|
||||
await replaceTagLinks(id, data.tagKeys ?? [], tx);
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
// v1.8.5 — read the persisted link keys back so the update
|
||||
// response mirrors the list GET shape exactly.
|
||||
const links = await tx.moodEntryTagLink.findMany({
|
||||
where: { moodEntryId: id },
|
||||
select: { moodTag: { select: { key: true } } },
|
||||
});
|
||||
|
||||
return {
|
||||
entry: updated,
|
||||
persistedTagKeys: links.map((link) => link.moodTag.key),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await auditLog("moodEntry.update", {
|
||||
userId: user.id,
|
||||
@@ -194,7 +206,14 @@ export const PUT = apiHandler(
|
||||
});
|
||||
}
|
||||
|
||||
return apiSuccess({ ...entry, tags: parseTags(entry.tags) });
|
||||
return apiSuccess({
|
||||
...entry,
|
||||
tags: parseTags(entry.tags),
|
||||
// v1.8.5 — surface the persisted structured-tag keys so a client
|
||||
// hydrating from the update response renders the tag set without a
|
||||
// refetch (shape-matches the list GET).
|
||||
tagKeys: persistedTagKeys,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -243,6 +243,30 @@ describe("POST /api/mood-entries — entry + tag-links transaction (v1.8.5)", ()
|
||||
expect(createTagLinks).toHaveBeenCalledWith("mood-1", ["happy"], txClient);
|
||||
});
|
||||
|
||||
it("returns the persisted tagKeys in the create response", async () => {
|
||||
txClient.moodEntry.create.mockResolvedValue({
|
||||
id: "mood-1",
|
||||
tags: null,
|
||||
moodLoggedAt: new Date(VALID_BODY.moodLoggedAt),
|
||||
mood: "GUT",
|
||||
note: null,
|
||||
source: "MANUAL",
|
||||
date: "2026-06-01",
|
||||
});
|
||||
txClient.moodEntryTagLink.findMany.mockResolvedValue([
|
||||
{ moodTag: { key: "happy" } },
|
||||
]);
|
||||
|
||||
const res = await POST(postReq(VALID_BODY));
|
||||
expect(res.status).toBe(201);
|
||||
const body = (await res.json()) as {
|
||||
data: { id: string; tagKeys: string[] };
|
||||
};
|
||||
// Read-after-write parity: the create response carries the same
|
||||
// `tagKeys` the list GET surfaces.
|
||||
expect(body.data.tagKeys).toEqual(["happy"]);
|
||||
});
|
||||
|
||||
it("rolls the entry back when the tag-link write fails (no commit)", async () => {
|
||||
txClient.moodEntry.create.mockResolvedValue({
|
||||
id: "mood-2",
|
||||
|
||||
@@ -172,29 +172,42 @@ async function postMoodEntry(request: NextRequest) {
|
||||
// tag-link failure must roll the entry back too — otherwise a client
|
||||
// retry on the 5xx mints a duplicate entry. The tx client is threaded
|
||||
// through the helper so both writes commit (or abort) together.
|
||||
const entry = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.moodEntry.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
date,
|
||||
tz,
|
||||
mood,
|
||||
score,
|
||||
tags: tags ? JSON.stringify(tags) : null,
|
||||
note: note ?? null,
|
||||
source: source ?? "MANUAL",
|
||||
moodLoggedAt,
|
||||
},
|
||||
});
|
||||
const { entry, persistedTagKeys } = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const created = await tx.moodEntry.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
date,
|
||||
tz,
|
||||
mood,
|
||||
score,
|
||||
tags: tags ? JSON.stringify(tags) : null,
|
||||
note: note ?? null,
|
||||
source: source ?? "MANUAL",
|
||||
moodLoggedAt,
|
||||
},
|
||||
});
|
||||
|
||||
if (tagKeys && tagKeys.length > 0) {
|
||||
// Unknown keys are dropped inside the helper (the catalog is the
|
||||
// source of truth).
|
||||
await createTagLinks(created.id, tagKeys, tx);
|
||||
}
|
||||
if (tagKeys && tagKeys.length > 0) {
|
||||
// Unknown keys are dropped inside the helper (the catalog is the
|
||||
// source of truth).
|
||||
await createTagLinks(created.id, tagKeys, tx);
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
// v1.8.5 — read the persisted link keys back so the create
|
||||
// response mirrors the list GET shape exactly (unknown keys
|
||||
// already filtered out).
|
||||
const links = await tx.moodEntryTagLink.findMany({
|
||||
where: { moodEntryId: created.id },
|
||||
select: { moodTag: { select: { key: true } } },
|
||||
});
|
||||
|
||||
return {
|
||||
entry: created,
|
||||
persistedTagKeys: links.map((link) => link.moodTag.key),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await auditLog("moodEntry.create", {
|
||||
userId: user.id,
|
||||
@@ -250,7 +263,17 @@ async function postMoodEntry(request: NextRequest) {
|
||||
// rejection in the Next.js runtime.
|
||||
});
|
||||
|
||||
return apiSuccess({ ...entry, tags: parseTags(entry.tags) }, 201);
|
||||
return apiSuccess(
|
||||
{
|
||||
...entry,
|
||||
tags: parseTags(entry.tags),
|
||||
// v1.8.5 — surface the persisted structured-tag keys so a client
|
||||
// hydrating from the create response renders the tag set without a
|
||||
// refetch (shape-matches the list GET).
|
||||
tagKeys: persistedTagKeys,
|
||||
},
|
||||
201,
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
|
||||
Reference in new issue
Block a user