初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
import {drizzle} from 'drizzle-orm/postgres-js';
import {and, asc, count, desc, eq, exists, gt, gte, lt, lte, or, sql} from 'drizzle-orm';
import postgres from 'postgres';
import * as schema from './schema.ts';
import {config} from 'dotenv';
import path from 'path';
import {fileURLToPath} from 'url';
// 加载环境变量(优先使用工作目录的 .env,确保构建后运行时能正确加载)
config({ path: path.resolve(process.cwd(), '.env') });
// 检查环境变量
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable is not set');
}
// 创建PostgreSQL连接
const connectionString = process.env.DATABASE_URL;
// 检测数据库类型
const isNeonDatabase = connectionString.includes('neon.tech') || connectionString.includes('neon.database.com');
// 根据数据库类型选择配置
const getDatabaseConfig = () => {
if (isNeonDatabase) {
// Neon Database Serverless 优化配置
return {
max: 1, // Serverless 环境下每个实例保持最小连接数,利用 Neon 自身的连接池
idle_timeout: 0, // 立即释放空闲连接,适应 Serverless 的快速冻结特性
connect_timeout: 10, // Neon 连接速度快,减少超时时间
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: 'require', // Neon 默认需要 SSL
prepare: false, // 禁用预处理语句以提高兼容性
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
} else {
// 标准 PostgreSQL 数据库配置
return {
max: process.env.NODE_ENV === 'production' ? 10 : 5, // 普通PostgreSQL可以支持更多连接
idle_timeout: 20, // 增加空闲超时时间
connect_timeout: 30, // 增加连接超时时间以适应网络延迟
max_lifetime: 3600, // 连接最大生命周期(1小时)
ssl: connectionString.includes('sslmode=require') || connectionString.includes('ssl=true') ? 'require' : false,
prepare: false, // 禁用预处理语句以提高兼容性
transform: {
undefined: null, // 将undefined转换为null
},
connection: {
application_name: 'voicehub-app'
},
onnotice: process.env.NODE_ENV === 'development' ? console.log : undefined,
debug: process.env.NODE_ENV === 'development' && process.env.DEBUG_SQL === 'true'
};
}
};
const client = postgres(connectionString, getDatabaseConfig());
// 创建Drizzle数据库实例
export const db = drizzle(client, { schema });
// 导出连接客户端(用于手动查询或关闭连接)
export { client };
// 导出schema以便在其他地方使用
export * from './schema.ts';
// 导出drizzle-orm函数
export {eq, and, gt, gte, lt, lte, count, exists, desc, asc, or, sql};
// 数据库连接测试函数
export async function testConnection() {
try {
await client`SELECT 1`;
console.log('✅ Database connection successful');
return true;
} catch (error) {
console.error('❌ Database connection failed:', error);
return false;
}
}
// 获取数据库连接状态
export function getConnectionStatus() {
return {
isConnected: !client.ended,
maxConnections: client.options.max,
idleTimeout: client.options.idle_timeout,
connectTimeout: client.options.connect_timeout
};
}
// 连接管理 - 根据数据库类型自适应
let idleTimer: NodeJS.Timeout | null = null;
// Neon 数据库使用更短的空闲时间以支持自动启停,普通 PostgreSQL 使用更长的空闲时间
const IDLE_TIMEOUT = isNeonDatabase ? 5 * 60 * 1000 : 10 * 60 * 1000; // Neon: 5分钟,PostgreSQL: 10分钟
// 重置空闲计时器
function resetIdleTimer() {
if (idleTimer) {
clearTimeout(idleTimer);
}
// 只在生产环境启用自动断开
if (process.env.NODE_ENV === 'production') {
idleTimer = setTimeout(async () => {
try {
if (!client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 Auto-closing idle ${dbType} database connections${isNeonDatabase ? ' for Serverless optimization' : ''}`);
await client.end({ timeout: isNeonDatabase ? 5 : 10 });
}
} catch (error) {
console.error('❌ Error during auto-close:', error);
}
}, IDLE_TIMEOUT);
}
}
// 包装数据库操作以支持自动启停
export function withAutoReconnect<T extends any[], R>(
operation: (...args: T) => Promise<R>
) {
return async (...args: T): Promise<R> => {
resetIdleTimer();
try {
return await operation(...args);
} catch (error: any) {
// 如果连接已关闭,记录信息
if (error?.code === 'CONNECTION_ENDED' || client.ended) {
const dbType = isNeonDatabase ? 'Neon' : 'PostgreSQL';
console.log(`🔄 ${dbType} database connection ended${isNeonDatabase ? ', Neon will auto-reconnect on next query' : ', will reconnect on next query'}`);
}
throw error;
}
};
}
// 优雅关闭数据库连接
export async function closeConnection() {
try {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
if (!client.ended) {
await client.end({ timeout: 10 });
console.log('✅ Database connection closed gracefully');
}
} catch (error) {
console.error('❌ Error closing database connection:', error);
}
}
// 设置优雅关闭处理
if (typeof process !== 'undefined') {
const gracefulShutdown = async () => {
console.log('🔄 Shutting down database connections...');
await closeConnection();
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('beforeExit', gracefulShutdown);
}
@@ -0,0 +1,132 @@
DO $$ BEGIN
CREATE TYPE "public"."BlacklistType" AS ENUM('SONG', 'KEYWORD');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;--> statement-breakpoint
CREATE TABLE "NotificationSettings" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"userId" integer NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"songRequestEnabled" boolean DEFAULT true NOT NULL,
"songVotedEnabled" boolean DEFAULT true NOT NULL,
"songPlayedEnabled" boolean DEFAULT true NOT NULL,
"refreshInterval" integer DEFAULT 60 NOT NULL,
"songVotedThreshold" integer DEFAULT 1 NOT NULL,
CONSTRAINT "NotificationSettings_userId_unique" UNIQUE("userId")
);
--> statement-breakpoint
CREATE TABLE "Notification" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"type" text NOT NULL,
"message" text NOT NULL,
"read" boolean DEFAULT false NOT NULL,
"userId" integer NOT NULL,
"songId" integer
);
--> statement-breakpoint
CREATE TABLE "PlayTime" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"name" text NOT NULL,
"startTime" text,
"endTime" text,
"enabled" boolean DEFAULT true NOT NULL,
"description" text
);
--> statement-breakpoint
CREATE TABLE "Schedule" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"songId" integer NOT NULL,
"playDate" timestamp NOT NULL,
"played" boolean DEFAULT false NOT NULL,
"sequence" integer DEFAULT 1 NOT NULL,
"playTimeId" integer
);
--> statement-breakpoint
CREATE TABLE "Semester" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"name" text NOT NULL,
"isActive" boolean DEFAULT false NOT NULL,
CONSTRAINT "Semester_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "SongBlacklist" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"type" "BlacklistType" NOT NULL,
"value" text NOT NULL,
"reason" text,
"isActive" boolean DEFAULT true NOT NULL,
"createdBy" integer
);
--> statement-breakpoint
CREATE TABLE "Song" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"title" text NOT NULL,
"artist" text NOT NULL,
"requesterId" integer NOT NULL,
"played" boolean DEFAULT false NOT NULL,
"playedAt" timestamp,
"semester" text,
"preferredPlayTimeId" integer,
"cover" text,
"musicPlatform" text,
"musicId" text
);
--> statement-breakpoint
CREATE TABLE "SystemSettings" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"enablePlayTimeSelection" boolean DEFAULT false NOT NULL,
"siteTitle" text,
"siteLogoUrl" text,
"schoolLogoHomeUrl" text,
"schoolLogoPrintUrl" text,
"siteDescription" text,
"submissionGuidelines" text,
"icpNumber" text,
"enableSubmissionLimit" boolean DEFAULT false NOT NULL,
"dailySubmissionLimit" integer,
"weeklySubmissionLimit" integer,
"showBlacklistKeywords" boolean DEFAULT false NOT NULL,
"hideStudentInfo" boolean DEFAULT true NOT NULL
);
--> statement-breakpoint
CREATE TABLE "User" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"username" text NOT NULL,
"name" text,
"grade" text,
"class" text,
"role" text DEFAULT 'USER' NOT NULL,
"password" text NOT NULL,
"lastLogin" timestamp,
"lastLoginIp" text,
"passwordChangedAt" timestamp,
"forcePasswordChange" boolean DEFAULT true NOT NULL,
"meowNickname" text,
"meowBoundAt" timestamp,
CONSTRAINT "User_username_unique" UNIQUE("username")
);
--> statement-breakpoint
CREATE TABLE "Vote" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"songId" integer NOT NULL,
"userId" integer NOT NULL
);
@@ -0,0 +1,14 @@
-- 为排期表添加草稿支持
-- 添加草稿状态标识字段
ALTER TABLE "Schedule" ADD COLUMN "isDraft" boolean DEFAULT false NOT NULL;
-- 添加发布时间字段(发布后记录发布时间)
ALTER TABLE "Schedule" ADD COLUMN "publishedAt" timestamp;
-- 为现有排期设置为已发布状态(因为现有的都应该是已发布的)
UPDATE "Schedule" SET "isDraft" = false, "publishedAt" = "updatedAt" WHERE "isDraft" IS NULL;
-- 创建索引以优化查询性能
CREATE INDEX "idx_schedule_is_draft" ON "Schedule" ("isDraft");
CREATE INDEX "idx_schedule_published_at" ON "Schedule" ("publishedAt");
CREATE INDEX "idx_schedule_draft_date" ON "Schedule" ("isDraft", "playDate");
@@ -0,0 +1,33 @@
-- 创建用户状态枚举类型
CREATE TYPE "public"."user_status" AS ENUM('active', 'withdrawn');--> statement-breakpoint
-- 添加用户状态相关字段到用户表
ALTER TABLE "User" ADD COLUMN "status" "user_status" DEFAULT 'active' NOT NULL;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "statusChangedAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "statusChangedBy" integer;--> statement-breakpoint
-- 创建用户状态变更日志表
CREATE TABLE IF NOT EXISTS "user_status_logs" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"old_status" "user_status",
"new_status" "user_status" NOT NULL,
"reason" text,
"operator_id" integer,
"created_at" timestamp DEFAULT now() NOT NULL
);--> statement-breakpoint
-- 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS "idx_user_status" ON "User" USING btree ("status");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_user_status_logs_user_id" ON "user_status_logs" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_user_status_logs_created_at" ON "user_status_logs" USING btree ("created_at" DESC);--> statement-breakpoint
-- 添加外键约束
ALTER TABLE "User" ADD CONSTRAINT "User_statusChangedBy_User_id_fk" FOREIGN KEY ("statusChangedBy") REFERENCES "User"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_status_logs" ADD CONSTRAINT "user_status_logs_user_id_User_id_fk" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_status_logs" ADD CONSTRAINT "user_status_logs_operator_id_User_id_fk" FOREIGN KEY ("operator_id") REFERENCES "User"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
-- 为现有用户初始化状态变更日志
INSERT INTO "user_status_logs" ("user_id", "old_status", "new_status", "reason", "created_at")
SELECT "id", NULL, 'active', '系统迁移初始化', "createdAt"
FROM "User";
@@ -0,0 +1 @@
ALTER TABLE "Semester" DROP CONSTRAINT "Semester_name_unique";
@@ -0,0 +1,56 @@
-- 创建API Keys表
CREATE TABLE "api_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"name" varchar(255) NOT NULL,
"description" text,
"key_hash" varchar(255) NOT NULL,
"key_prefix" varchar(10) NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_used_at" timestamp with time zone,
"created_by_user_id" integer NOT NULL,
"usage_count" integer DEFAULT 0 NOT NULL,
CONSTRAINT "api_keys_key_hash_unique" UNIQUE("key_hash")
);--> statement-breakpoint
-- 创建API Key权限表
CREATE TABLE "api_key_permissions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"api_key_id" uuid NOT NULL,
"permission" varchar(100) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "api_key_permissions_api_key_id_permission_unique" UNIQUE("api_key_id", "permission")
);--> statement-breakpoint
-- 创建API访问日志表
CREATE TABLE "api_logs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"api_key_id" uuid,
"endpoint" varchar(500) NOT NULL,
"method" varchar(10) NOT NULL,
"ip_address" inet NOT NULL,
"user_agent" text,
"status_code" integer NOT NULL,
"response_time_ms" integer NOT NULL,
"request_body" text,
"response_body" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"error_message" text
);--> statement-breakpoint
-- 创建外键约束
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_created_by_user_id_User_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "api_key_permissions" ADD CONSTRAINT "api_key_permissions_api_key_id_api_keys_id_fk" FOREIGN KEY ("api_key_id") REFERENCES "api_keys"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "api_logs" ADD CONSTRAINT "api_logs_api_key_id_api_keys_id_fk" FOREIGN KEY ("api_key_id") REFERENCES "api_keys"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
-- 创建索引
CREATE INDEX "idx_api_keys_key_hash" ON "api_keys"("key_hash");--> statement-breakpoint
CREATE INDEX "idx_api_keys_created_by" ON "api_keys"("created_by_user_id");--> statement-breakpoint
CREATE INDEX "idx_api_keys_active" ON "api_keys"("is_active") WHERE "is_active" = true;--> statement-breakpoint
CREATE INDEX "idx_api_key_permissions_api_key_id" ON "api_key_permissions"("api_key_id");--> statement-breakpoint
CREATE INDEX "idx_api_logs_api_key_id" ON "api_logs"("api_key_id");--> statement-breakpoint
CREATE INDEX "idx_api_logs_created_at" ON "api_logs"("created_at" DESC);--> statement-breakpoint
CREATE INDEX "idx_api_logs_status_code" ON "api_logs"("status_code");--> statement-breakpoint
CREATE INDEX "idx_api_logs_endpoint" ON "api_logs"("endpoint");
@@ -0,0 +1,2 @@
ALTER TABLE "NotificationSettings" DROP CONSTRAINT "NotificationSettings_userId_unique";--> statement-breakpoint
ALTER TABLE "User" DROP CONSTRAINT "User_username_unique";
@@ -0,0 +1,20 @@
-- 添加用户邮箱字段
ALTER TABLE "User" ADD COLUMN "email" text;
ALTER TABLE "User" ADD COLUMN "emailVerified" boolean DEFAULT false NOT NULL;
ALTER TABLE "User" ADD COLUMN "emailVerifiedAt" timestamp;
-- 添加系统设置SMTP配置字段
ALTER TABLE "SystemSettings" ADD COLUMN "smtpEnabled" boolean DEFAULT false NOT NULL;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpHost" text;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpPort" integer DEFAULT 587;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpSecure" boolean DEFAULT false NOT NULL;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpUsername" text;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpPassword" text;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpFromEmail" text;
ALTER TABLE "SystemSettings" ADD COLUMN "smtpFromName" text;
-- 添加通知设置邮件通知字段
ALTER TABLE "NotificationSettings" ADD COLUMN "emailSongRequestEnabled" boolean DEFAULT true NOT NULL;
ALTER TABLE "NotificationSettings" ADD COLUMN "emailSongVotedEnabled" boolean DEFAULT true NOT NULL;
ALTER TABLE "NotificationSettings" ADD COLUMN "emailSongPlayedEnabled" boolean DEFAULT true NOT NULL;
ALTER TABLE "NotificationSettings" ADD COLUMN "emailSystemNoticeEnabled" boolean DEFAULT true NOT NULL;
@@ -0,0 +1,10 @@
ALTER TABLE "SystemSettings" ADD COLUMN "smtpEnabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpHost" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpPort" integer DEFAULT 587;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpSecure" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpUsername" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpPassword" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpFromEmail" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "smtpFromName" text DEFAULT '校园广播站';--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "email" text;--> statement-breakpoint
ALTER TABLE "User" ADD COLUMN "emailVerified" boolean DEFAULT false;
@@ -0,0 +1,10 @@
CREATE TABLE "EmailTemplate" (
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"key" varchar(100) NOT NULL,
"name" varchar(200) NOT NULL,
"subject" varchar(300) NOT NULL,
"html" text NOT NULL,
"updatedByUserId" integer
);
@@ -0,0 +1,2 @@
-- 添加歌曲播放地址字段
ALTER TABLE "Song" ADD COLUMN "playUrl" text;
@@ -0,0 +1,3 @@
ALTER TABLE "Schedule" ADD COLUMN "isDraft" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "Schedule" ADD COLUMN "publishedAt" timestamp;--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "playUrl" text;
@@ -0,0 +1,42 @@
CREATE TYPE "public"."collaborator_status" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED');--> statement-breakpoint
CREATE TABLE "collaboration_logs"
(
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"collaborator_id" uuid NOT NULL,
"action" varchar(50) NOT NULL,
"operator_id" integer NOT NULL,
"ip_address" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "RequestTime"
(
"id" serial PRIMARY KEY NOT NULL,
"createdAt" timestamp(6) DEFAULT now() NOT NULL,
"updatedAt" timestamp(6) DEFAULT now() NOT NULL,
"name" text NOT NULL,
"startTime" timestamp NOT NULL,
"endTime" timestamp NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"description" text,
"expected" bigint DEFAULT 0 NOT NULL,
"accepted" bigint DEFAULT 0 NOT NULL,
"past" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "song_collaborators"
(
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"song_id" integer NOT NULL,
"user_id" integer NOT NULL,
"status" "collaborator_status" DEFAULT 'PENDING' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "Song"
ADD COLUMN "hitRequestId" integer;--> statement-breakpoint
ALTER TABLE "SystemSettings"
ADD COLUMN "enableRequestTimeLimitation" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "SystemSettings"
ADD COLUMN "forceBlockAllRequests" boolean DEFAULT false NOT NULL;
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS "song_replay_requests" (
"id" serial PRIMARY KEY NOT NULL,
"song_id" integer NOT NULL,
"user_id" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "song_replay_requests_song_id_user_id_unique" UNIQUE("song_id","user_id")
);
ALTER TABLE "SystemSettings" ADD COLUMN "enableReplayRequests" boolean DEFAULT false NOT NULL;
@@ -0,0 +1,2 @@
CREATE TYPE "public"."replay_request_status" AS ENUM('PENDING', 'FULFILLED', 'REJECTED');--> statement-breakpoint
ALTER TABLE "song_replay_requests" ADD COLUMN "status" "replay_request_status" DEFAULT 'PENDING' NOT NULL;
@@ -0,0 +1,11 @@
CREATE TABLE "UserIdentity" (
"id" serial PRIMARY KEY NOT NULL,
"userId" integer NOT NULL,
"provider" text NOT NULL,
"providerUserId" text NOT NULL,
"providerUsername" text,
"createdAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "UserIdentity_provider_providerUserId_unique" UNIQUE("provider","providerUserId")
);
--> statement-breakpoint
ALTER TABLE "UserIdentity" ADD CONSTRAINT "UserIdentity_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1 @@
ALTER TABLE "SystemSettings" ADD COLUMN "monthlySubmissionLimit" integer;
@@ -0,0 +1,2 @@
ALTER TABLE "SystemSettings" ADD COLUMN "gonganNumber" text;--> statement-breakpoint
ALTER TABLE "SystemSettings" ADD COLUMN "gonganLink" text;
@@ -0,0 +1 @@
ALTER TABLE "SystemSettings" DROP COLUMN "gonganLink";
@@ -0,0 +1 @@
ALTER TABLE "SystemSettings" ADD COLUMN "enableCollaborativeSubmission" boolean DEFAULT true NOT NULL;
@@ -0,0 +1,3 @@
ALTER TABLE "Song" ADD COLUMN "submissionNote" text;
ALTER TABLE "Song" ADD COLUMN "submissionNotePublic" boolean DEFAULT false NOT NULL;
ALTER TABLE "SystemSettings" ADD COLUMN "enableSubmissionRemarks" boolean DEFAULT false NOT NULL;
@@ -0,0 +1 @@
ALTER TYPE "public"."user_status" ADD VALUE 'graduate';
@@ -0,0 +1,4 @@
ALTER TABLE "Song" ALTER COLUMN "title" SET DATA TYPE varchar(100);--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "kg_hash" varchar(64);--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "singer" varchar(100);--> statement-breakpoint
ALTER TABLE "Song" ADD COLUMN "cover_url" varchar(255);
@@ -0,0 +1 @@
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "siteVersion" text;
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS "ChangelogEntry" (
"id" serial PRIMARY KEY,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"version" text NOT NULL,
"title" text NOT NULL,
"content" text NOT NULL,
"published" boolean DEFAULT false NOT NULL,
"publishedAt" timestamp,
"createdBy" integer
);
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS "SongComment" (
"id" serial PRIMARY KEY,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"songId" integer NOT NULL,
"userId" integer NOT NULL,
"content" text NOT NULL
);
CREATE INDEX IF NOT EXISTS "SongComment_songId_idx" ON "SongComment" ("songId");
@@ -0,0 +1,791 @@
{
"id": "7431b15e-7067-4715-87c6-cc4c83712e0b",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.NotificationSettings": {
"name": "NotificationSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songRequestEnabled": {
"name": "songRequestEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songVotedEnabled": {
"name": "songVotedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songPlayedEnabled": {
"name": "songPlayedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"refreshInterval": {
"name": "refreshInterval",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 60
},
"songVotedThreshold": {
"name": "songVotedThreshold",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"NotificationSettings_userId_unique": {
"name": "NotificationSettings_userId_unique",
"nullsNotDistinct": false,
"columns": [
"userId"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Notification": {
"name": "Notification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"read": {
"name": "read",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PlayTime": {
"name": "PlayTime",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"startTime": {
"name": "startTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endTime": {
"name": "endTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Schedule": {
"name": "Schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"playDate": {
"name": "playDate",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"playTimeId": {
"name": "playTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Semester": {
"name": "Semester",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Semester_name_unique": {
"name": "Semester_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SongBlacklist": {
"name": "SongBlacklist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "BlacklistType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdBy": {
"name": "createdBy",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Song": {
"name": "Song",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"artist": {
"name": "artist",
"type": "text",
"primaryKey": false,
"notNull": true
},
"requesterId": {
"name": "requesterId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"playedAt": {
"name": "playedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"semester": {
"name": "semester",
"type": "text",
"primaryKey": false,
"notNull": false
},
"preferredPlayTimeId": {
"name": "preferredPlayTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cover": {
"name": "cover",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicPlatform": {
"name": "musicPlatform",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicId": {
"name": "musicId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SystemSettings": {
"name": "SystemSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"enablePlayTimeSelection": {
"name": "enablePlayTimeSelection",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"siteTitle": {
"name": "siteTitle",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteLogoUrl": {
"name": "siteLogoUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoHomeUrl": {
"name": "schoolLogoHomeUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoPrintUrl": {
"name": "schoolLogoPrintUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteDescription": {
"name": "siteDescription",
"type": "text",
"primaryKey": false,
"notNull": false
},
"submissionGuidelines": {
"name": "submissionGuidelines",
"type": "text",
"primaryKey": false,
"notNull": false
},
"icpNumber": {
"name": "icpNumber",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enableSubmissionLimit": {
"name": "enableSubmissionLimit",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"dailySubmissionLimit": {
"name": "dailySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"weeklySubmissionLimit": {
"name": "weeklySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"showBlacklistKeywords": {
"name": "showBlacklistKeywords",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"hideStudentInfo": {
"name": "hideStudentInfo",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"grade": {
"name": "grade",
"type": "text",
"primaryKey": false,
"notNull": false
},
"class": {
"name": "class",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'USER'"
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true
},
"lastLogin": {
"name": "lastLogin",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"lastLoginIp": {
"name": "lastLoginIp",
"type": "text",
"primaryKey": false,
"notNull": false
},
"passwordChangedAt": {
"name": "passwordChangedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"forcePasswordChange": {
"name": "forcePasswordChange",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"meowNickname": {
"name": "meowNickname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"meowBoundAt": {
"name": "meowBoundAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_username_unique": {
"name": "User_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.BlacklistType": {
"name": "BlacklistType",
"schema": "public",
"values": [
"SONG",
"KEYWORD"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -0,0 +1,783 @@
{
"id": "5412e543-4a9c-416f-87b1-b8a405a19172",
"prevId": "7431b15e-7067-4715-87c6-cc4c83712e0b",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.NotificationSettings": {
"name": "NotificationSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songRequestEnabled": {
"name": "songRequestEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songVotedEnabled": {
"name": "songVotedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songPlayedEnabled": {
"name": "songPlayedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"refreshInterval": {
"name": "refreshInterval",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 60
},
"songVotedThreshold": {
"name": "songVotedThreshold",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"NotificationSettings_userId_unique": {
"name": "NotificationSettings_userId_unique",
"nullsNotDistinct": false,
"columns": [
"userId"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Notification": {
"name": "Notification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"read": {
"name": "read",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PlayTime": {
"name": "PlayTime",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"startTime": {
"name": "startTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endTime": {
"name": "endTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Schedule": {
"name": "Schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"playDate": {
"name": "playDate",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"playTimeId": {
"name": "playTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Semester": {
"name": "Semester",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SongBlacklist": {
"name": "SongBlacklist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "BlacklistType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdBy": {
"name": "createdBy",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Song": {
"name": "Song",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"artist": {
"name": "artist",
"type": "text",
"primaryKey": false,
"notNull": true
},
"requesterId": {
"name": "requesterId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"playedAt": {
"name": "playedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"semester": {
"name": "semester",
"type": "text",
"primaryKey": false,
"notNull": false
},
"preferredPlayTimeId": {
"name": "preferredPlayTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cover": {
"name": "cover",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicPlatform": {
"name": "musicPlatform",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicId": {
"name": "musicId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SystemSettings": {
"name": "SystemSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"enablePlayTimeSelection": {
"name": "enablePlayTimeSelection",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"siteTitle": {
"name": "siteTitle",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteLogoUrl": {
"name": "siteLogoUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoHomeUrl": {
"name": "schoolLogoHomeUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoPrintUrl": {
"name": "schoolLogoPrintUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteDescription": {
"name": "siteDescription",
"type": "text",
"primaryKey": false,
"notNull": false
},
"submissionGuidelines": {
"name": "submissionGuidelines",
"type": "text",
"primaryKey": false,
"notNull": false
},
"icpNumber": {
"name": "icpNumber",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enableSubmissionLimit": {
"name": "enableSubmissionLimit",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"dailySubmissionLimit": {
"name": "dailySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"weeklySubmissionLimit": {
"name": "weeklySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"showBlacklistKeywords": {
"name": "showBlacklistKeywords",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"hideStudentInfo": {
"name": "hideStudentInfo",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"grade": {
"name": "grade",
"type": "text",
"primaryKey": false,
"notNull": false
},
"class": {
"name": "class",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'USER'"
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true
},
"lastLogin": {
"name": "lastLogin",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"lastLoginIp": {
"name": "lastLoginIp",
"type": "text",
"primaryKey": false,
"notNull": false
},
"passwordChangedAt": {
"name": "passwordChangedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"forcePasswordChange": {
"name": "forcePasswordChange",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"meowNickname": {
"name": "meowNickname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"meowBoundAt": {
"name": "meowBoundAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_username_unique": {
"name": "User_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.BlacklistType": {
"name": "BlacklistType",
"schema": "public",
"values": [
"SONG",
"KEYWORD"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -0,0 +1,767 @@
{
"id": "b7406ed0-51d0-4217-92f0-0131b12db197",
"prevId": "5412e543-4a9c-416f-87b1-b8a405a19172",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.NotificationSettings": {
"name": "NotificationSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songRequestEnabled": {
"name": "songRequestEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songVotedEnabled": {
"name": "songVotedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"songPlayedEnabled": {
"name": "songPlayedEnabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"refreshInterval": {
"name": "refreshInterval",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 60
},
"songVotedThreshold": {
"name": "songVotedThreshold",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Notification": {
"name": "Notification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"read": {
"name": "read",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.PlayTime": {
"name": "PlayTime",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"startTime": {
"name": "startTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endTime": {
"name": "endTime",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Schedule": {
"name": "Schedule",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"playDate": {
"name": "playDate",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"playTimeId": {
"name": "playTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Semester": {
"name": "Semester",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SongBlacklist": {
"name": "SongBlacklist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"type": {
"name": "type",
"type": "BlacklistType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"reason": {
"name": "reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"createdBy": {
"name": "createdBy",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Song": {
"name": "Song",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"artist": {
"name": "artist",
"type": "text",
"primaryKey": false,
"notNull": true
},
"requesterId": {
"name": "requesterId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"played": {
"name": "played",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"playedAt": {
"name": "playedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"semester": {
"name": "semester",
"type": "text",
"primaryKey": false,
"notNull": false
},
"preferredPlayTimeId": {
"name": "preferredPlayTimeId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cover": {
"name": "cover",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicPlatform": {
"name": "musicPlatform",
"type": "text",
"primaryKey": false,
"notNull": false
},
"musicId": {
"name": "musicId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.SystemSettings": {
"name": "SystemSettings",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"enablePlayTimeSelection": {
"name": "enablePlayTimeSelection",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"siteTitle": {
"name": "siteTitle",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteLogoUrl": {
"name": "siteLogoUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoHomeUrl": {
"name": "schoolLogoHomeUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"schoolLogoPrintUrl": {
"name": "schoolLogoPrintUrl",
"type": "text",
"primaryKey": false,
"notNull": false
},
"siteDescription": {
"name": "siteDescription",
"type": "text",
"primaryKey": false,
"notNull": false
},
"submissionGuidelines": {
"name": "submissionGuidelines",
"type": "text",
"primaryKey": false,
"notNull": false
},
"icpNumber": {
"name": "icpNumber",
"type": "text",
"primaryKey": false,
"notNull": false
},
"enableSubmissionLimit": {
"name": "enableSubmissionLimit",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"dailySubmissionLimit": {
"name": "dailySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"weeklySubmissionLimit": {
"name": "weeklySubmissionLimit",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"showBlacklistKeywords": {
"name": "showBlacklistKeywords",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"hideStudentInfo": {
"name": "hideStudentInfo",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"grade": {
"name": "grade",
"type": "text",
"primaryKey": false,
"notNull": false
},
"class": {
"name": "class",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'USER'"
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true
},
"lastLogin": {
"name": "lastLogin",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"lastLoginIp": {
"name": "lastLoginIp",
"type": "text",
"primaryKey": false,
"notNull": false
},
"passwordChangedAt": {
"name": "passwordChangedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"forcePasswordChange": {
"name": "forcePasswordChange",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"meowNickname": {
"name": "meowNickname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"meowBoundAt": {
"name": "meowBoundAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"songId": {
"name": "songId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.BlacklistType": {
"name": "BlacklistType",
"schema": "public",
"values": [
"SONG",
"KEYWORD"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1756291495622,
"tag": "0000_friendly_garia",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1756299330141,
"tag": "20250827125530_little_james_howlett",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1756368610365,
"tag": "20250828081010_careful_shinobi_shaw",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1757772919975,
"tag": "20250913141519_clumsy_maria_hill",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1757819341115,
"tag": "20250914030901_majestic_black_widow",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1759395147253,
"tag": "20251002085227_deep_silver_sable",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1767777840967,
"tag": "20260107092400_add_collaboration_and_request_time",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1768042908810,
"tag": "20260110000001_add_replay_requests",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1769766215307,
"tag": "20260130094335_add_replay_request_status",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1770895837437,
"tag": "20260212113037_add_user_identities_table",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1772187630689,
"tag": "20260227102030_add_monthly_submission_limit",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1772889825480,
"tag": "20260307132345_add_gongan_filing",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1772890398452,
"tag": "20260307133318_remove_gongan_link",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1773923154561,
"tag": "20260319122554_add_collaborative_submission_toggle",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1773927000000,
"tag": "20260319133000_add_submission_remarks",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1775136952492,
"tag": "20260402133552_remarkable_star_brand",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1775323961479,
"tag": "20260404173241_wandering_marvel_apes",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1776517400000,
"tag": "20260418093000_add_site_version",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1776517800000,
"tag": "20260418103000_add_changelog_entries",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1776518400000,
"tag": "20260418112000_add_song_comments",
"breakpoints": true
}
]
}
+13
View File
@@ -0,0 +1,13 @@
import { relations } from "drizzle-orm/relations";
import { user, userIdentity } from "./schema";
export const userIdentityRelations = relations(userIdentity, ({one}) => ({
user: one(user, {
fields: [userIdentity.userId],
references: [user.id]
}),
}));
export const userRelations = relations(user, ({many}) => ({
userIdentities: many(userIdentity),
}));
+284
View File
@@ -0,0 +1,284 @@
import { pgTable, serial, timestamp, varchar, text, integer, boolean, bigint, uuid, unique, foreignKey, pgEnum } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
export const blacklistType = pgEnum("BlacklistType", ['SONG', 'KEYWORD'])
export const collaboratorStatus = pgEnum("collaborator_status", ['PENDING', 'ACCEPTED', 'REJECTED'])
export const replayRequestStatus = pgEnum("replay_request_status", ['PENDING', 'FULFILLED', 'REJECTED'])
export const userStatus = pgEnum("user_status", ['active', 'withdrawn'])
export const emailTemplate = pgTable("EmailTemplate", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
key: varchar({ length: 100 }).notNull(),
name: varchar({ length: 200 }).notNull(),
subject: varchar({ length: 300 }).notNull(),
html: text().notNull(),
updatedByUserId: integer(),
});
export const notification = pgTable("Notification", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
type: text().notNull(),
message: text().notNull(),
read: boolean().default(false).notNull(),
userId: integer().notNull(),
songId: integer(),
});
export const notificationSettings = pgTable("NotificationSettings", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
userId: integer().notNull(),
enabled: boolean().default(true).notNull(),
songRequestEnabled: boolean().default(true).notNull(),
songVotedEnabled: boolean().default(true).notNull(),
songPlayedEnabled: boolean().default(true).notNull(),
refreshInterval: integer().default(60).notNull(),
songVotedThreshold: integer().default(1).notNull(),
});
export const playTime = pgTable("PlayTime", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
name: text().notNull(),
startTime: text(),
endTime: text(),
enabled: boolean().default(true).notNull(),
description: text(),
});
export const requestTime = pgTable("RequestTime", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ precision: 6, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ precision: 6, mode: 'string' }).defaultNow().notNull(),
name: text().notNull(),
startTime: timestamp({ mode: 'string' }).notNull(),
endTime: timestamp({ mode: 'string' }).notNull(),
enabled: boolean().default(true).notNull(),
description: text(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
expected: bigint({ mode: "number" }).default(0).notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
accepted: bigint({ mode: "number" }).default(0).notNull(),
past: boolean().default(false).notNull(),
});
export const schedule = pgTable("Schedule", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
songId: integer().notNull(),
playDate: timestamp({ mode: 'string' }).notNull(),
played: boolean().default(false).notNull(),
sequence: integer().default(1).notNull(),
playTimeId: integer(),
isDraft: boolean().default(false).notNull(),
publishedAt: timestamp({ mode: 'string' }),
});
export const semester = pgTable("Semester", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
name: text().notNull(),
isActive: boolean().default(false).notNull(),
});
export const song = pgTable("Song", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
title: text().notNull(),
artist: text().notNull(),
requesterId: integer().notNull(),
played: boolean().default(false).notNull(),
playedAt: timestamp({ mode: 'string' }).defaultNow(),
semester: text(),
preferredPlayTimeId: integer(),
cover: text(),
playUrl: text(),
musicPlatform: text(),
musicId: text(),
hitRequestId: integer(),
});
export const songBlacklist = pgTable("SongBlacklist", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
type: blacklistType().notNull(),
value: text().notNull(),
reason: text(),
isActive: boolean().default(true).notNull(),
createdBy: integer(),
});
export const systemSettings = pgTable("SystemSettings", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
enablePlayTimeSelection: boolean().default(false).notNull(),
siteTitle: text(),
siteLogoUrl: text(),
schoolLogoHomeUrl: text(),
schoolLogoPrintUrl: text(),
siteDescription: text(),
submissionGuidelines: text(),
icpNumber: text(),
enableSubmissionLimit: boolean().default(false).notNull(),
dailySubmissionLimit: integer(),
weeklySubmissionLimit: integer(),
showBlacklistKeywords: boolean().default(false).notNull(),
hideStudentInfo: boolean().default(true).notNull(),
smtpEnabled: boolean().default(false).notNull(),
smtpHost: text(),
smtpPort: integer().default(587),
smtpSecure: boolean().default(false),
smtpUsername: text(),
smtpPassword: text(),
smtpFromEmail: text(),
smtpFromName: text().default('校园广播站'),
enableRequestTimeLimitation: boolean().default(false).notNull(),
forceBlockAllRequests: boolean().default(false).notNull(),
enableReplayRequests: boolean().default(false).notNull(),
monthlySubmissionLimit: integer(),
gonganNumber: text(),
enableCollaborativeSubmission: boolean().default(true).notNull(),
});
export const vote = pgTable("Vote", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
songId: integer().notNull(),
userId: integer().notNull(),
});
export const apiKeyPermissions = pgTable("api_key_permissions", {
id: uuid().defaultRandom().primaryKey().notNull(),
apiKeyId: uuid("api_key_id").notNull(),
permission: varchar({ length: 100 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
});
export const apiKeys = pgTable("api_keys", {
id: uuid().defaultRandom().primaryKey().notNull(),
name: varchar({ length: 255 }).notNull(),
description: text(),
keyHash: varchar("key_hash", { length: 255 }).notNull(),
keyPrefix: varchar("key_prefix", { length: 10 }).notNull(),
isActive: boolean("is_active").default(true).notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true, mode: 'string' }),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: 'string' }),
createdByUserId: integer("created_by_user_id").notNull(),
usageCount: integer("usage_count").default(0).notNull(),
}, (table) => [
unique("api_keys_key_hash_unique").on(table.keyHash),
]);
export const apiLogs = pgTable("api_logs", {
id: uuid().defaultRandom().primaryKey().notNull(),
apiKeyId: uuid("api_key_id"),
endpoint: varchar({ length: 500 }).notNull(),
method: varchar({ length: 10 }).notNull(),
ipAddress: text("ip_address").notNull(),
userAgent: text("user_agent"),
statusCode: integer("status_code").notNull(),
responseTimeMs: integer("response_time_ms").notNull(),
requestBody: text("request_body"),
responseBody: text("response_body"),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
errorMessage: text("error_message"),
});
export const collaborationLogs = pgTable("collaboration_logs", {
id: uuid().defaultRandom().primaryKey().notNull(),
collaboratorId: uuid("collaborator_id").notNull(),
action: varchar({ length: 50 }).notNull(),
operatorId: integer("operator_id").notNull(),
ipAddress: text("ip_address"),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
});
export const songCollaborators = pgTable("song_collaborators", {
id: uuid().defaultRandom().primaryKey().notNull(),
songId: integer("song_id").notNull(),
userId: integer("user_id").notNull(),
status: collaboratorStatus().default('PENDING').notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
});
export const songReplayRequests = pgTable("song_replay_requests", {
id: serial().primaryKey().notNull(),
songId: integer("song_id").notNull(),
userId: integer("user_id").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
status: replayRequestStatus().default('PENDING').notNull(),
}, (table) => [
unique("song_replay_requests_song_id_user_id_unique").on(table.songId, table.userId),
]);
export const userStatusLogs = pgTable("user_status_logs", {
id: serial().primaryKey().notNull(),
userId: integer("user_id").notNull(),
oldStatus: userStatus("old_status"),
newStatus: userStatus("new_status").notNull(),
reason: text(),
operatorId: integer("operator_id"),
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
});
export const user = pgTable("User", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
username: text().notNull(),
name: text(),
grade: text(),
class: text(),
role: text().default('USER').notNull(),
password: text().notNull(),
email: text(),
emailVerified: boolean().default(false),
lastLogin: timestamp({ mode: 'string' }),
lastLoginIp: text(),
passwordChangedAt: timestamp({ mode: 'string' }),
forcePasswordChange: boolean().default(true).notNull(),
meowNickname: text(),
meowBoundAt: timestamp({ mode: 'string' }),
status: userStatus().default('active').notNull(),
statusChangedAt: timestamp({ mode: 'string' }).defaultNow(),
statusChangedBy: integer(),
});
export const userIdentity = pgTable("UserIdentity", {
id: serial().primaryKey().notNull(),
userId: integer().notNull(),
provider: text().notNull(),
providerUserId: text().notNull(),
providerUsername: text(),
createdAt: timestamp({ mode: 'string' }).defaultNow().notNull(),
}, (table) => [
foreignKey({
columns: [table.userId],
foreignColumns: [user.id],
name: "UserIdentity_userId_User_id_fk"
}).onDelete("cascade"),
unique("UserIdentity_provider_providerUserId_unique").on(table.provider, table.providerUserId),
]);
export const drizzleMigrations = pgTable("__drizzle_migrations__", {
id: serial().primaryKey().notNull(),
hash: text().notNull(),
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
createdAt: bigint("created_at", { mode: "number" }),
});
+863
View File
@@ -0,0 +1,863 @@
import {bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, unique, index, uniqueIndex} from 'drizzle-orm/pg-core';
import {relations, sql} from 'drizzle-orm';
// 枚举定义
export const blacklistTypeEnum = pgEnum('BlacklistType', ['SONG', 'KEYWORD', 'LANGUAGE', 'GENRE']);
export const userStatusEnum = pgEnum('user_status', ['active', 'withdrawn', 'graduate', 'pending', 'rejected']);
export const collaboratorStatusEnum = pgEnum('collaborator_status', ['PENDING', 'ACCEPTED', 'REJECTED']);
export const replayRequestStatusEnum = pgEnum('replay_request_status', ['PENDING', 'FULFILLED', 'REJECTED']);
export const cardCodeStatusEnum = pgEnum('card_code_status', ['AVAILABLE', 'LOCKED', 'REDEEMED', 'INVALID']);
// 用户表
export const users = pgTable('User', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
username: text('username').notNull(),
name: text('name'),
grade: text('grade'),
class: text('class'),
studentId: text('studentId'),
role: text('role').default('USER').notNull(),
password: text('password').notNull(),
email: text('email'),
emailVerified: boolean('emailVerified').default(false),
lastLogin: timestamp('lastLogin'),
lastLoginIp: text('lastLoginIp'),
passwordChangedAt: timestamp('passwordChangedAt'),
forcePasswordChange: boolean('forcePasswordChange').default(false).notNull(),
meowNickname: text('meowNickname'),
meowBoundAt: timestamp('meowBoundAt'),
status: userStatusEnum('status').default('active').notNull(),
statusChangedAt: timestamp('statusChangedAt').defaultNow(),
statusChangedBy: integer('statusChangedBy'),
tokenVersion: integer('tokenVersion').default(0).notNull(),
avatarProvider: text('avatarProvider'),
avatarProviderUserId: text('avatarProviderUserId'),
remark: text('remark'),
}, (table) => [
uniqueIndex('User_username_unique').on(table.username),
]);
// 认证会话表
export const authSessions = pgTable('auth_sessions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
sessionToken: varchar('session_token', { length: 255 }).notNull().unique(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
lastActiveAt: timestamp('last_active_at', { withTimezone: true }).defaultNow().notNull(),
isRevoked: boolean('is_revoked').default(false).notNull(),
}, (table) => [
index('auth_sessions_user_id_idx').on(table.userId),
index('auth_sessions_expires_at_idx').on(table.expiresAt),
uniqueIndex('auth_sessions_session_token_unique').on(table.sessionToken),
]);
// 年级班级配置表
export const gradeClasses = pgTable('GradeClass', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
grade: text('grade').notNull(),
class: text('class').notNull(),
enabled: boolean('enabled').default(true).notNull(),
}, (table) => [
unique('grade_class_unique').on(table.grade, table.class),
]);
// 播出时段表
export const playTimes = pgTable('PlayTime', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
name: text('name').notNull(),
startTime: text('startTime'),
endTime: text('endTime'),
enabled: boolean('enabled').default(true).notNull(),
description: text('description'),
});
// 歌曲表
export const songs = pgTable('Song', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
kgHash: varchar('kg_hash', { length: 64 }),
title: varchar('title', { length: 100 }).notNull(),
singer: varchar('singer', { length: 100 }),
coverUrl: varchar('cover_url', { length: 255 }),
artist: text('artist').notNull(),
requesterId: integer('requesterId').notNull(),
played: boolean('played').default(false).notNull(),
playedAt: timestamp('playedAt').defaultNow(),
semester: text('semester'),
preferredPlayTimeId: integer('preferredPlayTimeId'),
cover: text('cover'),
playUrl: text('playUrl'),
musicPlatform: text('musicPlatform'),
musicId: text('musicId'),
submissionNote: text('submissionNote'),
submissionNotePublic: boolean('submissionNotePublic').default(false).notNull(),
hitRequestId: integer(),
expectedPlayDate: text('expectedPlayDate'),
expectedPlaySequence: integer('expectedPlaySequence'),
durationSeconds: integer('durationSeconds'),
submissionNotePublicStatus: text('submissionNotePublicStatus'),
cardCodeId: integer('cardCodeId').references(() => cardCodes.id, { onDelete: 'set null' }),
}, (table) => [
index('song_card_code_id_idx').on(table.cardCodeId),
index('song_semester_created_at_idx').on(table.semester, table.createdAt),
index('song_requester_id_idx').on(table.requesterId),
]);
// 投票表
export const votes = pgTable('Vote', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
songId: integer('songId').notNull(),
userId: integer('userId').notNull(),
}, (table) => [
unique('vote_song_user_unique').on(table.songId, table.userId),
index('vote_user_song_idx').on(table.userId, table.songId),
]);
export const songComments = pgTable('SongComment', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
songId: integer('songId').notNull(),
userId: integer('userId').notNull(),
content: text('content').notNull(),
});
// 排期表
export const schedules = pgTable('Schedule', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
songId: integer('songId').notNull(),
playDate: timestamp('playDate').notNull(),
played: boolean('played').default(false).notNull(),
sequence: integer('sequence').default(1).notNull(),
playTimeId: integer('playTimeId'),
isDraft: boolean('isDraft').default(false).notNull(),
publishedAt: timestamp('publishedAt'),
replayRequestId: integer('replay_request_id'),
}, (table) => [
index('schedule_published_song_idx').on(table.publishedAt, table.songId),
index('schedule_published_date_idx').on(table.publishedAt, table.playDate),
]);
// 排期备选池表
export const scheduleSongPool = pgTable('schedule_song_pool', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
songId: integer('song_id').notNull().references(() => songs.id, { onDelete: 'cascade' }),
targetPlayDate: timestamp('target_play_date'),
priority: integer('priority').default(0).notNull(),
addedBy: integer('added_by'),
note: text('note'),
status: text('status').default('PENDING').notNull(),
}, (table) => [
index('schedule_song_pool_song_id_idx').on(table.songId),
index('schedule_song_pool_target_date_idx').on(table.targetPlayDate),
]);
// 期望播出日期表
export const expectedPlayDates = pgTable('ExpectedPlayDate', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
date: text('date').notNull().unique(),
availableSequences: text('availableSequences').notNull(),
enabled: boolean('enabled').default(true).notNull(),
});
// 通知表
export const notifications = pgTable('Notification', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
type: text('type').notNull(),
message: text('message').notNull(),
read: boolean('read').default(false).notNull(),
userId: integer('userId').notNull(),
songId: integer('songId'),
batchId: uuid('batch_id'),
source: text('source').default('SYSTEM').notNull(),
senderId: integer('sender_id'),
senderName: text('sender_name'),
senderUsername: text('sender_username'),
title: text('title'),
important: boolean('important').default(false).notNull(),
userDeleted: boolean('user_deleted').default(false).notNull(),
}, (table) => [
index('notification_user_important_read_created_idx').on(table.userId, table.important, table.read, table.createdAt),
index('notification_batch_id_idx').on(table.batchId),
]);
// 通知设置表
export const notificationSettings = pgTable('NotificationSettings', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
userId: integer('userId').notNull(),
enabled: boolean('enabled').default(true).notNull(),
songRequestEnabled: boolean('songRequestEnabled').default(true).notNull(),
songVotedEnabled: boolean('songVotedEnabled').default(true).notNull(),
songPlayedEnabled: boolean('songPlayedEnabled').default(true).notNull(),
refreshInterval: integer('refreshInterval').default(60).notNull(),
songVotedThreshold: integer('songVotedThreshold').default(1).notNull(),
});
// 学期表
export const semesters = pgTable('Semester', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
name: text('name').notNull(),
isActive: boolean('isActive').default(false).notNull(),
});
// 系统设置表
export const systemSettings = pgTable('SystemSettings', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
enablePlayTimeSelection: boolean('enablePlayTimeSelection').default(false).notNull(),
siteTitle: text('siteTitle'),
siteLogoUrl: text('siteLogoUrl'),
schoolLogoHomeUrl: text('schoolLogoHomeUrl'),
schoolLogoPrintUrl: text('schoolLogoPrintUrl'),
siteDescription: text('siteDescription'),
siteVersion: text('siteVersion'),
submissionGuidelines: text('submissionGuidelines'),
icpNumber: text('icpNumber'),
gonganNumber: text('gonganNumber'),
enableSubmissionLimit: boolean('enableSubmissionLimit').default(false).notNull(),
dailySubmissionLimit: integer('dailySubmissionLimit'),
weeklySubmissionLimit: integer('weeklySubmissionLimit'),
monthlySubmissionLimit: integer('monthlySubmissionLimit'),
showBlacklistKeywords: boolean('showBlacklistKeywords').default(false).notNull(),
hideStudentInfo: boolean('hideStudentInfo').default(true).notNull(),
smtpEnabled: boolean('smtpEnabled').default(false).notNull(),
smtpHost: text('smtpHost'),
smtpPort: integer('smtpPort').default(587),
smtpSecure: boolean('smtpSecure').default(false),
smtpUsername: text('smtpUsername'),
smtpPassword: text('smtpPassword'),
smtpFromEmail: text('smtpFromEmail'),
smtpFromName: text('smtpFromName').default('校园广播站'),
enableRequestTimeLimitation: boolean('enableRequestTimeLimitation').default(false).notNull(),
forceBlockAllRequests: boolean('forceBlockAllRequests').default(false).notNull(),
enableReplayRequests: boolean('enableReplayRequests').default(false).notNull(),
enableCollaborativeSubmission: boolean('enableCollaborativeSubmission').default(true).notNull(),
enableSubmissionRemarks: boolean('enableSubmissionRemarks').default(false).notNull(),
instanceId: text('instanceId'),
telemetryEnabled: boolean('telemetryEnabled').default(true).notNull(),
showBeianIcon: boolean('showBeianIcon').default(true).notNull(),
scheduleDaysDisplay: integer('scheduleDaysDisplay').default(7).notNull(),
scheduleDaysEditable: integer('scheduleDaysEditable').default(3).notNull(),
scheduleDaysRequestable: integer('scheduleDaysRequestable').default(14).notNull(),
cardCodeEnabled: boolean('cardCodeEnabled').default(false).notNull(),
cardCodeSubmissionRequired: boolean('cardCodeSubmissionRequired').default(false).notNull(),
cardCodeVoteEnabled: boolean('cardCodeVoteEnabled').default(false).notNull(),
cardCodeReplayEnabled: boolean('cardCodeReplayEnabled').default(false).notNull(),
submissionRestrictionEnabled: boolean('submissionRestrictionEnabled').default(false).notNull(),
submissionRestrictionGradeEnabled: boolean('submissionRestrictionGradeEnabled').default(false).notNull(),
submissionRestrictionClassEnabled: boolean('submissionRestrictionClassEnabled').default(false).notNull(),
submissionRestrictionGraduateEnabled: boolean('submissionRestrictionGraduateEnabled').default(false).notNull(),
submissionRestrictionWithdrawnEnabled: boolean('submissionRestrictionWithdrawnEnabled').default(false).notNull(),
captchaProvider: text('captchaProvider').default('TURNSTILE'),
captchaEnabled: boolean('captchaEnabled').default(false).notNull(),
turnstileSiteKey: text('turnstileSiteKey'),
turnstileSecretKey: text('turnstileSecretKey'),
allowRegister: boolean('allowRegister').default(true).notNull(),
registerRequireEmail: boolean('registerRequireEmail').default(false).notNull(),
registerRequireEmailVerify: boolean('registerRequireEmailVerify').default(false).notNull(),
registerDefaultRole: text('registerDefaultRole').default('USER'),
registerEnabledGradeClasses: text('registerEnabledGradeClasses'),
oauthEnabled: boolean('oauthEnabled').default(false).notNull(),
oauthAllowRegister: boolean('oauthAllowRegister').default(true).notNull(),
oauthAllowLogin: boolean('oauthAllowLogin').default(true).notNull(),
oauthAllowBind: boolean('oauthAllowBind').default(true).notNull(),
autoBackupEnabled: boolean('autoBackupEnabled').default(false).notNull(),
autoBackupIntervalHours: integer('autoBackupIntervalHours').default(24).notNull(),
autoBackupRetentionDays: integer('autoBackupRetentionDays').default(30).notNull(),
statisticsCode: text('statisticsCode'),
defaultTheme: text('defaultTheme').default('light'),
enabledThemes: text('enabledThemes').default('light,dark'),
enabledPlatforms: text('enabledPlatforms'),
platformOrder: text('platformOrder'),
});
// 歌曲黑名单表
export const songBlacklists = pgTable('SongBlacklist', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
type: blacklistTypeEnum('type').notNull(),
value: text('value').notNull(),
reason: text('reason'),
isActive: boolean('isActive').default(true).notNull(),
createdBy: integer('createdBy'),
});
// 卡密表
export const cardCodes = pgTable('card_codes', {
id: serial('id').primaryKey(),
code: varchar('code', { length: 64 }).notNull().unique(),
status: cardCodeStatusEnum('status').default('AVAILABLE').notNull(),
type: text('type').notNull(),
value: integer('value').default(1).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }),
lockedAt: timestamp('locked_at', { withTimezone: true }),
lockedBy: integer('locked_by'),
batchId: varchar('batch_id', { length: 64 }),
note: text('note'),
}, (table) => [
index('card_codes_status_idx').on(table.status),
index('card_codes_batch_id_idx').on(table.batchId),
index('card_codes_expires_at_idx').on(table.expiresAt),
]);
// 卡密兑换记录表
export const cardCodeRedemptions = pgTable('card_code_redemptions', {
id: serial('id').primaryKey(),
cardCodeId: integer('card_code_id').notNull().references(() => cardCodes.id, { onDelete: 'cascade' }),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
redeemedAt: timestamp('redeemed_at', { withTimezone: true }).defaultNow().notNull(),
songId: integer('song_id').references(() => songs.id, { onDelete: 'set null' }),
purpose: text('purpose').notNull(),
}, (table) => [
index('card_code_redemptions_user_id_idx').on(table.userId),
index('card_code_redemptions_card_code_id_idx').on(table.cardCodeId),
]);
// OAuth 提供商表
export const oauthProviders = pgTable('oauth_providers', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
displayName: text('displayName'),
enabled: boolean('enabled').default(true).notNull(),
clientId: text('clientId'),
clientSecret: text('clientSecret'),
authorizationEndpoint: text('authorizationEndpoint'),
tokenEndpoint: text('tokenEndpoint'),
userInfoEndpoint: text('userInfoEndpoint'),
scope: text('scope'),
redirectUri: text('redirectUri'),
usernameClaim: text('usernameClaim').default('preferred_username'),
nameClaim: text('nameClaim').default('name'),
emailClaim: text('emailClaim').default('email'),
avatarClaim: text('avatarClaim'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
sortOrder: integer('sortOrder').default(0).notNull(),
icon: text('icon'),
});
// 用户 Passkeys 表
export const userPasskeys = pgTable('user_passkeys', {
id: text('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name'),
publicKey: text('public_key').notNull(),
credentialDeviceType: text('credential_device_type').notNull(),
credentialBackedUp: boolean('credential_backed_up').notNull(),
transports: text('transports'),
counter: integer('counter').default(0).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
}, (table) => [
index('user_passkeys_user_id_idx').on(table.userId),
]);
// Passkey Challenges 表
export const passkeyChallenges = pgTable('passkey_challenges', {
id: uuid('id').primaryKey().defaultRandom(),
challenge: text('challenge').notNull(),
type: text('type').notNull(),
userId: integer('user_id'),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => [
index('passkey_challenges_challenge_idx').on(table.challenge),
index('passkey_challenges_expires_at_idx').on(table.expiresAt),
]);
// 备份记录表
export const backupRecords = pgTable('backup_records', {
id: serial('id').primaryKey(),
filename: text('filename').notNull(),
filePath: text('file_path').notNull(),
fileSize: bigint('file_size', { mode: 'number' }).notNull(),
type: text('type').default('MANUAL').notNull(),
status: text('status').default('COMPLETED').notNull(),
errorMessage: text('error_message'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdBy: integer('created_by'),
expiresAt: timestamp('expires_at', { withTimezone: true }),
}, (table) => [
index('backup_records_created_at_idx').on(table.createdAt),
index('backup_records_status_idx').on(table.status),
]);
// 投稿限制豁免表
export const submissionRestrictionExemptions = pgTable('submission_restriction_exemptions', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
exemptionType: text('exemption_type').notNull(),
reason: text('reason'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }),
createdBy: integer('created_by'),
}, (table) => [
unique('submission_restriction_exemptions_user_type_unique').on(table.userId, table.exemptionType),
index('submission_restriction_exemptions_user_id_idx').on(table.userId),
]);
// API Keys表
export const apiKeys = pgTable('api_keys', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
keyHash: varchar('key_hash', { length: 255 }).notNull().unique(),
keyPrefix: varchar('key_prefix', { length: 10 }).notNull(),
isActive: boolean('is_active').default(true).notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
createdByUserId: integer('created_by_user_id').notNull(),
usageCount: integer('usage_count').default(0).notNull(),
});
// API Key权限表
export const apiKeyPermissions = pgTable('api_key_permissions', {
id: uuid('id').primaryKey().defaultRandom(),
apiKeyId: uuid('api_key_id').notNull(),
permission: varchar('permission', { length: 100 }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
// API访问日志表
export const apiLogs = pgTable('api_logs', {
id: uuid('id').primaryKey().defaultRandom(),
apiKeyId: uuid('api_key_id'),
endpoint: varchar('endpoint', { length: 500 }).notNull(),
method: varchar('method', { length: 10 }).notNull(),
ipAddress: text('ip_address').notNull(),
userAgent: text('user_agent'),
statusCode: integer('status_code').notNull(),
responseTimeMs: integer('response_time_ms').notNull(),
requestBody: text('request_body'),
responseBody: text('response_body'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
errorMessage: text('error_message'),
});
// 用户状态变更日志表
export const userStatusLogs = pgTable('user_status_logs', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull(),
oldStatus: userStatusEnum('old_status'),
newStatus: userStatusEnum('new_status').notNull(),
reason: text('reason'),
operatorId: integer('operator_id'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const requestTimes = pgTable("RequestTime", {
id: serial().primaryKey().notNull(),
createdAt: timestamp({ precision: 6, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp({ precision: 6, mode: 'string' }).defaultNow().notNull(),
name: text().notNull(),
startTime: timestamp({ mode: 'string' }).notNull(),
endTime: timestamp({ mode: 'string' }).notNull(),
enabled: boolean().default(true).notNull(),
description: text(),
expected: bigint({ mode: "number" }).default(0).notNull(),
accepted: bigint({ mode: "number" }).default(0).notNull(),
past: boolean().default(false).notNull(),
});
// 联合投稿人表
export const songCollaborators = pgTable('song_collaborators', {
id: uuid('id').primaryKey().defaultRandom(),
songId: integer('song_id').notNull(),
userId: integer('user_id').notNull(),
status: collaboratorStatusEnum('status').default('PENDING').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
// 联合投稿审计日志表
export const collaborationLogs = pgTable('collaboration_logs', {
id: uuid('id').primaryKey().defaultRandom(),
collaboratorId: uuid('collaborator_id').notNull(),
action: varchar('action', { length: 50 }).notNull(),
operatorId: integer('operator_id').notNull(),
ipAddress: text('ip_address'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
// 歌曲重播申请表
export const songReplayRequests = pgTable('song_replay_requests', {
id: serial('id').primaryKey(),
songId: integer('song_id').notNull(),
userId: integer('user_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
status: replayRequestStatusEnum('status').default('PENDING').notNull(),
}, (t) => ({
unq: unique().on(t.songId, t.userId),
}));
// 第三方身份关联表
export const userIdentities = pgTable('UserIdentity', {
id: serial('id').primaryKey(),
userId: integer('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
provider: text('provider').notNull(),
providerUserId: text('providerUserId').notNull(),
providerUsername: text('providerUsername'),
createdAt: timestamp('createdAt').defaultNow().notNull(),
}, (t) => ({
unq: unique().on(t.provider, t.providerUserId),
}));
export const changelogEntries = pgTable('ChangelogEntry', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
version: text('version').notNull(),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
publishedAt: timestamp('publishedAt'),
createdBy: integer('createdBy'),
});
// 邮件模板表
export const emailTemplates = pgTable('EmailTemplate', {
id: serial('id').primaryKey(),
createdAt: timestamp('createdAt').defaultNow().notNull(),
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
key: varchar('key', { length: 100 }).notNull(),
name: varchar('name', { length: 200 }).notNull(),
subject: varchar('subject', { length: 300 }).notNull(),
html: text('html').notNull(),
updatedByUserId: integer('updatedByUserId'),
});
// 关系定义
export const usersRelations = relations(users, ({ many, one }) => ({
songs: many(songs),
votes: many(votes),
notifications: many(notifications),
songComments: many(songComments),
identities: many(userIdentities),
notificationSettings: one(notificationSettings, {
fields: [users.id],
references: [notificationSettings.userId],
}),
apiKeys: many(apiKeys),
statusLogs: many(userStatusLogs),
collaborations: many(songCollaborators),
replayRequests: many(songReplayRequests),
statusChangedByUser: one(users, {
fields: [users.statusChangedBy],
references: [users.id],
}),
authSessions: many(authSessions),
passkeys: many(userPasskeys),
cardCodeRedemptions: many(cardCodeRedemptions),
submissionRestrictionExemptions: many(submissionRestrictionExemptions),
}));
export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
user: one(users, {
fields: [userIdentities.userId],
references: [users.id],
}),
}));
export const authSessionsRelations = relations(authSessions, ({ one }) => ({
user: one(users, {
fields: [authSessions.userId],
references: [users.id],
}),
}));
export const userPasskeysRelations = relations(userPasskeys, ({ one }) => ({
user: one(users, {
fields: [userPasskeys.userId],
references: [users.id],
}),
}));
export const songsRelations = relations(songs, ({ one, many }) => ({
requester: one(users, {
fields: [songs.requesterId],
references: [users.id],
}),
preferredPlayTime: one(playTimes, {
fields: [songs.preferredPlayTimeId],
references: [playTimes.id],
}),
votes: many(votes),
comments: many(songComments),
schedules: many(schedules),
notifications: many(notifications),
collaborators: many(songCollaborators),
replayRequests: many(songReplayRequests),
cardCode: one(cardCodes, {
fields: [songs.cardCodeId],
references: [cardCodes.id],
}),
scheduleSongPool: many(scheduleSongPool),
cardCodeRedemptions: many(cardCodeRedemptions),
}));
export const songCommentsRelations = relations(songComments, ({ one }) => ({
song: one(songs, {
fields: [songComments.songId],
references: [songs.id],
}),
user: one(users, {
fields: [songComments.userId],
references: [users.id],
}),
}));
export const votesRelations = relations(votes, ({ one }) => ({
song: one(songs, {
fields: [votes.songId],
references: [songs.id],
}),
user: one(users, {
fields: [votes.userId],
references: [users.id],
}),
}));
export const schedulesRelations = relations(schedules, ({ one }) => ({
song: one(songs, {
fields: [schedules.songId],
references: [songs.id],
}),
playTime: one(playTimes, {
fields: [schedules.playTimeId],
references: [playTimes.id],
}),
}));
export const scheduleSongPoolRelations = relations(scheduleSongPool, ({ one }) => ({
song: one(songs, {
fields: [scheduleSongPool.songId],
references: [songs.id],
}),
}));
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, {
fields: [notifications.userId],
references: [users.id],
}),
song: one(songs, {
fields: [notifications.songId],
references: [songs.id],
}),
sender: one(users, {
fields: [notifications.senderId],
references: [users.id],
}),
}));
export const notificationSettingsRelations = relations(notificationSettings, ({ one }) => ({
user: one(users, {
fields: [notificationSettings.userId],
references: [users.id],
}),
}));
export const playTimesRelations = relations(playTimes, ({ many }) => ({
songs: many(songs),
schedules: many(schedules),
}));
export const apiKeysRelations = relations(apiKeys, ({ one, many }) => ({
createdByUser: one(users, {
fields: [apiKeys.createdByUserId],
references: [users.id],
}),
permissions: many(apiKeyPermissions),
logs: many(apiLogs),
}));
export const apiKeyPermissionsRelations = relations(apiKeyPermissions, ({ one }) => ({
apiKey: one(apiKeys, {
fields: [apiKeyPermissions.apiKeyId],
references: [apiKeys.id],
}),
}));
export const apiLogsRelations = relations(apiLogs, ({ one }) => ({
apiKey: one(apiKeys, {
fields: [apiLogs.apiKeyId],
references: [apiKeys.id],
}),
}));
export const userStatusLogsRelations = relations(userStatusLogs, ({ one }) => ({
user: one(users, {
fields: [userStatusLogs.userId],
references: [users.id],
}),
operator: one(users, {
fields: [userStatusLogs.operatorId],
references: [users.id],
}),
}));
export const songCollaboratorsRelations = relations(songCollaborators, ({ one, many }) => ({
song: one(songs, {
fields: [songCollaborators.songId],
references: [songs.id],
}),
user: one(users, {
fields: [songCollaborators.userId],
references: [users.id],
}),
logs: many(collaborationLogs),
}));
export const collaborationLogsRelations = relations(collaborationLogs, ({ one }) => ({
collaborator: one(songCollaborators, {
fields: [collaborationLogs.collaboratorId],
references: [songCollaborators.id],
}),
operator: one(users, {
fields: [collaborationLogs.operatorId],
references: [users.id],
}),
}));
export const songReplayRequestsRelations = relations(songReplayRequests, ({ one }) => ({
song: one(songs, {
fields: [songReplayRequests.songId],
references: [songs.id],
}),
user: one(users, {
fields: [songReplayRequests.userId],
references: [users.id],
}),
}));
export const cardCodesRelations = relations(cardCodes, ({ many }) => ({
songs: many(songs),
redemptions: many(cardCodeRedemptions),
}));
export const cardCodeRedemptionsRelations = relations(cardCodeRedemptions, ({ one }) => ({
cardCode: one(cardCodes, {
fields: [cardCodeRedemptions.cardCodeId],
references: [cardCodes.id],
}),
user: one(users, {
fields: [cardCodeRedemptions.userId],
references: [users.id],
}),
song: one(songs, {
fields: [cardCodeRedemptions.songId],
references: [songs.id],
}),
}));
export const submissionRestrictionExemptionsRelations = relations(submissionRestrictionExemptions, ({ one }) => ({
user: one(users, {
fields: [submissionRestrictionExemptions.userId],
references: [users.id],
}),
createdByUser: one(users, {
fields: [submissionRestrictionExemptions.createdBy],
references: [users.id],
}),
}));
// 导出所有表的类型
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type AuthSession = typeof authSessions.$inferSelect;
export type NewAuthSession = typeof authSessions.$inferInsert;
export type GradeClass = typeof gradeClasses.$inferSelect;
export type NewGradeClass = typeof gradeClasses.$inferInsert;
export type Song = typeof songs.$inferSelect;
export type NewSong = typeof songs.$inferInsert;
export type Vote = typeof votes.$inferSelect;
export type NewVote = typeof votes.$inferInsert;
export type SongComment = typeof songComments.$inferSelect;
export type NewSongComment = typeof songComments.$inferInsert;
export type Schedule = typeof schedules.$inferSelect;
export type NewSchedule = typeof schedules.$inferInsert;
export type ScheduleSongPool = typeof scheduleSongPool.$inferSelect;
export type NewScheduleSongPool = typeof scheduleSongPool.$inferInsert;
export type ExpectedPlayDate = typeof expectedPlayDates.$inferSelect;
export type NewExpectedPlayDate = typeof expectedPlayDates.$inferInsert;
export type Notification = typeof notifications.$inferSelect;
export type NewNotification = typeof notifications.$inferInsert;
export type NotificationSettings = typeof notificationSettings.$inferSelect;
export type NewNotificationSettings = typeof notificationSettings.$inferInsert;
export type PlayTime = typeof playTimes.$inferSelect;
export type NewPlayTime = typeof playTimes.$inferInsert;
export type Semester = typeof semesters.$inferSelect;
export type NewSemester = typeof semesters.$inferInsert;
export type SystemSettings = typeof systemSettings.$inferSelect;
export type NewSystemSettings = typeof systemSettings.$inferInsert;
export type SongBlacklist = typeof songBlacklists.$inferSelect;
export type NewSongBlacklist = typeof songBlacklists.$inferInsert;
export type CardCode = typeof cardCodes.$inferSelect;
export type NewCardCode = typeof cardCodes.$inferInsert;
export type CardCodeRedemption = typeof cardCodeRedemptions.$inferSelect;
export type NewCardCodeRedemption = typeof cardCodeRedemptions.$inferInsert;
export type OAuthProvider = typeof oauthProviders.$inferSelect;
export type NewOAuthProvider = typeof oauthProviders.$inferInsert;
export type UserPasskey = typeof userPasskeys.$inferSelect;
export type NewUserPasskey = typeof userPasskeys.$inferInsert;
export type PasskeyChallenge = typeof passkeyChallenges.$inferSelect;
export type NewPasskeyChallenge = typeof passkeyChallenges.$inferInsert;
export type BackupRecord = typeof backupRecords.$inferSelect;
export type NewBackupRecord = typeof backupRecords.$inferInsert;
export type SubmissionRestrictionExemption = typeof submissionRestrictionExemptions.$inferSelect;
export type NewSubmissionRestrictionExemption = typeof submissionRestrictionExemptions.$inferInsert;
export type ApiKey = typeof apiKeys.$inferSelect;
export type NewApiKey = typeof apiKeys.$inferInsert;
export type ApiKeyPermission = typeof apiKeyPermissions.$inferSelect;
export type NewApiKeyPermission = typeof apiKeyPermissions.$inferInsert;
export type ApiLog = typeof apiLogs.$inferSelect;
export type NewApiLog = typeof apiLogs.$inferInsert;
export type UserStatusLog = typeof userStatusLogs.$inferSelect;
export type NewUserStatusLog = typeof userStatusLogs.$inferInsert;
export type SongCollaborator = typeof songCollaborators.$inferSelect;
export type NewSongCollaborator = typeof songCollaborators.$inferInsert;
export type CollaborationLog = typeof collaborationLogs.$inferSelect;
export type NewCollaborationLog = typeof collaborationLogs.$inferInsert;
export type SongReplayRequest = typeof songReplayRequests.$inferSelect;
export type NewSongReplayRequest = typeof songReplayRequests.$inferInsert;
export type RequestTime = typeof requestTimes.$inferSelect;
export type NewRequestTime = typeof requestTimes.$inferInsert;
export type UserIdentity = typeof userIdentities.$inferSelect;
export type NewUserIdentity = typeof userIdentities.$inferInsert;
export type ChangelogEntry = typeof changelogEntries.$inferSelect;
export type NewChangelogEntry = typeof changelogEntries.$inferInsert;
export type EmailTemplate = typeof emailTemplates.$inferSelect;
export type NewEmailTemplate = typeof emailTemplates.$inferInsert;