refactor: atomic, DRY, and logging improvements across codebase

- Split llmModerationClient.ts (2170 lines) into 5 focused sub-modules
- Split aiAnalyzer.ts (1282 lines) into 4 modular pipelines
- Split messages.db.ts (826 lines) into 5 domain-specific modules
- Moved shared schema to @bete/shared, eliminated backend duplication
- Added createChildLogger to all voice-recording and AI moderation modules
- Extracted tryCommandThenFallback, normalizeMediaState, DEFAULT_VOICE_STATUS
- Created shared pagination.ts utility, eliminated 5+ cursor-pagination duplications
- Created shared messageMapper.ts for row mapping
- Standardized backend error handling with asyncHandler
- Added frontend createLogger utility and useAsyncAction hook
- Added structured logging to frontend hooks, socket, and API client

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-09 19:46:08 +07:00
co-authored by Claude Opus 4.8
parent b68789fffc
commit 07032ab521
61 changed files with 3808 additions and 3043 deletions
@@ -1,14 +1,9 @@
import type { PageResult } from "@bete/shared";
import { pgAttachmentsTable, pgMessagesTable } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import { and, desc, eq, inArray, lt, ne, type SQL } from "drizzle-orm";
import {
bigint as pgBigint,
integer as pgInteger,
real as pgReal,
pgTable,
text as pgText,
} from "drizzle-orm/pg-core";
import { getDatabase } from "../../shared/database/index.js";
import { mapMessageRow } from "../../shared/utils/messageMapper.js";
import type {
MessageCreate,
MessageQuery,
@@ -17,71 +12,6 @@ import type {
const logger = createChildLogger("messages.repository");
/**
* Local table definitions mirroring services/discord-gateway/src/shared/database/schema.ts.
* These are query-building references only — schema source of truth remains in discord-gateway.
*/
const messages = pgTable("messages", {
id: pgText("id").primaryKey(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id").notNull(),
thread_id: pgText("thread_id"),
user_id: pgText("user_id").notNull(),
username: pgText("username").notNull(),
avatar_url: pgText("avatar_url"),
content: pgText("content").notNull(),
edited_content: pgText("edited_content"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
edited_at: pgBigint("edited_at", { mode: "number" }),
deleted_at: pgBigint("deleted_at", { mode: "number" }),
type: pgText("type", {
enum: ["text", "edited", "deleted"],
})
.notNull()
.default("text"),
metadata: pgText("metadata"),
ai_status: pgText("ai_status", {
enum: ["pending", "processing", "clean", "warn", "flagged", "error"],
})
.notNull()
.default("pending"),
ai_moderation_flags: pgText("ai_moderation_flags"),
ai_moderation_score: pgReal("ai_moderation_score"),
ai_analysis: pgText("ai_analysis"),
ai_categories: pgText("ai_categories"),
ai_severity: pgText("ai_severity", {
enum: ["none", "low", "medium", "high", "critical"],
}),
ai_confidence: pgReal("ai_confidence"),
ai_recommended_action: pgText("ai_recommended_action", {
enum: ["none", "monitor", "warn", "review", "delete", "escalate"],
}),
ai_analyzed_at: pgBigint("ai_analyzed_at", { mode: "number" }),
ai_error: pgText("ai_error"),
});
const attachments = pgTable("attachments", {
id: pgText("id").primaryKey(),
message_id: pgText("message_id").notNull(),
guild_id: pgText("guild_id").notNull(),
channel_id: pgText("channel_id").notNull(),
thread_id: pgText("thread_id"),
user_id: pgText("user_id").notNull(),
filename: pgText("filename").notNull(),
size: pgInteger("size").notNull(),
type: pgText("type").notNull(),
discord_url: pgText("discord_url").notNull(),
uploaded_url: pgText("uploaded_url"),
upload_status: pgText("upload_status", {
enum: ["pending", "uploaded", "failed"],
})
.notNull()
.default("pending"),
upload_error: pgText("upload_error"),
created_at: pgBigint("created_at", { mode: "number" }).notNull(),
uploaded_at: pgBigint("uploaded_at", { mode: "number" }),
});
export interface AttachmentResult {
id: string;
message_id: string;
@@ -100,35 +30,6 @@ export interface AttachmentResult {
uploaded_at: number | null;
}
function mapMessageRow(row: Record<string, unknown>) {
return {
id: String(row.id ?? ""),
guild_id: String(row.guild_id ?? ""),
channel_id: String(row.channel_id ?? ""),
thread_id: (row.thread_id as string | null) ?? null,
user_id: String(row.user_id ?? ""),
username: String(row.username ?? ""),
avatar_url: (row.avatar_url as string | null) ?? null,
content: String(row.content ?? ""),
edited_content: (row.edited_content as string | null) ?? null,
created_at: Number(row.created_at ?? 0),
edited_at: (row.edited_at as number | null) ?? null,
deleted_at: (row.deleted_at as number | null) ?? null,
type: String(row.type ?? "text"),
metadata: (row.metadata as string | null) ?? null,
ai_status: (row.ai_status as string | null) ?? null,
ai_moderation_flags: (row.ai_moderation_flags as string | null) ?? null,
ai_moderation_score: (row.ai_moderation_score as number | null) ?? null,
ai_analysis: (row.ai_analysis as string | null) ?? null,
ai_categories: (row.ai_categories as string | null) ?? null,
ai_severity: (row.ai_severity as string | null) ?? null,
ai_confidence: (row.ai_confidence as number | null) ?? null,
ai_recommended_action: (row.ai_recommended_action as string | null) ?? null,
ai_analyzed_at: (row.ai_analyzed_at as number | null) ?? null,
ai_error: (row.ai_error as string | null) ?? null,
};
}
export class MessagesRepository {
async findMany(
query: MessageQuery,
@@ -138,27 +39,27 @@ export class MessagesRepository {
const conditions: SQL[] = [];
if (query.guildId) {
conditions.push(eq(messages.guild_id, query.guildId));
conditions.push(eq(pgMessagesTable.guild_id, query.guildId));
}
if (query.channelId) {
conditions.push(eq(messages.channel_id, query.channelId));
conditions.push(eq(pgMessagesTable.channel_id, query.channelId));
}
if (query.userId) {
conditions.push(eq(messages.user_id, query.userId));
conditions.push(eq(pgMessagesTable.user_id, query.userId));
}
if (query.status) {
conditions.push(eq(messages.ai_status, query.status));
conditions.push(eq(pgMessagesTable.ai_status, query.status));
}
if (query.cursor) {
conditions.push(lt(messages.created_at, Number(query.cursor)));
conditions.push(lt(pgMessagesTable.created_at, Number(query.cursor)));
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const rows = await db
.select()
.from(messages)
.from(pgMessagesTable)
.where(where)
.orderBy(desc(messages.created_at))
.orderBy(desc(pgMessagesTable.created_at))
.limit(limit + 1);
const data = rows
@@ -175,8 +76,8 @@ export class MessagesRepository {
const db = getDatabase();
const [row] = await db
.select()
.from(messages)
.where(eq(messages.id, id))
.from(pgMessagesTable)
.where(eq(pgMessagesTable.id, id))
.limit(1);
if (!row) return null;
@@ -189,17 +90,17 @@ export class MessagesRepository {
): Promise<PageResult<ReturnType<typeof mapMessageRow>>> {
const db = getDatabase();
const limit = query.limit ?? 50;
const conditions: SQL[] = [eq(messages.channel_id, channelId)];
const conditions: SQL[] = [eq(pgMessagesTable.channel_id, channelId)];
if (query.cursor) {
conditions.push(lt(messages.created_at, Number(query.cursor)));
conditions.push(lt(pgMessagesTable.created_at, Number(query.cursor)));
}
const rows = await db
.select()
.from(messages)
.from(pgMessagesTable)
.where(and(...conditions))
.orderBy(desc(messages.created_at))
.orderBy(desc(pgMessagesTable.created_at))
.limit(limit + 1);
const data = rows
@@ -216,7 +117,7 @@ export class MessagesRepository {
const id = crypto.randomUUID();
const [row] = await db
.insert(messages)
.insert(pgMessagesTable)
.values({
id,
guild_id: data.guildId,
@@ -242,7 +143,7 @@ export class MessagesRepository {
async update(id: string, data: MessageUpdate) {
const db = getDatabase();
const setData: Partial<typeof messages.$inferInsert> = {};
const setData: Partial<typeof pgMessagesTable.$inferInsert> = {};
if (data.editedContent !== undefined) {
setData.edited_content = data.editedContent;
@@ -266,9 +167,9 @@ export class MessagesRepository {
if (Object.keys(setData).length === 0) return this.findById(id);
const [row] = await db
.update(messages)
.update(pgMessagesTable)
.set(setData)
.where(eq(messages.id, id))
.where(eq(pgMessagesTable.id, id))
.returning();
if (!row) return null;
@@ -288,20 +189,20 @@ export class MessagesRepository {
messageIds?: string[];
}): Promise<number> {
const db = getDatabase();
const conditions: SQL[] = [eq(messages.ai_status, "error")];
const conditions: SQL[] = [eq(pgMessagesTable.ai_status, "error")];
if (opts.messageIds && opts.messageIds.length > 0) {
conditions.push(inArray(messages.id, opts.messageIds));
conditions.push(inArray(pgMessagesTable.id, opts.messageIds));
}
if (opts.guildId) {
conditions.push(eq(messages.guild_id, opts.guildId));
conditions.push(eq(pgMessagesTable.guild_id, opts.guildId));
}
if (opts.channelId) {
conditions.push(eq(messages.channel_id, opts.channelId));
conditions.push(eq(pgMessagesTable.channel_id, opts.channelId));
}
const result = await db
.update(messages)
.update(pgMessagesTable)
.set({ ai_status: "pending" })
.where(and(...conditions));
@@ -317,9 +218,14 @@ export class MessagesRepository {
async markForReanalysis(id: string): Promise<void> {
const db = getDatabase();
await db
.update(messages)
.update(pgMessagesTable)
.set({ ai_status: "pending" })
.where(and(eq(messages.id, id), ne(messages.ai_status, "pending")));
.where(
and(
eq(pgMessagesTable.id, id),
ne(pgMessagesTable.ai_status, "pending"),
),
);
}
/**
@@ -332,32 +238,32 @@ export class MessagesRepository {
): Promise<Record<string, unknown>[]> {
const db = getDatabase();
const conditions: SQL[] = [
inArray(messages.ai_status, ["warn", "flagged"]),
inArray(pgMessagesTable.ai_status, ["warn", "flagged"]),
];
if (channelId) {
conditions.push(eq(messages.channel_id, channelId));
conditions.push(eq(pgMessagesTable.channel_id, channelId));
}
const rows = await db
.select({
id: messages.id,
guild_id: messages.guild_id,
channel_id: messages.channel_id,
user_id: messages.user_id,
username: messages.username,
avatar_url: messages.avatar_url,
content: messages.content,
type: messages.type,
created_at: messages.created_at,
ai_status: messages.ai_status,
ai_severity: messages.ai_severity,
ai_confidence: messages.ai_confidence,
ai_analysis: messages.ai_analysis,
id: pgMessagesTable.id,
guild_id: pgMessagesTable.guild_id,
channel_id: pgMessagesTable.channel_id,
user_id: pgMessagesTable.user_id,
username: pgMessagesTable.username,
avatar_url: pgMessagesTable.avatar_url,
content: pgMessagesTable.content,
type: pgMessagesTable.type,
created_at: pgMessagesTable.created_at,
ai_status: pgMessagesTable.ai_status,
ai_severity: pgMessagesTable.ai_severity,
ai_confidence: pgMessagesTable.ai_confidence,
ai_analysis: pgMessagesTable.ai_analysis,
})
.from(messages)
.from(pgMessagesTable)
.where(and(...conditions))
.orderBy(desc(messages.created_at))
.orderBy(desc(pgMessagesTable.created_at))
.limit(limit);
return rows as unknown as Record<string, unknown>[];
@@ -365,7 +271,9 @@ export class MessagesRepository {
async delete(id: string): Promise<boolean> {
const db = getDatabase();
const result = await db.delete(messages).where(eq(messages.id, id));
const result = await db
.delete(pgMessagesTable)
.where(eq(pgMessagesTable.id, id));
return (result.rowCount ?? 0) > 0;
}
@@ -376,17 +284,17 @@ export class MessagesRepository {
): Promise<PageResult<AttachmentResult>> {
const db = getDatabase();
const limit = query.limit ?? 50;
const conditions: SQL[] = [eq(attachments.channel_id, channelId)];
const conditions: SQL[] = [eq(pgAttachmentsTable.channel_id, channelId)];
if (query.cursor) {
conditions.push(lt(attachments.created_at, Number(query.cursor)));
conditions.push(lt(pgAttachmentsTable.created_at, Number(query.cursor)));
}
const rows = await db
.select()
.from(attachments)
.from(pgAttachmentsTable)
.where(and(...conditions))
.orderBy(desc(attachments.created_at))
.orderBy(desc(pgAttachmentsTable.created_at))
.limit(limit + 1);
const data = rows.map((r) => ({