Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type * as generatedGermanText from "../generatedGermanText.js";
import type * as generatedGermanTextRepair from "../generatedGermanTextRepair.js";
import type * as learningPlanAi from "../learningPlanAi.js";
import type * as learningPlans from "../learningPlans.js";
import type * as learningTimes from "../learningTimes.js";
import type * as scheduleConflicts from "../scheduleConflicts.js";
import type * as users from "../users.js";

Expand All @@ -34,6 +35,7 @@ declare const fullApi: ApiFromModules<{
generatedGermanTextRepair: typeof generatedGermanTextRepair;
learningPlanAi: typeof learningPlanAi;
learningPlans: typeof learningPlans;
learningTimes: typeof learningTimes;
scheduleConflicts: typeof scheduleConflicts;
users: typeof users;
}>;
Expand Down
24 changes: 24 additions & 0 deletions convex/dayEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,27 @@ test("manual timed entry adjacent to an existing entry is allowed", async () =>

expect(createdId).toBeTruthy();
});

test("retrying the same create returns the existing entry instead of conflicting with itself", async () => {
const t = convexTest(schema, modules).withIdentity(user);

const payload = {
dayKey: "2026-06-15",
title: "Informatik Mündliche Prüfung",
time: "12:00",
kind: "Leistungskontrolle",
plannedDateLabel: "Montag, 15. Juni",
durationMinutes: 30,
examTypeLabel: "Mündliche Prüfung",
};

const createdId = await t.mutation(api.dayEntries.create, payload);
const retriedId = await t.mutation(api.dayEntries.create, payload);

expect(retriedId).toBe(createdId);

const entries = await t.query(api.dayEntries.listByDayKeys, {
dayKeys: ["2026-06-15"],
});
expect(entries["2026-06-15"]).toHaveLength(1);
});
71 changes: 70 additions & 1 deletion convex/dayEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,60 @@ const getRequestedDayKey = (
return berlinDayKey ? queryKeyToRequestedDayKey.get(berlinDayKey) : undefined;
};

const optionalValuesMatch = <TValue>(
left: TValue | undefined,
right: TValue | undefined,
) => (left ?? undefined) === (right ?? undefined);

const isSameCreatePayload = (
entry: Doc<"dayEntries">,
args: OptionalEntryFields & { title: string },
) =>
entry.title === args.title &&
optionalValuesMatch(entry.time, args.time) &&
optionalValuesMatch(entry.kind, args.kind) &&
optionalValuesMatch(entry.notes, args.notes) &&
optionalValuesMatch(entry.dueDateKey, args.dueDateKey) &&
optionalValuesMatch(entry.dueDateLabel, args.dueDateLabel) &&
optionalValuesMatch(entry.plannedDateLabel, args.plannedDateLabel) &&
optionalValuesMatch(entry.durationMinutes, args.durationMinutes) &&
optionalValuesMatch(entry.examTypeLabel, args.examTypeLabel) &&
optionalValuesMatch(entry.completed, args.completed) &&
optionalValuesMatch(entry.relatedLearningPlanId, args.relatedLearningPlanId) &&
optionalValuesMatch(
entry.relatedLearningPlanSessionId,
args.relatedLearningPlanSessionId,
);

const findExistingSameEntry = async (
ctx: QueryCtx | MutationCtx,
{
ownerTokenIdentifier,
dayKey,
args,
}: {
ownerTokenIdentifier: string;
dayKey: string;
args: OptionalEntryFields & { title: string };
},
) => {
for (const queryDayKey of getDayKeyQueryVariants(dayKey)) {
const entries = await ctx.db
.query("dayEntries")
.withIndex("by_ownerTokenIdentifier_and_dayKey", (q) =>
q
.eq("ownerTokenIdentifier", ownerTokenIdentifier)
.eq("dayKey", queryDayKey),
)
.take(100);

const existing = entries.find((entry) => isSameCreatePayload(entry, args));
if (existing) return existing;
}

return null;
};

const entryFields = {
title: v.string(),
time: v.optional(v.string()),
Expand Down Expand Up @@ -179,7 +233,13 @@ export const listByDayKeys = query({
plan = await ctx.db.get("learningPlans", session.learningPlanId);
planCache.set(session.learningPlanId, plan);
}
if (!plan || plan.ownerTokenIdentifier !== ownerTokenIdentifier) continue;
if (
!plan ||
plan.ownerTokenIdentifier !== ownerTokenIdentifier ||
plan.status !== "accepted"
) {
continue;
}

grouped[requestedDayKey] = [
...(grouped[requestedDayKey] ?? []),
Expand Down Expand Up @@ -217,6 +277,15 @@ export const create = mutation({
if (!title) {
throw new Error("Titel darf nicht leer sein.");
}
const existingSameEntry = await findExistingSameEntry(ctx, {
ownerTokenIdentifier,
dayKey: args.dayKey,
args: { ...args, title },
});
if (existingSameEntry) {
return existingSameEntry._id;
}

await assertNoScheduleConflict(ctx, {
ownerTokenIdentifier,
dayKey: args.dayKey,
Expand Down
Loading