fix(unpair): allow whatsapp_groups delete by relaxing run_targets FK
Symptom
-------
Click Unpair on a connected account (or Delete Account). The web
action runs:
DELETE FROM whatsapp_groups WHERE account_id = ?
and Postgres rejects it:
error: update or delete on table "whatsapp_groups" violates
foreign key constraint
"reminder_run_targets_group_id_whatsapp_groups_id_fk"
on table "reminder_run_targets"
Cause
-----
\`reminder_run_targets.group_id\` had a non-null FK to
whatsapp_groups.id with no ON DELETE rule (defaults to NO ACTION /
RESTRICT). So any reminder that had ever fired pinned the group rows
in place. Unpair couldn't wipe the synced groups, the action threw,
and the row never reached \`status='unpaired'\`.
Fix
---
Mirror the pattern \`reminder_runs.reminder_id\` already uses
(migration 0005): nullable column + ON DELETE SET NULL + a
denormalised label snapshot, so historical fan-out records survive a
group wipe but stay readable.
Migration 0006:
- Drop the composite \`(run_id, group_id)\` PK; add a surrogate
\`id uuid pk default gen_random_uuid()\` since \`group_id\` can no
longer be part of the PK once it's nullable.
- Make \`group_id\` nullable.
- Re-create the FK with ON DELETE SET NULL.
- Add \`group_label text\` for the snapshot.
fire-reminder.ts now writes the group's name into \`group_label\`
on every insert path (success / failed / skipped /
account-not-connected / group-missing) so the Activity tab can keep
showing "Sent to <Group Name>" even after the group is gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
731d6d66a6
commit
52126765f4
@ -40,9 +40,14 @@ export async function fireReminder(payload: FireReminderPayload): Promise<void>
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
logger.warn({ reminderId: reminder.id }, "fire-reminder: account not connected");
|
logger.warn({ reminderId: reminder.id }, "fire-reminder: account not connected");
|
||||||
for (const target of reminder.targets) {
|
for (const target of reminder.targets) {
|
||||||
|
const g = await db.query.whatsappGroups.findFirst({
|
||||||
|
where: (g, { eq }) => eq(g.id, target.groupId),
|
||||||
|
columns: { name: true },
|
||||||
|
});
|
||||||
await db.insert(reminderRunTargets).values({
|
await db.insert(reminderRunTargets).values({
|
||||||
runId,
|
runId,
|
||||||
groupId: target.groupId,
|
groupId: target.groupId,
|
||||||
|
groupLabel: g?.name ?? null,
|
||||||
status: "skipped",
|
status: "skipped",
|
||||||
error: "account not connected",
|
error: "account not connected",
|
||||||
});
|
});
|
||||||
@ -64,6 +69,7 @@ export async function fireReminder(payload: FireReminderPayload): Promise<void>
|
|||||||
await db.insert(reminderRunTargets).values({
|
await db.insert(reminderRunTargets).values({
|
||||||
runId,
|
runId,
|
||||||
groupId: target.groupId,
|
groupId: target.groupId,
|
||||||
|
groupLabel: null,
|
||||||
status: "skipped",
|
status: "skipped",
|
||||||
error: "group missing from db",
|
error: "group missing from db",
|
||||||
});
|
});
|
||||||
@ -104,6 +110,7 @@ export async function fireReminder(payload: FireReminderPayload): Promise<void>
|
|||||||
await db.insert(reminderRunTargets).values({
|
await db.insert(reminderRunTargets).values({
|
||||||
runId,
|
runId,
|
||||||
groupId: target.groupId,
|
groupId: target.groupId,
|
||||||
|
groupLabel: group.name,
|
||||||
status: "sent",
|
status: "sent",
|
||||||
waMessageId: lastMessageId ?? null,
|
waMessageId: lastMessageId ?? null,
|
||||||
latencyMs: Date.now() - start,
|
latencyMs: Date.now() - start,
|
||||||
@ -114,6 +121,7 @@ export async function fireReminder(payload: FireReminderPayload): Promise<void>
|
|||||||
await db.insert(reminderRunTargets).values({
|
await db.insert(reminderRunTargets).values({
|
||||||
runId,
|
runId,
|
||||||
groupId: target.groupId,
|
groupId: target.groupId,
|
||||||
|
groupLabel: group.name,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: (err as Error).message,
|
error: (err as Error).message,
|
||||||
});
|
});
|
||||||
|
|||||||
11
packages/db/migrations/0006_adorable_nehzno.sql
Normal file
11
packages/db/migrations/0006_adorable_nehzno.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE "reminder_run_targets" DROP CONSTRAINT "reminder_run_targets_group_id_whatsapp_groups_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "reminder_run_targets" DROP CONSTRAINT "reminder_run_targets_run_id_group_id_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "reminder_run_targets" ALTER COLUMN "group_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "reminder_run_targets" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "reminder_run_targets" ADD COLUMN "group_label" text;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "reminder_run_targets" ADD CONSTRAINT "reminder_run_targets_group_id_whatsapp_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."whatsapp_groups"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1024
packages/db/migrations/meta/0006_snapshot.json
Normal file
1024
packages/db/migrations/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -43,6 +43,13 @@
|
|||||||
"when": 1778347437350,
|
"when": 1778347437350,
|
||||||
"tag": "0005_flippant_joystick",
|
"tag": "0005_flippant_joystick",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778385559051,
|
||||||
|
"tag": "0006_adorable_nehzno",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -122,20 +122,20 @@ export const reminderRuns = pgTable("reminder_runs", {
|
|||||||
errorSummary: text("error_summary"),
|
errorSummary: text("error_summary"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const reminderRunTargets = pgTable(
|
export const reminderRunTargets = pgTable("reminder_run_targets", {
|
||||||
"reminder_run_targets",
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
{
|
runId: uuid("run_id").notNull().references(() => reminderRuns.id, { onDelete: "cascade" }),
|
||||||
runId: uuid("run_id").notNull().references(() => reminderRuns.id, { onDelete: "cascade" }),
|
// Nullable + ON DELETE SET NULL: unpair/delete-account wipes the
|
||||||
groupId: uuid("group_id").notNull().references(() => whatsappGroups.id),
|
// associated whatsapp_groups rows but the historical fan-out record
|
||||||
status: text("status").notNull(),
|
// ("we tried to send to this group, here's the result") must survive.
|
||||||
waMessageId: text("wa_message_id"),
|
// The accompanying snapshot field below preserves the readable label.
|
||||||
error: text("error"),
|
groupId: uuid("group_id").references(() => whatsappGroups.id, { onDelete: "set null" }),
|
||||||
latencyMs: integer("latency_ms"),
|
groupLabel: text("group_label"),
|
||||||
},
|
status: text("status").notNull(),
|
||||||
(t) => ({
|
waMessageId: text("wa_message_id"),
|
||||||
pk: primaryKey({ columns: [t.runId, t.groupId] }),
|
error: text("error"),
|
||||||
}),
|
latencyMs: integer("latency_ms"),
|
||||||
);
|
});
|
||||||
|
|
||||||
export const auditLog = pgTable("audit_log", {
|
export const auditLog = pgTable("audit_log", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user