Files
famapp/src/modules/pets/server/actions.ts
T

382 lines
13 KiB
TypeScript

"use server";
import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import type { ApiAuthContext } from "@/lib/api-auth";
import { db } from "@/lib/db";
import { getCurrentSession } from "@/lib/session";
import { logActivityForScope } from "@/modules/_core/activity";
import { cancelReminder, listReminderOffsets } from "@/modules/_core/reminders";
import { petAppointments, petPrescriptions, pets, petVaccinations } from "../schema";
import { syncPetAppointmentReminders, syncPetDueDateReminder } from "./reminders";
import {
appointmentInput,
appointmentUpdateInput,
petInput,
petUpdateInput,
prescriptionInput,
prescriptionUpdateInput,
vaccinationInput,
vaccinationUpdateInput,
} from "./schemas";
type Scope = Pick<ApiAuthContext, "householdId" | "userId">;
const idInput = z.object({ id: z.string().uuid() });
async function requirePet(scope: Scope, id: string) {
const [pet] = await db
.select()
.from(pets)
.where(and(eq(pets.id, id), eq(pets.householdId, scope.householdId)))
.limit(1);
if (!pet) throw new Error("Pet not found");
return pet;
}
async function requireAppointment(scope: Scope, id: string) {
const [row] = await db
.select()
.from(petAppointments)
.where(and(eq(petAppointments.id, id), eq(petAppointments.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Record not found");
return row;
}
async function requireVaccination(scope: Scope, id: string) {
const [row] = await db
.select()
.from(petVaccinations)
.where(and(eq(petVaccinations.id, id), eq(petVaccinations.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Record not found");
return row;
}
async function requirePrescription(scope: Scope, id: string) {
const [row] = await db
.select()
.from(petPrescriptions)
.where(and(eq(petPrescriptions.id, id), eq(petPrescriptions.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Record not found");
return row;
}
function revalidatePet(id: string) {
revalidatePath("/pets");
revalidatePath(`/pets/${id}`);
}
export async function createPetForScope(scope: Scope, input: z.input<typeof petInput>) {
const parsed = petInput.parse(input);
const [pet] = await db
.insert(pets)
.values({
householdId: scope.householdId,
...parsed,
breed: parsed.breed ?? null,
birthDate: parsed.birthDate ?? null,
notes: parsed.notes ?? null,
primaryImageUrl: parsed.primaryImageUrl ?? parsed.images[0] ?? null,
})
.returning();
if (!pet) throw new Error("Pet was not created");
await logActivityForScope(scope, {
entityType: "pets.pet",
entityId: pet.id,
action: "create",
payload: { name: pet.name },
});
return pet;
}
export async function createPet(input: z.input<typeof petInput>) {
const { household, user } = await getCurrentSession();
const pet = await createPetForScope({ householdId: household.id, userId: user.id }, input);
revalidatePet(pet.id);
return pet;
}
export async function updatePetForScope(
scope: Scope,
input: { id: string } & z.input<typeof petUpdateInput>,
) {
const parsed = idInput.and(petUpdateInput).parse(input);
await requirePet(scope, parsed.id);
const { id, ...changes } = parsed;
await db
.update(pets)
.set({ ...changes, updatedAt: new Date() })
.where(eq(pets.id, id));
await logActivityForScope(scope, { entityType: "pets.pet", entityId: id, action: "update" });
}
export async function updatePet(input: { id: string } & z.input<typeof petUpdateInput>) {
const { household, user } = await getCurrentSession();
await updatePetForScope({ householdId: household.id, userId: user.id }, input);
revalidatePet(input.id);
}
export async function deletePetForScope(scope: Scope, input: { id: string }) {
const { id } = idInput.parse(input);
const pet = await requirePet(scope, id);
await logActivityForScope(scope, {
entityType: "pets.pet",
entityId: id,
action: "delete",
payload: { name: pet.name },
});
await db.delete(pets).where(eq(pets.id, id));
}
export async function deletePet(input: { id: string }) {
const { household, user } = await getCurrentSession();
await deletePetForScope({ householdId: household.id, userId: user.id }, input);
revalidatePath("/pets");
}
export async function createAppointmentForScope(
scope: Scope,
input: z.input<typeof appointmentInput>,
) {
const parsed = appointmentInput.parse(input);
const pet = await requirePet(scope, parsed.petId);
const [row] = await db
.insert(petAppointments)
.values({
householdId: scope.householdId,
petId: parsed.petId,
title: parsed.title,
appointmentAt: parsed.appointmentAt,
clinic: parsed.clinic ?? null,
notes: parsed.notes ?? null,
})
.returning();
if (!row) throw new Error("Appointment was not created");
if (scope.userId)
await syncPetAppointmentReminders({
householdId: scope.householdId,
appointmentId: row.id,
appointmentAt: row.appointmentAt,
createdBy: scope.userId,
petName: pet.name,
title: row.title,
offsets: parsed.reminderOffsets,
});
return row;
}
export async function updateAppointmentForScope(
scope: Scope,
input: { id: string } & z.input<typeof appointmentUpdateInput>,
) {
const parsed = idInput.and(appointmentUpdateInput).parse(input);
const row = await requireAppointment(scope, parsed.id);
const pet = await requirePet(scope, row.petId);
const offsets = parsed.reminderOffsets ?? (await listReminderOffsets("pets.appointment", row.id));
const { id, reminderOffsets: _, ...changes } = parsed;
const [updated] = await db
.update(petAppointments)
.set({ ...changes, updatedAt: new Date() })
.where(eq(petAppointments.id, id))
.returning();
if (!updated) throw new Error("Appointment not found");
if (scope.userId)
await syncPetAppointmentReminders({
householdId: scope.householdId,
appointmentId: id,
appointmentAt: updated.appointmentAt,
createdBy: scope.userId,
petName: pet.name,
title: updated.title,
offsets,
});
return updated;
}
export async function deleteAppointmentForScope(scope: Scope, input: { id: string }) {
const { id } = idInput.parse(input);
await requireAppointment(scope, id);
await cancelReminder("pets.appointment", id);
await db.delete(petAppointments).where(eq(petAppointments.id, id));
}
export async function createAppointment(input: z.input<typeof appointmentInput>) {
const { household, user } = await getCurrentSession();
const row = await createAppointmentForScope(
{ householdId: household.id, userId: user.id },
input,
);
revalidatePet(row.petId);
return row;
}
export async function updateAppointment(
input: { id: string } & z.input<typeof appointmentUpdateInput>,
) {
const { household, user } = await getCurrentSession();
const row = await updateAppointmentForScope(
{ householdId: household.id, userId: user.id },
input,
);
revalidatePet(row.petId);
return row;
}
export async function deleteAppointment(input: { id: string }) {
const { household, user } = await getCurrentSession();
const row = await requireAppointment({ householdId: household.id, userId: user.id }, input.id);
await deleteAppointmentForScope({ householdId: household.id, userId: user.id }, input);
revalidatePet(row.petId);
}
export async function createVaccinationForScope(
scope: Scope,
input: z.input<typeof vaccinationInput>,
) {
const parsed = vaccinationInput.parse(input);
const pet = await requirePet(scope, parsed.petId);
const [row] = await db
.insert(petVaccinations)
.values({
householdId: scope.householdId,
...parsed,
dueOn: parsed.dueOn ?? null,
provider: parsed.provider ?? null,
notes: parsed.notes ?? null,
})
.returning();
if (!row) throw new Error("Vaccination was not created");
if (scope.userId)
await syncPetDueDateReminder({
householdId: scope.householdId,
entityType: "pets.vaccination",
entityId: row.id,
dueOn: row.dueOn,
createdBy: scope.userId,
title: `Vaccination due: ${pet.name}`,
body: row.name,
});
return row;
}
export async function updateVaccinationForScope(
scope: Scope,
input: { id: string } & z.input<typeof vaccinationUpdateInput>,
) {
const parsed = idInput.and(vaccinationUpdateInput).parse(input);
const row = await requireVaccination(scope, parsed.id);
const pet = await requirePet(scope, row.petId);
const { id, ...changes } = parsed;
const [updated] = await db
.update(petVaccinations)
.set({ ...changes, updatedAt: new Date() })
.where(eq(petVaccinations.id, id))
.returning();
if (!updated) throw new Error("Vaccination not found");
if (scope.userId)
await syncPetDueDateReminder({
householdId: scope.householdId,
entityType: "pets.vaccination",
entityId: id,
dueOn: updated.dueOn,
createdBy: scope.userId,
title: `Vaccination due: ${pet.name}`,
body: updated.name,
});
return updated;
}
export async function deleteVaccinationForScope(scope: Scope, input: { id: string }) {
const { id } = idInput.parse(input);
await requireVaccination(scope, id);
await cancelReminder("pets.vaccination", id);
await db.delete(petVaccinations).where(eq(petVaccinations.id, id));
}
export async function createVaccination(input: z.input<typeof vaccinationInput>) {
const { household, user } = await getCurrentSession();
const row = await createVaccinationForScope(
{ householdId: household.id, userId: user.id },
input,
);
revalidatePet(row.petId);
return row;
}
export async function deleteVaccination(input: { id: string }) {
const { household, user } = await getCurrentSession();
const row = await requireVaccination({ householdId: household.id, userId: user.id }, input.id);
await deleteVaccinationForScope({ householdId: household.id, userId: user.id }, input);
revalidatePet(row.petId);
}
export async function createPrescriptionForScope(
scope: Scope,
input: z.input<typeof prescriptionInput>,
) {
const parsed = prescriptionInput.parse(input);
const pet = await requirePet(scope, parsed.petId);
const [row] = await db
.insert(petPrescriptions)
.values({
householdId: scope.householdId,
...parsed,
instructions: parsed.instructions ?? null,
prescribedOn: parsed.prescribedOn ?? null,
expiresOn: parsed.expiresOn ?? null,
refillsRemaining: parsed.refillsRemaining ?? null,
})
.returning();
if (!row) throw new Error("Prescription was not created");
if (scope.userId)
await syncPetDueDateReminder({
householdId: scope.householdId,
entityType: "pets.prescription",
entityId: row.id,
dueOn: row.active ? row.expiresOn : null,
createdBy: scope.userId,
title: `Prescription due: ${pet.name}`,
body: row.medication,
});
return row;
}
export async function updatePrescriptionForScope(
scope: Scope,
input: { id: string } & z.input<typeof prescriptionUpdateInput>,
) {
const parsed = idInput.and(prescriptionUpdateInput).parse(input);
const row = await requirePrescription(scope, parsed.id);
const pet = await requirePet(scope, row.petId);
const { id, ...changes } = parsed;
const [updated] = await db
.update(petPrescriptions)
.set({ ...changes, updatedAt: new Date() })
.where(eq(petPrescriptions.id, id))
.returning();
if (!updated) throw new Error("Prescription not found");
if (scope.userId)
await syncPetDueDateReminder({
householdId: scope.householdId,
entityType: "pets.prescription",
entityId: id,
dueOn: updated.active ? updated.expiresOn : null,
createdBy: scope.userId,
title: `Prescription due: ${pet.name}`,
body: updated.medication,
});
return updated;
}
export async function deletePrescriptionForScope(scope: Scope, input: { id: string }) {
const { id } = idInput.parse(input);
await requirePrescription(scope, id);
await cancelReminder("pets.prescription", id);
await db.delete(petPrescriptions).where(eq(petPrescriptions.id, id));
}
export async function createPrescription(input: z.input<typeof prescriptionInput>) {
const { household, user } = await getCurrentSession();
const row = await createPrescriptionForScope(
{ householdId: household.id, userId: user.id },
input,
);
revalidatePet(row.petId);
return row;
}
export async function deletePrescription(input: { id: string }) {
const { household, user } = await getCurrentSession();
const row = await requirePrescription({ householdId: household.id, userId: user.id }, input.id);
await deletePrescriptionForScope({ householdId: household.id, userId: user.id }, input);
revalidatePet(row.petId);
}