110 lines
3.2 KiB
TypeScript

import { eq, sql } from "drizzle-orm";
import {
reminders,
reminderMessages,
reminderTargets,
type Reminder,
} from "@cmbot/db";
import { db } from "../db.js";
import { DEFAULT_TIMEZONE } from "@cmbot/shared";
export type CreateReminderInput = {
accountId: string;
groupId: string;
name: string;
scheduledAt: Date;
text?: string | null;
mediaId?: string | null;
caption?: string | null;
createdBy: string;
timezone?: string;
};
export type ReminderWithDetails = Reminder & {
targets: { groupId: string }[];
messages: { id: string; position: number; kind: string; textContent: string | null; mediaId: string | null }[];
};
export async function createReminder(input: CreateReminderInput): Promise<string> {
return await db.transaction(async (tx) => {
const [rem] = await tx
.insert(reminders)
.values({
accountId: input.accountId,
name: input.name,
scheduleKind: "one_off",
scheduledAt: input.scheduledAt,
timezone: input.timezone ?? DEFAULT_TIMEZONE,
status: "active",
createdBy: input.createdBy,
})
.returning({ id: reminders.id });
await tx.insert(reminderTargets).values({
reminderId: rem!.id,
groupId: input.groupId,
position: 0,
});
let position = 0;
if (input.text && !input.mediaId) {
await tx.insert(reminderMessages).values({
reminderId: rem!.id,
position: position++,
kind: "text",
textContent: input.text,
mediaId: null,
});
} else if (input.mediaId) {
await tx.insert(reminderMessages).values({
reminderId: rem!.id,
position: position++,
kind: "media",
textContent: input.caption ?? input.text ?? null,
mediaId: input.mediaId,
});
}
return rem!.id;
});
}
export async function getReminderWithDetails(id: string): Promise<ReminderWithDetails | null> {
const rem = await db.query.reminders.findFirst({
where: (r, { eq }) => eq(r.id, id),
});
if (!rem) return null;
const targets = await db.query.reminderTargets.findMany({
where: (t, { eq }) => eq(t.reminderId, id),
});
const messages = await db.query.reminderMessages.findMany({
where: (m, { eq }) => eq(m.reminderId, id),
orderBy: (m, { asc }) => [asc(m.position)],
});
return { ...rem, targets, messages };
}
export async function listRemindersForOperator(
operatorId: string,
limit = 50,
): Promise<(Reminder & { accountLabel: string; groupCount: number })[]> {
// Use parameterized SQL via drizzle's sql tag for safety. operatorId is a
// server-controlled UUID, but parameterizing is the right habit anyway.
const rows = await db.execute(sql`
SELECT
r.*,
wa.label as account_label,
(SELECT count(*) FROM reminder_targets rt WHERE rt.reminder_id = r.id) as group_count
FROM reminders r
JOIN whatsapp_accounts wa ON wa.id = r.account_id
WHERE wa.operator_id = ${operatorId}
ORDER BY r.scheduled_at DESC NULLS LAST, r.created_at DESC
LIMIT ${limit}
`);
return rows.rows as never;
}
export async function deleteReminder(id: string): Promise<void> {
await db.delete(reminders).where(eq(reminders.id, id));
}