yiekheng 52126765f4 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>
2026-05-10 12:01:16 +08:00

1024 lines
26 KiB
JSON

{
"id": "7e3e4181-1c77-4152-97a2-f3f51ec42452",
"prevId": "7ac909ad-0e99-4069-a47a-762b75d375de",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.audit_log": {
"name": "audit_log",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"operator_id": {
"name": "operator_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"source": {
"name": "source",
"type": "text",
"primaryKey": false,
"notNull": true
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true
},
"target_type": {
"name": "target_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"target_id": {
"name": "target_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"payload": {
"name": "payload",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"audit_log_operator_id_operators_id_fk": {
"name": "audit_log_operator_id_operators_id_fk",
"tableFrom": "audit_log",
"tableTo": "operators",
"columnsFrom": [
"operator_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.auth_sessions": {
"name": "auth_sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"operator_id": {
"name": "operator_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"ip_address": {
"name": "ip_address",
"type": "inet",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"auth_sessions_operator_id_operators_id_fk": {
"name": "auth_sessions_operator_id_operators_id_fk",
"tableFrom": "auth_sessions",
"tableTo": "operators",
"columnsFrom": [
"operator_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"auth_sessions_token_hash_unique": {
"name": "auth_sessions_token_hash_unique",
"nullsNotDistinct": false,
"columns": [
"token_hash"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.cache_entries": {
"name": "cache_entries",
"schema": "",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true
},
"value": {
"name": "value",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.media_files": {
"name": "media_files",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"operator_id": {
"name": "operator_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"filename_original": {
"name": "filename_original",
"type": "text",
"primaryKey": false,
"notNull": true
},
"mime_type": {
"name": "mime_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"size_bytes": {
"name": "size_bytes",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"sha256": {
"name": "sha256",
"type": "text",
"primaryKey": false,
"notNull": true
},
"storage_path": {
"name": "storage_path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"media_files_operator_id_operators_id_fk": {
"name": "media_files_operator_id_operators_id_fk",
"tableFrom": "media_files",
"tableTo": "operators",
"columnsFrom": [
"operator_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.operators": {
"name": "operators",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"telegram_user_id": {
"name": "telegram_user_id",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'admin'"
},
"default_timezone": {
"name": "default_timezone",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'Asia/Kuala_Lumpur'"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"operators_telegram_user_id_uq": {
"name": "operators_telegram_user_id_uq",
"columns": [
{
"expression": "telegram_user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rate_limit_buckets": {
"name": "rate_limit_buckets",
"schema": "",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true
},
"window_start": {
"name": "window_start",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reminder_messages": {
"name": "reminder_messages",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"reminder_id": {
"name": "reminder_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"text_content": {
"name": "text_content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"media_id": {
"name": "media_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"reminder_messages_reminder_id_reminders_id_fk": {
"name": "reminder_messages_reminder_id_reminders_id_fk",
"tableFrom": "reminder_messages",
"tableTo": "reminders",
"columnsFrom": [
"reminder_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"reminder_messages_media_id_media_files_id_fk": {
"name": "reminder_messages_media_id_media_files_id_fk",
"tableFrom": "reminder_messages",
"tableTo": "media_files",
"columnsFrom": [
"media_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reminder_run_targets": {
"name": "reminder_run_targets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"group_id": {
"name": "group_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"group_label": {
"name": "group_label",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true
},
"wa_message_id": {
"name": "wa_message_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"latency_ms": {
"name": "latency_ms",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"reminder_run_targets_run_id_reminder_runs_id_fk": {
"name": "reminder_run_targets_run_id_reminder_runs_id_fk",
"tableFrom": "reminder_run_targets",
"tableTo": "reminder_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"reminder_run_targets_group_id_whatsapp_groups_id_fk": {
"name": "reminder_run_targets_group_id_whatsapp_groups_id_fk",
"tableFrom": "reminder_run_targets",
"tableTo": "whatsapp_groups",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reminder_runs": {
"name": "reminder_runs",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"reminder_id": {
"name": "reminder_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"reminder_name": {
"name": "reminder_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"fired_at": {
"name": "fired_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true
},
"error_summary": {
"name": "error_summary",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"reminder_runs_reminder_id_reminders_id_fk": {
"name": "reminder_runs_reminder_id_reminders_id_fk",
"tableFrom": "reminder_runs",
"tableTo": "reminders",
"columnsFrom": [
"reminder_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reminder_targets": {
"name": "reminder_targets",
"schema": "",
"columns": {
"reminder_id": {
"name": "reminder_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"group_id": {
"name": "group_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"reminder_targets_reminder_id_reminders_id_fk": {
"name": "reminder_targets_reminder_id_reminders_id_fk",
"tableFrom": "reminder_targets",
"tableTo": "reminders",
"columnsFrom": [
"reminder_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"reminder_targets_group_id_whatsapp_groups_id_fk": {
"name": "reminder_targets_group_id_whatsapp_groups_id_fk",
"tableFrom": "reminder_targets",
"tableTo": "whatsapp_groups",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"reminder_targets_reminder_id_group_id_pk": {
"name": "reminder_targets_reminder_id_group_id_pk",
"columns": [
"reminder_id",
"group_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.reminders": {
"name": "reminders",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"schedule_kind": {
"name": "schedule_kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"scheduled_at": {
"name": "scheduled_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"rrule": {
"name": "rrule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ends_at": {
"name": "ends_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"max_runs": {
"name": "max_runs",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'active'"
},
"created_by": {
"name": "created_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"last_fired_at": {
"name": "last_fired_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"reminders_account_id_whatsapp_accounts_id_fk": {
"name": "reminders_account_id_whatsapp_accounts_id_fk",
"tableFrom": "reminders",
"tableTo": "whatsapp_accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"reminders_created_by_operators_id_fk": {
"name": "reminders_created_by_operators_id_fk",
"tableFrom": "reminders",
"tableTo": "operators",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.whatsapp_accounts": {
"name": "whatsapp_accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"operator_id": {
"name": "operator_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"label": {
"name": "label",
"type": "text",
"primaryKey": false,
"notNull": true
},
"phone_number": {
"name": "phone_number",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"last_connected_at": {
"name": "last_connected_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_qr_at": {
"name": "last_qr_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_qr_png": {
"name": "last_qr_png",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"whatsapp_accounts_operator_label_uq": {
"name": "whatsapp_accounts_operator_label_uq",
"columns": [
{
"expression": "operator_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "label",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"whatsapp_accounts_operator_id_operators_id_fk": {
"name": "whatsapp_accounts_operator_id_operators_id_fk",
"tableFrom": "whatsapp_accounts",
"tableTo": "operators",
"columnsFrom": [
"operator_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.whatsapp_groups": {
"name": "whatsapp_groups",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"wa_group_jid": {
"name": "wa_group_jid",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"participant_count": {
"name": "participant_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"is_archived": {
"name": "is_archived",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"last_synced_at": {
"name": "last_synced_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"whatsapp_groups_account_jid_uq": {
"name": "whatsapp_groups_account_jid_uq",
"columns": [
{
"expression": "account_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "wa_group_jid",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"whatsapp_groups_account_id_whatsapp_accounts_id_fk": {
"name": "whatsapp_groups_account_id_whatsapp_accounts_id_fk",
"tableFrom": "whatsapp_groups",
"tableTo": "whatsapp_accounts",
"columnsFrom": [
"account_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}