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
@@ -0,0 +1,129 @@
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
import type { PoolClient } from "pg";
import { Pool } from "pg";
import { config } from "../../shared/config/config.js";
import { createChildLogger } from "../../shared/logger/logger.js";
import * as schema from "./schema.js";
const logger = createChildLogger("drizzle");
let db: ReturnType<typeof drizzlePostgres> | null = null;
let rawPool: Pool | null = null;
/**
* Initialize the PostgreSQL database connection.
*/
export async function initializeDatabase() {
if (db !== null) {
return db;
}
let pool: Pool;
if (config.DATABASE_URL) {
pool = new Pool({
connectionString: config.DATABASE_URL,
min: config.POSTGRES_POOL_MIN,
max: config.POSTGRES_POOL_MAX,
});
} else {
pool = new Pool({
host: config.POSTGRES_HOST,
port: config.POSTGRES_PORT,
user: config.POSTGRES_USER,
password: config.POSTGRES_PASSWORD,
database: config.POSTGRES_DB,
min: config.POSTGRES_POOL_MIN,
max: config.POSTGRES_POOL_MAX,
});
}
rawPool = pool;
db = drizzlePostgres(pool, { schema });
try {
(db as { run?: (sql: string) => Promise<unknown> }).run = (sql: string) =>
pool.query(sql);
} catch {
// ignore
}
logger.info("PostgreSQL database initialized");
return db;
}
/**
* Get the initialized database instance.
* Throws if database has not been initialized.
*/
export function getDatabase() {
if (db === null) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
return db;
}
function convertPlaceholdersForPostgres(sql: string) {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}
export async function executeAll(sql: string, params?: unknown[]) {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const query = convertPlaceholdersForPostgres(sql);
const result = await rawPool.query(query, params || []);
return result.rows;
}
export async function executeGet(sql: string, params?: unknown[]) {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const query = convertPlaceholdersForPostgres(sql);
const result = await rawPool.query(query, params || []);
return result.rows[0] ?? null;
}
/**
* Run a function with a dedicated PostgreSQL client from the shared pool.
* Use this for session-scoped operations such as advisory locks.
*/
export async function withDatabaseClient<T>(
callback: (client: PoolClient) => Promise<T>,
): Promise<T> {
if (!rawPool) {
throw new Error(
"Database not initialized. Call initializeDatabase() first.",
);
}
const client = await rawPool.connect();
try {
return await callback(client);
} finally {
client.release();
}
}
/**
* Close the PostgreSQL connection pool.
*/
export async function closeDatabase() {
if (rawPool !== null) {
await rawPool.end();
}
rawPool = null;
db = null;
logger.info("PostgreSQL database closed");
}