fix: store username on moderation_actions for retention safety

Add denormalized  column to moderation_actions so the
dashboard TARGET field survives message table purges. Backfills all
existing 217 rows. Changes: schema + autoDeleteManager + backend
repository query + migration 0017.
This commit is contained in:
asepharyana
2026-08-27 12:15:06 +07:00
parent 17aad2cc39
commit cccfd89266
8 changed files with 24 additions and 2 deletions
@@ -124,7 +124,7 @@ export class ModerationRepository {
a.score, a.score,
a.evidence, a.evidence,
a.policy_version, a.policy_version,
m.username, a.username,
LEFT(m.content, 300) AS content LEFT(m.content, 300) AS content
FROM moderation_actions a FROM moderation_actions a
LEFT JOIN messages m ON m.id = a.message_id LEFT JOIN messages m ON m.id = a.message_id
@@ -314,7 +314,7 @@ export class ModerationRepository {
SELECT SELECT
a.id, a.message_id, a.user_id, a.guild_id, a.action_type, a.id, a.message_id, a.user_id, a.guild_id, a.action_type,
a.reason, a.status, a.created_at, a.severity, a.confidence, a.score, a.reason, a.status, a.created_at, a.severity, a.confidence, a.score,
m.username, LEFT(m.content, 300) AS content a.username, LEFT(m.content, 300) AS content
FROM moderation_actions a FROM moderation_actions a
LEFT JOIN messages m ON m.id = a.message_id LEFT JOIN messages m ON m.id = a.message_id
WHERE a.created_at >= ${since} WHERE a.created_at >= ${since}
@@ -0,0 +1,9 @@
ALTER TABLE "moderation_actions" ADD COLUMN "username" text;
--> statement-breakpoint
-- Backfill username from messages table for existing actions
UPDATE "moderation_actions" a
SET "username" = m."username"
FROM "messages" m
WHERE a."message_id" = m."id"
AND a."username" IS NULL
AND m."username" IS NOT NULL;
@@ -120,6 +120,13 @@
"when": 1787185000000, "when": 1787185000000,
"tag": "0016_drop_user_reputations", "tag": "0016_drop_user_reputations",
"breakpoints": true "breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1787810000000,
"tag": "0017_add_username_to_moderation_actions",
"breakpoints": true
} }
] ]
} }
@@ -197,6 +197,7 @@ async function logAutoDeleteAttempt(
action_type: "delete_message", action_type: "delete_message",
reason: result.reason, reason: result.reason,
...verdictToActionFields(message), ...verdictToActionFields(message),
username: message.username,
executed_by: "auto-delete-manager", executed_by: "auto-delete-manager",
status: result.deleted status: result.deleted
? "executed" ? "executed"
@@ -262,6 +263,7 @@ export async function attemptAutoDeleteFlaggedMessage(
reason: reason:
"nickname melanggar aturan server (offensive_username); pesan dibiarkan", "nickname melanggar aturan server (offensive_username); pesan dibiarkan",
...verdictToActionFields(message), ...verdictToActionFields(message),
username: message.username,
executed_by: "auto-delete-manager", executed_by: "auto-delete-manager",
status: resetOk ? "executed" : "failed", status: resetOk ? "executed" : "failed",
error: resetOk ? null : "nickname_reset_failed", error: resetOk ? null : "nickname_reset_failed",
@@ -103,6 +103,7 @@ export class ModerationHandler {
| "kick_user" | "kick_user"
| "ban_user", | "ban_user",
reason: payload.reason ?? null, reason: payload.reason ?? null,
username: null,
executed_by: payload.executed_by ?? "command-handler", executed_by: payload.executed_by ?? "command-handler",
status: "executed", status: "executed",
error: null, error: null,
@@ -642,6 +642,7 @@ export const pgModerationActionsTable = pgTable(
score: pgReal("score"), // 0..1 raw model score score: pgReal("score"), // 0..1 raw model score
evidence: pgText("evidence"), // JSON array of short quoted snippets evidence: pgText("evidence"), // JSON array of short quoted snippets
policy_version: pgText("policy_version"), // rules.ts policy version string policy_version: pgText("policy_version"), // rules.ts policy version string
username: pgText("username"), // denormalized from messages table for retention safety
}, },
(table) => ({ (table) => ({
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on( messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
@@ -59,6 +59,7 @@ export const pgModerationActionsTable = pgTable(
score: pgReal("score"), // 0..1 raw model score score: pgReal("score"), // 0..1 raw model score
evidence: pgText("evidence"), // JSON array of short quoted snippets evidence: pgText("evidence"), // JSON array of short quoted snippets
policy_version: pgText("policy_version"), // rules.ts policy version string policy_version: pgText("policy_version"), // rules.ts policy version string
username: pgText("username"), // denormalized from messages table for retention safety
}, },
(table) => ({ (table) => ({
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on( messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
@@ -208,6 +208,7 @@ export interface ModerationAction {
guild_id: string; guild_id: string;
action_type: ModerationActionType; action_type: ModerationActionType;
reason: string | null; reason: string | null;
username: string | null;
executed_by: string | null; executed_by: string | null;
status: "pending" | "executed" | "failed"; status: "pending" | "executed" | "failed";
error: string | null; error: string | null;