feat(database): add automatic migration on startup and enhance text analysis cache source

This commit is contained in:
MythEclipse
2026-05-31 23:01:38 +07:00
parent 96cd0cfc62
commit 7607282db2
13 changed files with 178 additions and 14 deletions
+8
View File
@@ -1,6 +1,7 @@
import { Client } from "discord.js-selfbot-v13";
import { config } from "../config.js";
import { closeDatabase, initializeDatabase } from "../database/drizzle.js";
import { runMigrations } from "../database/migrate.js";
import { createDiscordClientOptions } from "../discordClientOptions.js";
import { createChildLogger } from "../logger.js";
import { startPendingAIAnalysisWorker } from "../moderation/aiAnalyzer.js";
@@ -40,6 +41,13 @@ export async function initializeApp() {
});
try {
if (config.AUTO_MIGRATE_ON_STARTUP) {
logger.info(
"AUTO_MIGRATE_ON_STARTUP enabled; running database migrations",
);
await runMigrations();
}
logger.info("Initializing database");
await initializeDatabase();
logger.info("PostgreSQL database initialized");
+5
View File
@@ -167,6 +167,11 @@ const configSchema = z
.optional()
.transform((v) => v === "true")
.default(true),
AUTO_MIGRATE_ON_STARTUP: z
.string()
.optional()
.transform((v) => v === "true")
.default(true),
DATABASE_URL: z.string().optional(),
POSTGRES_HOST: z.string().default("localhost"),
POSTGRES_PORT: z.coerce.number().int().positive().default(5432),
+22
View File
@@ -1,5 +1,6 @@
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import type { PoolClient } from "pg";
import { config } from "../config.js";
import { createChildLogger } from "../logger.js";
import * as schema from "./schema.js";
@@ -93,6 +94,27 @@ export async function executeGet(sql: string, params?: unknown[]) {
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.
*/
+30 -5
View File
@@ -1,19 +1,44 @@
import "dotenv/config";
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
import { config } from "../config.js";
import { createChildLogger } from "../logger.js";
import { closeDatabase, initializeDatabase } from "./drizzle.js";
import * as schema from "./schema.js";
import {
closeDatabase,
initializeDatabase,
withDatabaseClient,
} from "./drizzle.js";
const logger = createChildLogger("migrate");
const MIGRATION_LOCK_KEY_1 = 2026;
const MIGRATION_LOCK_KEY_2 = 531;
export async function runMigrations(): Promise<void> {
try {
logger.info("Starting PostgreSQL migrations");
const db = (await initializeDatabase()) as Parameters<
typeof migratePostgres
>[0];
await initializeDatabase();
try {
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
await withDatabaseClient(async (client) => {
const db = drizzlePostgres(client, { schema });
await client.query("SELECT pg_advisory_lock($1, $2)", [
MIGRATION_LOCK_KEY_1,
MIGRATION_LOCK_KEY_2,
]);
try {
await migratePostgres(db, {
migrationsFolder: "./drizzle/migrations",
});
} finally {
await client.query("SELECT pg_advisory_unlock($1, $2)", [
MIGRATION_LOCK_KEY_1,
MIGRATION_LOCK_KEY_2,
]);
}
});
} finally {
await closeDatabase();
}
+2 -2
View File
@@ -369,9 +369,9 @@ export const pgTextAnalysisCacheTable = pgTable(
text: pgText("text").primaryKey(),
/** JSON array of moderation flags detected for this text (e.g. ["vulgar_language","harassment"]). */
flags: pgText("flags").notNull().default("[]"),
/** Which source produced this result: "local" | "nvidia" | "primary_ai" | "groq". */
/** Which source produced this result: "local" | "nvidia" | "primary_ai" | "groq" | "vision_llm". */
source: pgText("source", {
enum: ["local", "nvidia", "primary_ai", "groq"],
enum: ["local", "nvidia", "primary_ai", "groq", "vision_llm"],
})
.notNull()
.default("local"),
+2 -2
View File
@@ -7,7 +7,7 @@ const logger = createChildLogger("text-cache-store");
export interface TextCacheEntry {
text: string;
flags: string[];
source: "local" | "nvidia" | "primary_ai" | "groq";
source: "local" | "nvidia" | "primary_ai" | "groq" | "vision_llm";
analyzed_at: number;
expires_at: number;
hit_count: number;
@@ -53,7 +53,7 @@ export async function getCachedText(
export async function upsertCachedText(
text: string,
flags: string[],
source: "local" | "nvidia" | "primary_ai" | "groq",
source: "local" | "nvidia" | "primary_ai" | "groq" | "vision_llm",
expiresAt: number,
): Promise<void> {
const now = Date.now();