291 lines
9.5 KiB
TypeScript
291 lines
9.5 KiB
TypeScript
|
|
import {
|
||
|
|
pgTable,
|
||
|
|
text as pgText,
|
||
|
|
integer as pgInteger,
|
||
|
|
bigint as pgBigint,
|
||
|
|
real as pgReal,
|
||
|
|
index as pgIndex,
|
||
|
|
foreignKey as pgForeignKey,
|
||
|
|
} from "drizzle-orm/pg-core";
|
||
|
|
import {
|
||
|
|
sqliteTable,
|
||
|
|
text as sqliteText,
|
||
|
|
integer as sqliteInteger,
|
||
|
|
real as sqliteReal,
|
||
|
|
index as sqliteIndex,
|
||
|
|
} from "drizzle-orm/sqlite-core";
|
||
|
|
import { config } from "../config";
|
||
|
|
|
||
|
|
// PostgreSQL Schema
|
||
|
|
// ==================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Muxer Jobs Table (PostgreSQL)
|
||
|
|
* Tracks audio post-processing jobs with status and retry logic
|
||
|
|
*/
|
||
|
|
export const pgMuxerJobsTable = pgTable(
|
||
|
|
"muxer_jobs",
|
||
|
|
{
|
||
|
|
id: pgText("id").primaryKey(),
|
||
|
|
data: pgText("data").notNull(),
|
||
|
|
status: pgText("status", {
|
||
|
|
enum: ["pending", "processing", "completed", "failed"],
|
||
|
|
})
|
||
|
|
.notNull()
|
||
|
|
.default("pending"),
|
||
|
|
attempts: pgInteger("attempts").notNull().default(0),
|
||
|
|
maxAttempts: pgInteger("maxAttempts").notNull().default(3),
|
||
|
|
createdAt: pgBigint("createdAt", { mode: "number" }).notNull(),
|
||
|
|
updatedAt: pgBigint("updatedAt", { mode: "number" }).notNull(),
|
||
|
|
error: pgText("error"),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
statusIdx: pgIndex("idx_muxer_jobs_status").on(table.status),
|
||
|
|
createdAtIdx: pgIndex("idx_muxer_jobs_createdAt").on(table.createdAt),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Messages Table (PostgreSQL)
|
||
|
|
* Stores text messages with AI moderation analysis
|
||
|
|
*/
|
||
|
|
export const pgMessagesTable = 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", "clean", "warn", "flagged", "error"],
|
||
|
|
})
|
||
|
|
.notNull()
|
||
|
|
.default("pending"),
|
||
|
|
ai_moderation_flags: pgText("ai_moderation_flags"),
|
||
|
|
ai_moderation_score: pgReal("ai_moderation_score"),
|
||
|
|
ai_moderation_raw: pgText("ai_moderation_raw"),
|
||
|
|
ai_analysis: pgText("ai_analysis"),
|
||
|
|
ai_analyzed_at: pgBigint("ai_analyzed_at", { mode: "number" }),
|
||
|
|
ai_error: pgText("ai_error"),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
channelIdx: pgIndex("idx_messages_channel").on(table.channel_id),
|
||
|
|
userIdx: pgIndex("idx_messages_user").on(table.user_id),
|
||
|
|
createdIdx: pgIndex("idx_messages_created").on(table.created_at),
|
||
|
|
threadIdx: pgIndex("idx_messages_thread").on(table.thread_id),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Attachments Table (PostgreSQL)
|
||
|
|
* Stores attachment metadata with upload status tracking
|
||
|
|
*/
|
||
|
|
export const pgAttachmentsTable = 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" }),
|
||
|
|
},
|
||
|
|
(table) => ({
|
||
|
|
channelIdx: pgIndex("idx_attachments_channel").on(table.channel_id),
|
||
|
|
messageIdx: pgIndex("idx_attachments_message").on(table.message_id),
|
||
|
|
statusIdx: pgIndex("idx_attachments_status").on(table.upload_status),
|
||
|
|
messageFk: pgForeignKey({
|
||
|
|
columns: [table.message_id],
|
||
|
|
foreignColumns: [pgMessagesTable.id],
|
||
|
|
name: "fk_attachments_message_id",
|
||
|
|
}).onDelete("cascade"),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* UI State Table (PostgreSQL)
|
||
|
|
* Stores persistent UI state (e.g., selected channel, filter preferences)
|
||
|
|
*/
|
||
|
|
export const pgUIStateTable = pgTable("ui_state", {
|
||
|
|
key: pgText("key").primaryKey(),
|
||
|
|
value: pgText("value").notNull(),
|
||
|
|
updated_at: pgBigint("updated_at", { mode: "number" }).notNull(),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 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_moderation_raw: sqliteText("ai_moderation_raw"),
|
||
|
|
ai_analysis: sqliteText("ai_analysis"),
|
||
|
|
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),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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(),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 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 table types for use in queries
|
||
|
|
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
||
|
|
export type MuxerJobInsert = typeof muxerJobsTable.$inferInsert;
|
||
|
|
|
||
|
|
export type Message = typeof messagesTable.$inferSelect;
|
||
|
|
export type MessageInsert = typeof messagesTable.$inferInsert;
|
||
|
|
|
||
|
|
export type Attachment = typeof attachmentsTable.$inferSelect;
|
||
|
|
export type AttachmentInsert = typeof attachmentsTable.$inferInsert;
|
||
|
|
|
||
|
|
export type UIState = typeof uiStateTable.$inferSelect;
|
||
|
|
export type UIStateInsert = typeof uiStateTable.$inferInsert;
|