refactor: split monolith into 3 microservices (frontend, backend, discord-gateway)

- Extract services into services/{frontend,backend,discord-gateway}
- Create packages/shared/ for shared logger, errors, utils, types
- Setup Modular MVC pattern in backend (controller→service→repository)
- Setup event-driven architecture in discord-gateway with Redis pub/sub
- Move Docker files to infra/docker/ with per-service Dockerfiles
- Update docker-compose.yml to use Traefik-only routing (no port exposes)
- Update GitHub Actions deploy workflow for multi-service matrix build
- Fix all import paths and resolve type errors across all services
- All 3 services pass tsc --noEmit clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-01 21:44:29 +07:00
co-authored by Claude Opus 4.8
parent bda8304bb9
commit c48a0c5e3b
193 changed files with 16879 additions and 1158 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@bete/shared",
"version": "1.0.0",
"description": "Shared utilities, types, and errors for Bete microservices",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./types": "./dist/types/index.js",
"./errors": "./dist/errors/index.js",
"./logger": "./dist/logger/index.js",
"./utils": "./dist/utils/index.js"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "^9.0.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^25.9.0",
"typescript": "^5.9.3"
}
}
+86
View File
@@ -0,0 +1,86 @@
// Custom error classes for all services
export class AppError extends Error {
constructor(
public code: string,
public statusCode: number,
message: string,
public details?: Record<string, unknown>,
) {
super(message);
this.name = "AppError";
}
}
export class ValidationError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super("VALIDATION_ERROR", 400, message, details);
this.name = "ValidationError";
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id?: string) {
super("NOT_FOUND", 404, `${resource} not found${id ? `: ${id}` : ""}`);
this.name = "NotFoundError";
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Unauthorized") {
super("UNAUTHORIZED", 401, message);
this.name = "UnauthorizedError";
}
}
export class ForbiddenError extends AppError {
constructor(message = "Forbidden") {
super("FORBIDDEN", 403, message);
this.name = "ForbiddenError";
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super("CONFLICT", 409, message);
this.name = "ConflictError";
}
}
export class InternalServerError extends AppError {
constructor(
message = "Internal server error",
details?: Record<string, unknown>,
) {
super("INTERNAL_SERVER_ERROR", 500, message, details);
this.name = "InternalServerError";
}
}
export class DatabaseError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super("DATABASE_ERROR", 500, message, details);
this.name = "DatabaseError";
}
}
export class ConfigError extends AppError {
constructor(message: string) {
super("CONFIG_ERROR", 500, message);
this.name = "ConfigError";
}
}
export class DiscordError extends AppError {
constructor(message: string, details?: Record<string, unknown>) {
super("DISCORD_ERROR", 500, message, details);
this.name = "DiscordError";
}
}
export class TimeoutError extends AppError {
constructor(operation: string) {
super("TIMEOUT", 504, `${operation} timed out`);
this.name = "TimeoutError";
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./errors/index.js";
export * from "./logger/index.js";
export * from "./types/index.js";
export * from "./utils/index.js";
+25
View File
@@ -0,0 +1,25 @@
import pino from "pino";
export type Logger = ReturnType<typeof createLogger>;
export function createLogger(context: string) {
return pino({
name: context,
level: process.env.LOG_LEVEL || "info",
transport:
process.env.NODE_ENV === "development"
? {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard",
ignore: "pid,hostname",
},
}
: undefined,
} as pino.LoggerOptions);
}
export function createChildLogger(context: string) {
return createLogger(context);
}
+70
View File
@@ -0,0 +1,70 @@
// Shared types for all services
export interface AppConfig {
NODE_ENV: "development" | "production" | "test";
LOG_LEVEL: string;
VERBOSE: boolean;
}
export interface DatabaseConfig {
DATABASE_URL: string;
AUTO_MIGRATE_ON_STARTUP: boolean;
}
export interface DiscordConfig {
DISCORD_TOKEN: string;
MONITOR_GUILD_ID: string;
}
export interface AIConfig {
AI_LLM_API_KEY: string;
}
export interface RedisConfig {
REDIS_URL: string;
}
export interface WebServerConfig {
WEBSERVER_PORT: number;
ADMIN_PASSWORD: string;
}
export interface MessageRecord {
id: string;
guildId: string;
channelId: string;
userId: string;
username: string;
content: string;
createdAt: Date;
editedAt?: Date;
deletedAt?: Date;
}
export interface AttachmentRecord {
id: string;
messageId: string;
filename: string;
size: number;
mimeType: string;
discordUrl: string;
uploadedUrl?: string;
uploadStatus: "pending" | "uploaded" | "failed";
createdAt: Date;
}
export interface VoiceSegment {
userId: string;
sessionStart: number;
segmentIndex: number;
duration: number;
filePath: string;
createdAt: Date;
}
export interface AnalyticsData {
totalMessages: number;
totalAttachments: number;
totalVoiceSegments: number;
activeUsers: number;
lastUpdated: Date;
}
+61
View File
@@ -0,0 +1,61 @@
// Utility functions shared across services
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}
export function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
export function sanitizeString(str: string): string {
return str.replace(/[<>]/g, "").trim().substring(0);
}
export interface PaginationParams {
page: number;
limit: number;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
pages: number;
}
export function calculatePagination(
total: number,
page: number,
limit: number,
): PaginatedResponse<never> {
return {
data: [],
total,
page,
limit,
pages: Math.ceil(total / limit),
};
}
export function getOffset(page: number, limit: number): number {
return (page - 1) * limit;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "bundler"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}