Refactor database handling to exclusively support PostgreSQL
- Removed SQLite support from the configuration and database initialization logic. - Updated database migration scripts to focus solely on PostgreSQL migrations. - Simplified logging messages to reflect PostgreSQL usage. - Adjusted database schema definitions to remove SQLite-specific types and structures. - Modified tests to ensure compatibility with PostgreSQL, including changes to table creation and data types. - Cleaned up unused imports and code related to SQLite.
This commit is contained in:
@@ -42,7 +42,7 @@ export async function initializeApp() {
|
||||
try {
|
||||
logger.info("Initializing database");
|
||||
await initializeDatabase();
|
||||
logger.info({ type: config.DATABASE_TYPE }, "Database initialized");
|
||||
logger.info("PostgreSQL database initialized");
|
||||
} catch (err) {
|
||||
logger.error({ error: err }, "Failed to initialize database");
|
||||
process.exit(1);
|
||||
|
||||
+6
-10
@@ -155,7 +155,6 @@ const configSchema = z
|
||||
.optional()
|
||||
.transform((v) => v === "true")
|
||||
.default(true),
|
||||
DATABASE_TYPE: z.enum(["sqlite", "postgres"]).default("sqlite"),
|
||||
DATABASE_URL: z.string().optional(),
|
||||
POSTGRES_HOST: z.string().default("localhost"),
|
||||
POSTGRES_PORT: z.coerce.number().int().positive().default(5432),
|
||||
@@ -179,15 +178,12 @@ const configSchema = z
|
||||
}
|
||||
|
||||
// Validate PostgreSQL configuration
|
||||
if (value.DATABASE_TYPE === "postgres") {
|
||||
if (!value.DATABASE_URL && !value.POSTGRES_HOST) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["DATABASE_URL"],
|
||||
message:
|
||||
"Either DATABASE_URL or POSTGRES_HOST must be provided when DATABASE_TYPE=postgres",
|
||||
});
|
||||
}
|
||||
if (!value.DATABASE_URL && !value.POSTGRES_HOST) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["DATABASE_URL"],
|
||||
message: "Either DATABASE_URL or POSTGRES_HOST must be provided",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+52
-92
@@ -1,5 +1,3 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import { config } from "../config.js";
|
||||
@@ -8,80 +6,54 @@ import * as schema from "./schema.js";
|
||||
|
||||
const logger = createChildLogger("drizzle");
|
||||
|
||||
let db:
|
||||
| ReturnType<typeof drizzlePostgres>
|
||||
| ReturnType<typeof drizzleSqlite>
|
||||
| null = null;
|
||||
let rawSqlite: ReturnType<typeof Database> | null = null;
|
||||
let db: ReturnType<typeof drizzlePostgres> | null = null;
|
||||
let rawPool: Pool | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the database connection based on DATABASE_TYPE config
|
||||
* Supports both PostgreSQL and SQLite
|
||||
* Initialize the PostgreSQL database connection.
|
||||
*/
|
||||
export async function initializeDatabase() {
|
||||
if (db !== null) {
|
||||
return db;
|
||||
}
|
||||
|
||||
// During tests prefer an isolated SQLite instance to avoid using shared
|
||||
// external Postgres instances which can lead to flaky test interference.
|
||||
const usePostgres =
|
||||
config.DATABASE_TYPE === "postgres" && process.env.NODE_ENV !== "test";
|
||||
let pool: Pool;
|
||||
|
||||
if (usePostgres) {
|
||||
let pool: Pool;
|
||||
|
||||
// Use DATABASE_URL if available, otherwise build from individual variables
|
||||
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 });
|
||||
// Provide a simple `run` helper for tests that expect it.
|
||||
try {
|
||||
(db as any).run = (sql: string) => pool.query(sql);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
logger.info("PostgreSQL database initialized");
|
||||
if (config.DATABASE_URL) {
|
||||
pool = new Pool({
|
||||
connectionString: config.DATABASE_URL,
|
||||
min: config.POSTGRES_POOL_MIN,
|
||||
max: config.POSTGRES_POOL_MAX,
|
||||
});
|
||||
} else {
|
||||
const sqlite = new Database(".muxer-queue.db");
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
|
||||
rawSqlite = sqlite;
|
||||
db = drizzleSqlite(sqlite, { schema });
|
||||
// Expose a convenience `run` method used by tests that expect a simple API.
|
||||
// `sqlite` is the underlying better-sqlite3 Database instance.
|
||||
try {
|
||||
(db as any).run = (sql: string) => sqlite.exec(sql);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
logger.info("SQLite database initialized");
|
||||
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
|
||||
* Get the initialized database instance.
|
||||
* Throws if database has not been initialized.
|
||||
*/
|
||||
export function getDatabase() {
|
||||
if (db === null) {
|
||||
@@ -97,51 +69,39 @@ function convertPlaceholdersForPostgres(sql: string) {
|
||||
return sql.replace(/\?/g, () => `$${++i}`);
|
||||
}
|
||||
|
||||
export async function executeAll(sql: string, params?: any[]) {
|
||||
if (rawPool) {
|
||||
const q = convertPlaceholdersForPostgres(sql);
|
||||
const res = await rawPool.query(q, params || []);
|
||||
return res.rows;
|
||||
export async function executeAll(sql: string, params?: unknown[]) {
|
||||
if (!rawPool) {
|
||||
throw new Error(
|
||||
"Database not initialized. Call initializeDatabase() first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (rawSqlite) {
|
||||
const stmt = rawSqlite.prepare(sql);
|
||||
return stmt.all(...(params || []));
|
||||
}
|
||||
|
||||
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?: any[]) {
|
||||
if (rawPool) {
|
||||
const q = convertPlaceholdersForPostgres(sql);
|
||||
const res = await rawPool.query(q, params || []);
|
||||
return res.rows[0] ?? null;
|
||||
export async function executeGet(sql: string, params?: unknown[]) {
|
||||
if (!rawPool) {
|
||||
throw new Error(
|
||||
"Database not initialized. Call initializeDatabase() first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (rawSqlite) {
|
||||
const stmt = rawSqlite.prepare(sql);
|
||||
return stmt.get(...(params || []));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
* For PostgreSQL, the pool will close on process exit
|
||||
* For SQLite, closes the database connection
|
||||
* Close the PostgreSQL connection pool.
|
||||
*/
|
||||
export async function closeDatabase() {
|
||||
if (db === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.DATABASE_TYPE === "postgres") {
|
||||
logger.info("PostgreSQL connection pool will close on process exit");
|
||||
} else {
|
||||
logger.info("SQLite database closed");
|
||||
if (rawPool !== null) {
|
||||
await rawPool.end();
|
||||
}
|
||||
|
||||
rawPool = null;
|
||||
db = null;
|
||||
logger.info("PostgreSQL database closed");
|
||||
}
|
||||
|
||||
+10
-31
@@ -1,45 +1,24 @@
|
||||
import "dotenv/config";
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate as migrateSqlite } from "drizzle-orm/better-sqlite3/migrator";
|
||||
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";
|
||||
|
||||
const logger = createChildLogger("migrate");
|
||||
|
||||
export function initializeMigrationSqliteDatabase(path = ".muxer-queue.db") {
|
||||
const sqlite = new Database(path);
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
return { sqlite, db: drizzleSqlite(sqlite) };
|
||||
}
|
||||
|
||||
export async function runMigrations(): Promise<void> {
|
||||
try {
|
||||
logger.info("Starting database migrations");
|
||||
logger.info("Starting PostgreSQL migrations");
|
||||
const db = (await initializeDatabase()) as Parameters<
|
||||
typeof migratePostgres
|
||||
>[0];
|
||||
|
||||
if (config.DATABASE_TYPE === "postgres") {
|
||||
logger.info("Running PostgreSQL migrations");
|
||||
const db = (await initializeDatabase()) as Parameters<
|
||||
typeof migratePostgres
|
||||
>[0];
|
||||
try {
|
||||
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
|
||||
} finally {
|
||||
await closeDatabase();
|
||||
}
|
||||
logger.info("PostgreSQL migrations completed successfully");
|
||||
} else {
|
||||
logger.info("Running SQLite migrations");
|
||||
const { sqlite, db } = initializeMigrationSqliteDatabase();
|
||||
try {
|
||||
migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" });
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
logger.info("SQLite migrations completed successfully");
|
||||
try {
|
||||
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
|
||||
} finally {
|
||||
await closeDatabase();
|
||||
}
|
||||
|
||||
logger.info("PostgreSQL migrations completed successfully");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ error: error instanceof Error ? error.message : String(error) },
|
||||
|
||||
+11
-382
@@ -8,14 +8,6 @@ import {
|
||||
pgTable,
|
||||
text as pgText,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import {
|
||||
index as sqliteIndex,
|
||||
integer as sqliteInteger,
|
||||
real as sqliteReal,
|
||||
sqliteTable,
|
||||
text as sqliteText,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { config } from "../config.js";
|
||||
|
||||
// PostgreSQL Schema
|
||||
// ==================
|
||||
@@ -244,227 +236,6 @@ export const pgVoiceRecordingsTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
// SQLite Schema
|
||||
// =============
|
||||
|
||||
/**
|
||||
* Muxer Jobs Table (SQLite)
|
||||
* Tracks audio post-processing jobs with status and retry logic
|
||||
*/
|
||||
export const sqliteMuxerJobsTable = sqliteTable(
|
||||
"muxer_jobs",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
data: sqliteText("data").notNull(),
|
||||
status: sqliteText("status", {
|
||||
enum: ["pending", "processing", "completed", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
attempts: sqliteInteger("attempts").notNull().default(0),
|
||||
maxAttempts: sqliteInteger("maxAttempts").notNull().default(3),
|
||||
createdAt: sqliteInteger("createdAt").notNull(),
|
||||
updatedAt: sqliteInteger("updatedAt").notNull(),
|
||||
error: sqliteText("error"),
|
||||
},
|
||||
(table) => ({
|
||||
statusIdx: sqliteIndex("idx_muxer_jobs_status").on(table.status),
|
||||
createdAtIdx: sqliteIndex("idx_muxer_jobs_createdAt").on(table.createdAt),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Messages Table (SQLite)
|
||||
* Stores text messages with AI moderation analysis
|
||||
*/
|
||||
export const sqliteMessagesTable = sqliteTable(
|
||||
"messages",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
guild_id: sqliteText("guild_id").notNull(),
|
||||
channel_id: sqliteText("channel_id").notNull(),
|
||||
thread_id: sqliteText("thread_id"),
|
||||
user_id: sqliteText("user_id").notNull(),
|
||||
username: sqliteText("username").notNull(),
|
||||
avatar_url: sqliteText("avatar_url"),
|
||||
content: sqliteText("content").notNull(),
|
||||
edited_content: sqliteText("edited_content"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
edited_at: sqliteInteger("edited_at"),
|
||||
deleted_at: sqliteInteger("deleted_at"),
|
||||
type: sqliteText("type", { enum: ["text", "edited", "deleted"] })
|
||||
.notNull()
|
||||
.default("text"),
|
||||
metadata: sqliteText("metadata"),
|
||||
ai_status: sqliteText("ai_status", {
|
||||
enum: ["pending", "clean", "warn", "flagged", "error"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
ai_moderation_flags: sqliteText("ai_moderation_flags"),
|
||||
ai_moderation_score: sqliteReal("ai_moderation_score"),
|
||||
ai_analysis: sqliteText("ai_analysis"),
|
||||
ai_categories: sqliteText("ai_categories"),
|
||||
ai_severity: sqliteText("ai_severity", {
|
||||
enum: ["none", "low", "medium", "high", "critical"],
|
||||
}),
|
||||
ai_confidence: sqliteReal("ai_confidence"),
|
||||
ai_recommended_action: sqliteText("ai_recommended_action", {
|
||||
enum: ["none", "monitor", "warn", "review", "delete", "escalate"],
|
||||
}),
|
||||
ai_analyzed_at: sqliteInteger("ai_analyzed_at"),
|
||||
ai_error: sqliteText("ai_error"),
|
||||
},
|
||||
(table) => ({
|
||||
channelIdx: sqliteIndex("idx_messages_channel").on(table.channel_id),
|
||||
userIdx: sqliteIndex("idx_messages_user").on(table.user_id),
|
||||
createdIdx: sqliteIndex("idx_messages_created").on(table.created_at),
|
||||
threadIdx: sqliteIndex("idx_messages_thread").on(table.thread_id),
|
||||
channelCreatedIdx: sqliteIndex("idx_messages_channel_created").on(
|
||||
table.channel_id,
|
||||
table.created_at,
|
||||
table.id,
|
||||
),
|
||||
threadCreatedIdx: sqliteIndex("idx_messages_thread_created").on(
|
||||
table.thread_id,
|
||||
table.created_at,
|
||||
table.id,
|
||||
),
|
||||
aiStatusCreatedIdx: sqliteIndex("idx_messages_ai_status_created").on(
|
||||
table.ai_status,
|
||||
table.created_at,
|
||||
table.id,
|
||||
),
|
||||
guildAiStatusCreatedIdx: sqliteIndex(
|
||||
"idx_messages_guild_ai_status_created",
|
||||
).on(table.guild_id, table.ai_status, table.created_at, table.id),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Attachments Table (SQLite)
|
||||
* Stores attachment metadata with upload status tracking
|
||||
*/
|
||||
export const sqliteAttachmentsTable = sqliteTable(
|
||||
"attachments",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
message_id: sqliteText("message_id").notNull(),
|
||||
guild_id: sqliteText("guild_id").notNull(),
|
||||
channel_id: sqliteText("channel_id").notNull(),
|
||||
thread_id: sqliteText("thread_id"),
|
||||
user_id: sqliteText("user_id").notNull(),
|
||||
filename: sqliteText("filename").notNull(),
|
||||
size: sqliteInteger("size").notNull(),
|
||||
type: sqliteText("type").notNull(),
|
||||
discord_url: sqliteText("discord_url").notNull(),
|
||||
uploaded_url: sqliteText("uploaded_url"),
|
||||
upload_status: sqliteText("upload_status", {
|
||||
enum: ["pending", "uploaded", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
upload_error: sqliteText("upload_error"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
uploaded_at: sqliteInteger("uploaded_at"),
|
||||
},
|
||||
(table) => ({
|
||||
channelIdx: sqliteIndex("idx_attachments_channel").on(table.channel_id),
|
||||
messageIdx: sqliteIndex("idx_attachments_message").on(table.message_id),
|
||||
statusIdx: sqliteIndex("idx_attachments_status").on(table.upload_status),
|
||||
channelCreatedIdx: sqliteIndex("idx_attachments_channel_created").on(
|
||||
table.channel_id,
|
||||
table.created_at,
|
||||
table.id,
|
||||
),
|
||||
threadCreatedIdx: sqliteIndex("idx_attachments_thread_created").on(
|
||||
table.thread_id,
|
||||
table.created_at,
|
||||
table.id,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* UI State Table (SQLite)
|
||||
* Stores persistent UI state (e.g., selected channel, filter preferences)
|
||||
*/
|
||||
export const sqliteUIStateTable = sqliteTable("ui_state", {
|
||||
key: sqliteText("key").primaryKey(),
|
||||
value: sqliteText("value").notNull(),
|
||||
updated_at: sqliteInteger("updated_at").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* AI Analysis Runs Table (SQLite)
|
||||
* Tracks AI analysis batch runs for conversation-level moderation
|
||||
*/
|
||||
export const sqliteAIAnalysisRunsTable = sqliteTable(
|
||||
"ai_analysis_runs",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
conversation_key: sqliteText("conversation_key").notNull(),
|
||||
target_message_ids: sqliteText("target_message_ids").notNull(), // JSON array
|
||||
model: sqliteText("model").notNull(),
|
||||
request_tokens_estimate: sqliteInteger("request_tokens_estimate"),
|
||||
response_raw: sqliteText("response_raw"),
|
||||
status: sqliteText("status", {
|
||||
enum: ["pending", "processing", "completed", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
error: sqliteText("error"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
completed_at: sqliteInteger("completed_at"),
|
||||
},
|
||||
(table) => ({
|
||||
conversationKeyIdx: sqliteIndex("idx_ai_analysis_runs_conversation_key").on(
|
||||
table.conversation_key,
|
||||
),
|
||||
statusIdx: sqliteIndex("idx_ai_analysis_runs_status").on(table.status),
|
||||
createdAtIdx: sqliteIndex("idx_ai_analysis_runs_created_at").on(
|
||||
table.created_at,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Voice Recordings Table (SQLite)
|
||||
* Stores voice recording segment metadata and upload status
|
||||
*/
|
||||
export const sqliteVoiceRecordingsTable = sqliteTable(
|
||||
"voice_recordings",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
user_id: sqliteText("user_id").notNull(),
|
||||
username: sqliteText("username").notNull(),
|
||||
avatar_url: sqliteText("avatar_url"),
|
||||
guild_id: sqliteText("guild_id"),
|
||||
channel_id: sqliteText("channel_id"),
|
||||
channel_name: sqliteText("channel_name"),
|
||||
filename: sqliteText("filename").notNull(),
|
||||
size_bytes: sqliteInteger("size_bytes").notNull(),
|
||||
download_url: sqliteText("download_url"),
|
||||
upload_status: sqliteText("upload_status", {
|
||||
enum: ["pending", "uploaded", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
upload_error: sqliteText("upload_error"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
uploaded_at: sqliteInteger("uploaded_at"),
|
||||
},
|
||||
(table) => ({
|
||||
userIdIdx: sqliteIndex("idx_voice_recordings_user_id").on(table.user_id),
|
||||
channelIdIdx: sqliteIndex("idx_voice_recordings_channel_id").on(
|
||||
table.channel_id,
|
||||
),
|
||||
createdIdx: sqliteIndex("idx_voice_recordings_created_at").on(
|
||||
table.created_at,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Message Reviews Table (PostgreSQL)
|
||||
* Tracks manual reviews of messages flagged by AI moderation
|
||||
@@ -502,43 +273,6 @@ export const pgMessageReviewsTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Message Reviews Table (SQLite)
|
||||
* Tracks manual reviews of messages flagged by AI moderation
|
||||
*/
|
||||
export const sqliteMessageReviewsTable = sqliteTable(
|
||||
"message_reviews",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
message_id: sqliteText("message_id").notNull(),
|
||||
guild_id: sqliteText("guild_id").notNull(),
|
||||
channel_id: sqliteText("channel_id").notNull(),
|
||||
reviewer_id: sqliteText("reviewer_id"),
|
||||
status: sqliteText("status", {
|
||||
enum: ["pending", "approved", "rejected", "escalated"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
notes: sqliteText("notes"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
reviewed_at: sqliteInteger("reviewed_at"),
|
||||
},
|
||||
(table) => ({
|
||||
messageIdIdx: sqliteIndex("idx_message_reviews_message_id").on(
|
||||
table.message_id,
|
||||
),
|
||||
statusIdx: sqliteIndex("idx_message_reviews_status").on(table.status),
|
||||
createdAtIdx: sqliteIndex("idx_message_reviews_created_at").on(
|
||||
table.created_at,
|
||||
),
|
||||
guildStatusIdx: sqliteIndex("idx_message_reviews_guild_status").on(
|
||||
table.guild_id,
|
||||
table.status,
|
||||
table.created_at,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Moderation Actions Table (PostgreSQL)
|
||||
* Tracks actions taken on messages (delete, mute, etc.)
|
||||
@@ -584,51 +318,6 @@ export const pgModerationActionsTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Moderation Actions Table (SQLite)
|
||||
* Tracks actions taken on messages (delete, mute, etc.)
|
||||
*/
|
||||
export const sqliteModerationActionsTable = sqliteTable(
|
||||
"moderation_actions",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
message_id: sqliteText("message_id"),
|
||||
user_id: sqliteText("user_id"),
|
||||
guild_id: sqliteText("guild_id").notNull(),
|
||||
action_type: sqliteText("action_type", {
|
||||
enum: [
|
||||
"delete_message",
|
||||
"mute_user",
|
||||
"warn_user",
|
||||
"kick_user",
|
||||
"ban_user",
|
||||
],
|
||||
}).notNull(),
|
||||
reason: sqliteText("reason"),
|
||||
executed_by: sqliteText("executed_by"),
|
||||
status: sqliteText("status", {
|
||||
enum: ["pending", "executed", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
error: sqliteText("error"),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
executed_at: sqliteInteger("executed_at"),
|
||||
},
|
||||
(table) => ({
|
||||
messageIdIdx: sqliteIndex("idx_moderation_actions_message_id").on(
|
||||
table.message_id,
|
||||
),
|
||||
userIdIdx: sqliteIndex("idx_moderation_actions_user_id").on(table.user_id),
|
||||
statusIdx: sqliteIndex("idx_moderation_actions_status").on(table.status),
|
||||
guildStatusIdx: sqliteIndex("idx_moderation_actions_guild_status").on(
|
||||
table.guild_id,
|
||||
table.status,
|
||||
table.created_at,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Retention Policies Table (PostgreSQL)
|
||||
* Defines data retention rules per guild/channel
|
||||
@@ -652,78 +341,18 @@ export const pgRetentionPoliciesTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Retention Policies Table (SQLite)
|
||||
* Defines data retention rules per guild/channel
|
||||
*/
|
||||
export const sqliteRetentionPoliciesTable = sqliteTable(
|
||||
"retention_policies",
|
||||
{
|
||||
id: sqliteText("id").primaryKey(),
|
||||
guild_id: sqliteText("guild_id").notNull(),
|
||||
channel_id: sqliteText("channel_id"),
|
||||
retention_days: sqliteInteger("retention_days").notNull().default(90),
|
||||
apply_to_media: sqliteInteger("apply_to_media", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
apply_to_voice: sqliteInteger("apply_to_voice", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
enabled: sqliteInteger("enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
created_at: sqliteInteger("created_at").notNull(),
|
||||
updated_at: sqliteInteger("updated_at").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
guildIdIdx: sqliteIndex("idx_retention_policies_guild_id").on(
|
||||
table.guild_id,
|
||||
),
|
||||
enabledIdx: sqliteIndex("idx_retention_policies_enabled").on(table.enabled),
|
||||
}),
|
||||
);
|
||||
// Runtime table exports
|
||||
// =====================
|
||||
|
||||
// Runtime table selection based on config
|
||||
// ========================================
|
||||
|
||||
export const muxerJobsTable =
|
||||
config.DATABASE_TYPE === "postgres" ? pgMuxerJobsTable : sqliteMuxerJobsTable;
|
||||
|
||||
export const messagesTable =
|
||||
config.DATABASE_TYPE === "postgres" ? pgMessagesTable : sqliteMessagesTable;
|
||||
|
||||
export const attachmentsTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgAttachmentsTable
|
||||
: sqliteAttachmentsTable;
|
||||
|
||||
export const uiStateTable =
|
||||
config.DATABASE_TYPE === "postgres" ? pgUIStateTable : sqliteUIStateTable;
|
||||
|
||||
export const aiAnalysisRunsTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgAIAnalysisRunsTable
|
||||
: sqliteAIAnalysisRunsTable;
|
||||
|
||||
export const voiceRecordingsTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgVoiceRecordingsTable
|
||||
: sqliteVoiceRecordingsTable;
|
||||
|
||||
export const messageReviewsTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgMessageReviewsTable
|
||||
: sqliteMessageReviewsTable;
|
||||
|
||||
export const moderationActionsTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgModerationActionsTable
|
||||
: sqliteModerationActionsTable;
|
||||
|
||||
export const retentionPoliciesTable =
|
||||
config.DATABASE_TYPE === "postgres"
|
||||
? pgRetentionPoliciesTable
|
||||
: sqliteRetentionPoliciesTable;
|
||||
export const muxerJobsTable = pgMuxerJobsTable;
|
||||
export const messagesTable = pgMessagesTable;
|
||||
export const attachmentsTable = pgAttachmentsTable;
|
||||
export const uiStateTable = pgUIStateTable;
|
||||
export const aiAnalysisRunsTable = pgAIAnalysisRunsTable;
|
||||
export const voiceRecordingsTable = pgVoiceRecordingsTable;
|
||||
export const messageReviewsTable = pgMessageReviewsTable;
|
||||
export const moderationActionsTable = pgModerationActionsTable;
|
||||
export const retentionPoliciesTable = pgRetentionPoliciesTable;
|
||||
|
||||
// Export table types for use in queries
|
||||
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { config } from "../config.js";
|
||||
import { executeAll, executeGet } from "../database/drizzle.js";
|
||||
import { createChildLogger } from "../logger.js";
|
||||
import type { MessageRecord } from "./types.js";
|
||||
@@ -107,11 +106,7 @@ export async function getHourlyStats(input: {
|
||||
|
||||
try {
|
||||
const since = Date.now() - hours * 3600_000;
|
||||
const isPg = config.DATABASE_TYPE === "postgres";
|
||||
|
||||
const hourExpr = isPg
|
||||
? `to_char(to_timestamp((created_at / 3600000) * 3600), 'YYYY-MM-DD HH24:MI:SS') as hour`
|
||||
: `datetime((created_at / 3600000) * 3600, 'unixepoch') as hour`;
|
||||
const hourExpr = `to_char(to_timestamp((created_at / 3600000) * 3600), 'YYYY-MM-DD HH24:MI:SS') as hour`;
|
||||
|
||||
const rows = await executeAll(
|
||||
`
|
||||
@@ -531,11 +526,7 @@ export async function getModerationStats(input: {
|
||||
|
||||
try {
|
||||
const since = Date.now() - hours * 3600_000;
|
||||
const isPg = config.DATABASE_TYPE === "postgres";
|
||||
|
||||
const avgScoreExpr = isPg
|
||||
? `round(avg(ai_moderation_score)::numeric, 2)`
|
||||
: `round(avg(ai_moderation_score), 2)`;
|
||||
const avgScoreExpr = `round(avg(ai_moderation_score)::numeric, 2)`;
|
||||
|
||||
const row = await executeGet(
|
||||
`
|
||||
@@ -738,11 +729,7 @@ export async function getDailyTrend(input: {
|
||||
|
||||
try {
|
||||
const since = Date.now() - hours * 3600_000;
|
||||
const isPg = config.DATABASE_TYPE === "postgres";
|
||||
|
||||
const dateExpr = isPg
|
||||
? `to_char(date_trunc('day', to_timestamp(created_at / 1000)), 'YYYY-MM-DD') as date`
|
||||
: `date(created_at / 1000, 'unixepoch') as date`;
|
||||
const dateExpr = `to_char(date_trunc('day', to_timestamp(created_at / 1000)), 'YYYY-MM-DD') as date`;
|
||||
|
||||
const rows = await executeAll(
|
||||
`
|
||||
@@ -832,15 +819,8 @@ export async function getActivityHeatmap(input: {
|
||||
|
||||
try {
|
||||
const since = Date.now() - hours * 3600_000;
|
||||
const isPg = config.DATABASE_TYPE === "postgres";
|
||||
|
||||
// SQLite: cast to int for modulo; Postgres: use extract()
|
||||
const dayExpr = isPg
|
||||
? `(extract(isodow from to_timestamp(created_at / 1000)) % 7)::int as day_of_week`
|
||||
: `(cast((created_at / 86400000) as integer) % 7) as day_of_week`;
|
||||
const hourExpr = isPg
|
||||
? `extract(hour from to_timestamp(created_at / 1000))::int as hour`
|
||||
: `(cast((created_at / 3600000) as integer) % 24) as hour`;
|
||||
const dayExpr = `(extract(isodow from to_timestamp(created_at / 1000)) % 7)::int as day_of_week`;
|
||||
const hourExpr = `extract(hour from to_timestamp(created_at / 1000))::int as hour`;
|
||||
|
||||
const rows = await executeAll(
|
||||
`
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export function getDatabase() {
|
||||
return undefined as unknown as never;
|
||||
}
|
||||
|
||||
// ── Persistent KV store (replaces SQLite uiState table) ────────────────────
|
||||
// ── Persistent KV store ────────────────────────────────────────────────────
|
||||
|
||||
const KV_PREFIX = "kv:";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user