984 lines
42 KiB
TypeScript
984 lines
42 KiB
TypeScript
import {bigint, boolean, integer, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, unique, index, uniqueIndex, jsonb} 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(),
|
||
browser: text('browser'),
|
||
operatingSystem: text('operating_system'),
|
||
device: text('device'),
|
||
loginMethod: text('login_method').default('password').notNull(),
|
||
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
||
revokedReason: text('revoked_reason')
|
||
}, (table) => [
|
||
index('auth_sessions_user_active_idx').on(table.userId, table.isRevoked, table.expiresAt),
|
||
index('auth_sessions_expires_at_idx').on(table.expiresAt),
|
||
index('auth_sessions_last_active_idx').on(table.lastActiveAt),
|
||
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(),
|
||
submissionNoteRequiresApproval: boolean('submissionNoteRequiresApproval').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(),
|
||
scheduleDaysBeforeEnabled: boolean('scheduleDaysBeforeEnabled').default(false).notNull(),
|
||
scheduleDaysBefore: integer('scheduleDaysBefore').default(1).notNull(),
|
||
scheduleDaysAfterEnabled: boolean('scheduleDaysAfterEnabled').default(false).notNull(),
|
||
scheduleDaysAfter: integer('scheduleDaysAfter').default(1).notNull(),
|
||
forcePasswordChangeOnFirstLogin: boolean('forcePasswordChangeOnFirstLogin').default(false).notNull(),
|
||
enableSubmissionRestriction: boolean('enableSubmissionRestriction').default(false).notNull(),
|
||
submissionRestrictionScope: text('submissionRestrictionScope').default('all').notNull(),
|
||
sameSongRestrictionHours: integer('sameSongRestrictionHours'),
|
||
sameArtistRestrictionHours: integer('sameArtistRestrictionHours'),
|
||
cardCodeEnabled: boolean('cardCodeEnabled').default(false).notNull(),
|
||
cardCodeSubmissionRequired: boolean('cardCodeSubmissionRequired').default(false).notNull(),
|
||
cardCodeVoteEnabled: boolean('cardCodeVoteEnabled').default(false).notNull(),
|
||
cardCodeReplayEnabled: boolean('cardCodeReplayEnabled').default(false).notNull(),
|
||
enableCardCodeRequests: boolean('enableCardCodeRequests').default(false).notNull(),
|
||
requireCardCodeForRequests: boolean('requireCardCodeForRequests').default(false).notNull(),
|
||
enableCardCodeLimitBypass: boolean('enableCardCodeLimitBypass').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'),
|
||
registerRequiresApproval: boolean('registerRequiresApproval').default(false).notNull(),
|
||
oauthRegisterRequiresApproval: boolean('oauthRegisterRequiresApproval').default(false).notNull(),
|
||
registerEmailRequired: boolean('registerEmailRequired').default(false).notNull(),
|
||
registerRequiresGradeClass: boolean('registerRequiresGradeClass').default(false).notNull(),
|
||
oauthEnabled: boolean('oauthEnabled').default(false).notNull(),
|
||
oauthAllowRegister: boolean('oauthAllowRegister').default(true).notNull(),
|
||
oauthAllowLogin: boolean('oauthAllowLogin').default(true).notNull(),
|
||
oauthAllowBind: boolean('oauthAllowBind').default(true).notNull(),
|
||
allowOAuthRegistration: boolean('allowOAuthRegistration').default(false).notNull(),
|
||
oauthRedirectUri: text('oauthRedirectUri'),
|
||
oauthStateSecret: text('oauthStateSecret'),
|
||
oauthProviders: text('oauthProviders').default('[]'),
|
||
githubOAuthEnabled: boolean('githubOAuthEnabled').default(false).notNull(),
|
||
githubClientId: text('githubClientId'),
|
||
githubClientSecret: text('githubClientSecret'),
|
||
casdoorOAuthEnabled: boolean('casdoorOAuthEnabled').default(false).notNull(),
|
||
casdoorServerUrl: text('casdoorServerUrl'),
|
||
casdoorClientId: text('casdoorClientId'),
|
||
casdoorClientSecret: text('casdoorClientSecret'),
|
||
casdoorOrganizationName: text('casdoorOrganizationName'),
|
||
googleOAuthEnabled: boolean('googleOAuthEnabled').default(false).notNull(),
|
||
googleClientId: text('googleClientId'),
|
||
googleClientSecret: text('googleClientSecret'),
|
||
aggregateOAuthEnabled: boolean('aggregateOAuthEnabled').default(false).notNull(),
|
||
aggregateOAuthAppId: text('aggregateOAuthAppId'),
|
||
aggregateOAuthAppKey: text('aggregateOAuthAppKey'),
|
||
aggregateOAuthLoginType: text('aggregateOAuthLoginType').default('qq'),
|
||
aggregateOAuthEndpoint: text('aggregateOAuthEndpoint'),
|
||
customOAuthEnabled: boolean('customOAuthEnabled').default(false).notNull(),
|
||
customOAuthDisplayName: text('customOAuthDisplayName'),
|
||
customOAuthAuthorizeUrl: text('customOAuthAuthorizeUrl'),
|
||
customOAuthTokenUrl: text('customOAuthTokenUrl'),
|
||
customOAuthUserInfoUrl: text('customOAuthUserInfoUrl'),
|
||
customOAuthScope: text('customOAuthScope'),
|
||
customOAuthClientId: text('customOAuthClientId'),
|
||
customOAuthClientSecret: text('customOAuthClientSecret'),
|
||
customOAuthUserIdField: text('customOAuthUserIdField'),
|
||
customOAuthUsernameField: text('customOAuthUsernameField'),
|
||
customOAuthNameField: text('customOAuthNameField'),
|
||
customOAuthEmailField: text('customOAuthEmailField'),
|
||
customOAuthAvatarField: text('customOAuthAvatarField'),
|
||
captchaMaxFailures: integer('captchaMaxFailures').default(3).notNull(),
|
||
autoBackupEnabled: boolean('autoBackupEnabled').default(false).notNull(),
|
||
autoBackupIntervalHours: integer('autoBackupIntervalHours').default(24).notNull(),
|
||
autoBackupRetentionDays: integer('autoBackupRetentionDays').default(30).notNull(),
|
||
autoBackupConfig: text('autoBackupConfig'),
|
||
statisticsCode: text('statisticsCode'),
|
||
statisticsCodeEnabled: boolean('statisticsCodeEnabled').default(false).notNull(),
|
||
defaultTheme: text('defaultTheme').default('ClassicDark').notNull(),
|
||
enabledThemes: text('enabledThemes').default('[\"System\",\"ClassicDark\",\"ClassicLight\",\"ModernLight\"]').notNull(),
|
||
enabledPlatforms: text('enabledPlatforms'),
|
||
platformOrder: text('platformOrder'),
|
||
kugouApiBaseUrl: text('kugouApiBaseUrl'),
|
||
kugouAdminCookie: text('kugouAdminCookie'),
|
||
kugouAdminToken: text('kugouAdminToken'),
|
||
kugouAdminUser: jsonb('kugouAdminUser').$type<Record<string, any>>(),
|
||
kugouAdminBoundAt: timestamp('kugouAdminBoundAt'),
|
||
});
|
||
|
||
export const kuGouPlaylistSyncs = pgTable('KuGouPlaylistSync', {
|
||
id: serial('id').primaryKey(),
|
||
createdAt: timestamp('createdAt').defaultNow().notNull(),
|
||
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
|
||
playDate: timestamp('playDate').notNull(),
|
||
semesterId: integer('semesterId'),
|
||
playlistId: text('playlistId'),
|
||
playlistName: text('playlistName').notNull(),
|
||
playlistUrl: text('playlistUrl'),
|
||
totalSongs: integer('totalSongs').default(0).notNull(),
|
||
addedSongs: integer('addedSongs').default(0).notNull(),
|
||
missingSongs: jsonb('missingSongs').default('[]').$type<any[]>().notNull(),
|
||
status: text('status').default('pending').notNull(),
|
||
error: text('error'),
|
||
triggeredByUserId: integer('triggeredByUserId'),
|
||
rawResponse: jsonb('rawResponse').default('{}').$type<Record<string, any>>().notNull(),
|
||
}, (table) => [
|
||
index('kugou_playlist_sync_play_date_idx').on(table.playDate),
|
||
]);
|
||
|
||
// 歌曲黑名单表
|
||
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),
|
||
}));
|
||
|
||
/**
|
||
* 微信订阅消息的授权记账表。
|
||
*
|
||
* 为什么需要它:微信**不提供**「这个用户还剩几条订阅授权」的查询接口,
|
||
* 而「一次性订阅」是发一条扣一条。所以这里按 (userId, templateId) 记账:
|
||
* 用户在小程序里点「允许」时 grantedCount +1,服务端发送成功时 sentCount +1,
|
||
* 被微信拒绝(如 43101)时 blockedCount +1 并记下 errcode。
|
||
*
|
||
* ⚠️ 这里算出的额度是**我们自己数的近似值**:用户可能在别处授权、上报可能没发出去、
|
||
* 微信侧额度也可能过期,真值只有发一条收到 43101 才知道。
|
||
*
|
||
* **所以这张表只用于展示和排查,绝不参与「发不发」的决策** —— 早先的版本拿它做额度
|
||
* 预检(记账为 0 就跳过不发送),结果记账一旦和微信侧不同步,通知就被无声地全部丢掉。
|
||
* 现在服务端一律照常尝试发送,让微信回 errcode 来裁决。
|
||
*/
|
||
export const wechatSubscribeGrants = pgTable('WechatSubscribeGrant', {
|
||
id: serial('id').primaryKey(),
|
||
userId: integer('userId').notNull(),
|
||
templateId: text('templateId').notNull(),
|
||
/** 对应的模板键(如 SONG_SELECTED),模板被换掉后这里可能对不上,仅作展示用 */
|
||
kind: text('kind'),
|
||
grantedCount: integer('grantedCount').default(0).notNull(),
|
||
sentCount: integer('sentCount').default(0).notNull(),
|
||
blockedCount: integer('blockedCount').default(0).notNull(),
|
||
lastGrantedAt: timestamp('lastGrantedAt'),
|
||
lastSentAt: timestamp('lastSentAt'),
|
||
lastBlockedAt: timestamp('lastBlockedAt'),
|
||
lastErrorCode: integer('lastErrorCode'),
|
||
lastErrorMessage: text('lastErrorMessage'),
|
||
createdAt: timestamp('createdAt').defaultNow().notNull(),
|
||
updatedAt: timestamp('updatedAt').defaultNow().notNull(),
|
||
}, (t) => ({
|
||
unq: unique().on(t.userId, t.templateId),
|
||
}));
|
||
|
||
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;
|