1734 lines
110 KiB
JavaScript
1734 lines
110 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.closeFriends = exports.privacySettings = exports.userSocialLinks = exports.SOCIAL_PLATFORMS = exports.userGalleryImages = exports.userPresence = exports.videoRecommendations = exports.pokes = exports.userBlocks = exports.friendships = exports.FRIENDSHIP_STATUS = exports.adClicks = exports.adImpressions = exports.ads = exports.playlistViews = exports.featuredPlaylists = exports.playlistVideos = exports.playlists = exports.contentReports = exports.REPORT_STATUS = exports.REPORT_TYPES = exports.moderationWordLists = exports.WORD_FILTER_LEVELS = exports.rateLimits = exports.appSettings = exports.chatThreadReadStatus = exports.userDailyActivity = exports.highlightCooldowns = exports.videoReactions = exports.REACTION_TYPES = exports.userFinishes = exports.userStats = exports.userAchievements = exports.emailChangeTokens = exports.passwordResetTokens = exports.emailVerificationTokens = exports.commentModeration = exports.sessionBans = exports.authTokens = exports.mediaUsers = exports.v2Users = exports.views = exports.comments = exports.upvotes = exports.sessions = exports.publicMedia = exports.jobs = exports.compilations = exports.videos = exports.DIRECTORY_TYPES = void 0;
|
|
exports.PIPELINE_STEP_STATUS = exports.PIPELINE_STATUS = exports.performerDiscrepancies = exports.DISCREPANCY_RESOLUTIONS = exports.DISCREPANCY_STATUS = exports.DISCREPANCY_TYPES = exports.performerFaces = exports.FACE_STATUS = exports.creators = exports.tagGenerationJobs = exports.watchPartyInvites = exports.INVITE_STATUS = exports.watchPartyReactions = exports.watchPartyChatMessages = exports.watchPartyParticipants = exports.watchPartySessions = exports.LOOP_MODE = exports.WATCH_PARTY_STATUS = exports.digestClipTags = exports.digestSuggestedTags = exports.SUGGESTED_TAG_STATUS = exports.videoTags = exports.videoSegments = exports.videoTagTimeline = exports.VOCAL_CATEGORIES = exports.SEGMENT_TYPES = exports.videoSceneCuts = exports.digestGeneratedScenes = exports.digestOutputFolders = exports.digestCompilations = exports.CAPTION_SIZES = exports.CAPTION_POSITIONS = exports.digestGeneratedClips = exports.digestSelectedClips = exports.CLIP_SOURCES = exports.CLIP_STATUS = exports.CLIP_TYPES = exports.digestVideoTags = exports.videoDigests = exports.DIGEST_STATUS = exports.publicMediaPerformers = exports.userTagPreferences = exports.userUploadSuggestedTags = exports.publicMediaTags = exports.tags = exports.tagCategories = exports.uploadInvites = exports.UPLOAD_INVITE_STATUS = exports.userUploads = exports.USER_UPLOAD_STATUS = void 0;
|
|
exports.videoOcrResults = exports.OCR_TEXT_TYPES = exports.publishedInboxFiles = exports.INBOX_FILE_TYPES = exports.geoBlockingRules = exports.GEO_BLOCKING_MODES = exports.notificationPreferences = exports.notifications = exports.NOTIFICATION_TYPES = exports.paymentAuditLog = exports.payments = exports.invoices = exports.userSubscriptions = exports.subscriptionPlans = exports.pipelineTemplates = exports.resourceSnapshots = exports.pipelineStepEvents = exports.pipelineSteps = exports.pipelines = void 0;
|
|
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
// Directory type enum for efficient filtering (replaces LIKE patterns)
|
|
exports.DIRECTORY_TYPES = ['studios', 'gifs', 'private', 'inbox', 'curated', 'playback', 'compilations', 'videos', 'highlights'];
|
|
exports.videos = (0, pg_core_1.pgTable)('videos', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
path: (0, pg_core_1.text)('path').notNull().unique(),
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
producer: (0, pg_core_1.text)('producer'),
|
|
creator: (0, pg_core_1.text)('creator'),
|
|
title: (0, pg_core_1.text)('title'),
|
|
durationSeconds: (0, pg_core_1.integer)('duration_seconds'),
|
|
quality: (0, pg_core_1.text)('quality'),
|
|
orientation: (0, pg_core_1.text)('orientation'),
|
|
hasAudio: (0, pg_core_1.boolean)('has_audio').default(true),
|
|
fileSize: (0, pg_core_1.bigint)('file_size', { mode: 'number' }),
|
|
fileHash: (0, pg_core_1.text)('file_hash'),
|
|
width: (0, pg_core_1.integer)('width'),
|
|
height: (0, pg_core_1.integer)('height'),
|
|
lastValidated: (0, pg_core_1.timestamp)('last_validated', { withTimezone: true }),
|
|
isValid: (0, pg_core_1.boolean)('is_valid').default(true),
|
|
thumbnailPath: (0, pg_core_1.text)('thumbnail_path'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
// Directory type for efficient filtering (indexed)
|
|
directoryType: (0, pg_core_1.text)('directory_type').$type(),
|
|
// Historical engagement stats (preserved when moved from public media)
|
|
publicViewCount: (0, pg_core_1.integer)('public_view_count'),
|
|
publicUpvoteCount: (0, pg_core_1.integer)('public_upvote_count'),
|
|
publicCommentCount: (0, pg_core_1.integer)('public_comment_count'),
|
|
publicCompletionCount: (0, pg_core_1.integer)('public_completion_count'),
|
|
publicTotalWatchTime: (0, pg_core_1.integer)('public_total_watch_time'),
|
|
movedFromPublicAt: (0, pg_core_1.timestamp)('moved_from_public_at', { withTimezone: true }),
|
|
// Name standardization tracking
|
|
originalFilename: (0, pg_core_1.text)('original_filename'),
|
|
originalPath: (0, pg_core_1.text)('original_path'),
|
|
standardizedAt: (0, pg_core_1.timestamp)('standardized_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
orientationIdx: (0, pg_core_1.index)('idx_orientation').on(table.orientation),
|
|
producerIdx: (0, pg_core_1.index)('idx_producer').on(table.producer),
|
|
isValidIdx: (0, pg_core_1.index)('idx_is_valid').on(table.isValid),
|
|
directoryTypeIdx: (0, pg_core_1.index)('idx_directory_type').on(table.directoryType),
|
|
fingerprintIdx: (0, pg_core_1.index)('idx_videos_fingerprint').on(table.durationSeconds, table.fileSize, table.width, table.height),
|
|
// Composite index for common filtering pattern (directory + valid + orientation)
|
|
directoryValidOrientationIdx: (0, pg_core_1.index)('idx_videos_directory_valid_orientation').on(table.directoryType, table.isValid, table.orientation),
|
|
}));
|
|
exports.compilations = (0, pg_core_1.pgTable)('compilations', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
path: (0, pg_core_1.text)('path'),
|
|
durationSeconds: (0, pg_core_1.integer)('duration_seconds'),
|
|
videoIds: (0, pg_core_1.jsonb)('video_ids').$type(),
|
|
settings: (0, pg_core_1.jsonb)('settings').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
});
|
|
exports.jobs = (0, pg_core_1.pgTable)('jobs', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
type: (0, pg_core_1.text)('type').notNull(),
|
|
status: (0, pg_core_1.text)('status').default('pending').$type(),
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
log: (0, pg_core_1.text)('log'),
|
|
params: (0, pg_core_1.jsonb)('params').$type(),
|
|
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
// Queue management columns
|
|
resourceCategory: (0, pg_core_1.text)('resource_category').default('cpu').$type(),
|
|
vramRequired: (0, pg_core_1.integer)('vram_required').default(0),
|
|
queuePosition: (0, pg_core_1.integer)('queue_position'),
|
|
waitingReason: (0, pg_core_1.text)('waiting_reason'),
|
|
priority: (0, pg_core_1.integer)('priority').default(5),
|
|
// Pipeline integration (optional - for jobs created via pipeline)
|
|
pipelineId: (0, pg_core_1.integer)('pipeline_id'),
|
|
pipelineStepId: (0, pg_core_1.integer)('pipeline_step_id'),
|
|
}, (table) => ({
|
|
queueIdx: (0, pg_core_1.index)('idx_jobs_queue').on(table.status, table.priority, table.createdAt),
|
|
resourceIdx: (0, pg_core_1.index)('idx_jobs_resource').on(table.resourceCategory, table.status),
|
|
pipelineIdx: (0, pg_core_1.index)('idx_jobs_pipeline').on(table.pipelineId),
|
|
}));
|
|
// ============================================
|
|
// Public Gallery Tables
|
|
// ============================================
|
|
exports.publicMedia = (0, pg_core_1.pgTable)('public_media', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
path: (0, pg_core_1.text)('path').notNull().unique(),
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
category: (0, pg_core_1.text)('category').notNull(),
|
|
durationSeconds: (0, pg_core_1.integer)('duration_seconds'),
|
|
quality: (0, pg_core_1.text)('quality'),
|
|
orientation: (0, pg_core_1.text)('orientation'),
|
|
thumbnailPath: (0, pg_core_1.text)('thumbnail_path'),
|
|
fileSize: (0, pg_core_1.bigint)('file_size', { mode: 'number' }),
|
|
// Denormalized counters for performance
|
|
viewCount: (0, pg_core_1.integer)('view_count').default(0),
|
|
upvoteCount: (0, pg_core_1.integer)('upvote_count').default(0),
|
|
commentCount: (0, pg_core_1.integer)('comment_count').default(0),
|
|
finishCount: (0, pg_core_1.integer)('finish_count').default(0),
|
|
totalWatchTime: (0, pg_core_1.integer)('total_watch_time').default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
// Locked video system
|
|
isLocked: (0, pg_core_1.boolean)('is_locked').default(false),
|
|
lockedAt: (0, pg_core_1.timestamp)('locked_at', { withTimezone: true }),
|
|
lockedBy: (0, pg_core_1.integer)('locked_by'),
|
|
// Manual ordering for playlist (curated category)
|
|
position: (0, pg_core_1.integer)('position').default(0),
|
|
// Track uploader for quickies
|
|
uploaderId: (0, pg_core_1.integer)('uploader_id'),
|
|
}, (table) => ({
|
|
categoryIdx: (0, pg_core_1.index)('idx_public_media_category').on(table.category),
|
|
pathIdx: (0, pg_core_1.index)('idx_public_media_path').on(table.path),
|
|
isLockedIdx: (0, pg_core_1.index)('idx_public_media_is_locked').on(table.isLocked),
|
|
positionIdx: (0, pg_core_1.index)('idx_public_media_position').on(table.position),
|
|
// Quickies performance indexes
|
|
uploaderIdx: (0, pg_core_1.index)('idx_public_media_uploader').on(table.uploaderId),
|
|
categoryDateIdx: (0, pg_core_1.index)('idx_public_media_category_date').on(table.category, table.createdAt),
|
|
orientationIdx: (0, pg_core_1.index)('idx_public_media_orientation').on(table.orientation),
|
|
// Composite index for common filtering (category + locked + date)
|
|
categoryLockedDateIdx: (0, pg_core_1.index)('idx_public_media_category_locked_date').on(table.category, table.isLocked, table.createdAt),
|
|
}));
|
|
exports.sessions = (0, pg_core_1.pgTable)('sessions', {
|
|
id: (0, pg_core_1.text)('id').primaryKey(), // UUID from client
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
lastSeenAt: (0, pg_core_1.timestamp)('last_seen_at', { withTimezone: true }),
|
|
// Device tracking
|
|
ipAddress: (0, pg_core_1.text)('ip_address'),
|
|
userAgent: (0, pg_core_1.text)('user_agent'),
|
|
deviceType: (0, pg_core_1.text)('device_type'), // 'desktop' | 'mobile' | 'tablet'
|
|
browser: (0, pg_core_1.text)('browser'),
|
|
os: (0, pg_core_1.text)('os'),
|
|
// Geography (from GeoIP)
|
|
country: (0, pg_core_1.text)('country'), // ISO code: 'US'
|
|
countryName: (0, pg_core_1.text)('country_name'), // 'United States'
|
|
region: (0, pg_core_1.text)('region'), // 'CA'
|
|
city: (0, pg_core_1.text)('city'), // 'Los Angeles'
|
|
timezone: (0, pg_core_1.text)('timezone'), // 'America/Los_Angeles'
|
|
latitude: (0, pg_core_1.real)('latitude'),
|
|
longitude: (0, pg_core_1.real)('longitude'),
|
|
// User correlation
|
|
userId: (0, pg_core_1.integer)('user_id'), // Links session to authenticated user
|
|
// Analytics
|
|
firstSeenAt: (0, pg_core_1.timestamp)('first_seen_at', { withTimezone: true }), // For return visitor detection
|
|
visitCount: (0, pg_core_1.integer)('visit_count').default(1), // Increment on return visits
|
|
}, (table) => ({
|
|
userIdIdx: (0, pg_core_1.index)('idx_sessions_user_id').on(table.userId),
|
|
countryIdx: (0, pg_core_1.index)('idx_sessions_country').on(table.country),
|
|
}));
|
|
exports.upvotes = (0, pg_core_1.pgTable)('upvotes', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
sessionId: (0, pg_core_1.text)('session_id').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
uniqueVoteIdx: (0, pg_core_1.index)('idx_upvotes_unique').on(table.mediaId, table.sessionId),
|
|
mediaIdx: (0, pg_core_1.index)('idx_upvotes_media').on(table.mediaId),
|
|
}));
|
|
exports.comments = (0, pg_core_1.pgTable)('comments', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
sessionId: (0, pg_core_1.text)('session_id').notNull(),
|
|
userId: (0, pg_core_1.integer)('user_id'),
|
|
content: (0, pg_core_1.text)('content').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
// Content safety fields for LLM-based moderation
|
|
safetyStatus: (0, pg_core_1.text)('safety_status').default('pending'),
|
|
safetyCheckedAt: (0, pg_core_1.timestamp)('safety_checked_at', { withTimezone: true }),
|
|
safetyCategories: (0, pg_core_1.jsonb)('safety_categories').$type(),
|
|
safetyReasoning: (0, pg_core_1.text)('safety_reasoning'),
|
|
// Hidden comment fields for auto-moderation
|
|
isHidden: (0, pg_core_1.boolean)('is_hidden').default(false),
|
|
hiddenAt: (0, pg_core_1.timestamp)('hidden_at', { withTimezone: true }),
|
|
hiddenReason: (0, pg_core_1.text)('hidden_reason'),
|
|
// Admin moderation notes (internal, not shown to users)
|
|
moderationNotes: (0, pg_core_1.text)('moderation_notes'),
|
|
}, (table) => ({
|
|
mediaIdx: (0, pg_core_1.index)('idx_comments_media').on(table.mediaId),
|
|
sessionIdx: (0, pg_core_1.index)('idx_comments_session').on(table.sessionId),
|
|
userIdx: (0, pg_core_1.index)('idx_comments_user').on(table.userId),
|
|
safetyStatusIdx: (0, pg_core_1.index)('idx_comments_safety_status').on(table.safetyStatus),
|
|
isHiddenIdx: (0, pg_core_1.index)('idx_comments_is_hidden').on(table.isHidden),
|
|
}));
|
|
exports.views = (0, pg_core_1.pgTable)('views', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
sessionId: (0, pg_core_1.text)('session_id').notNull(),
|
|
watchTimeSeconds: (0, pg_core_1.integer)('watch_time_seconds').default(0),
|
|
lastUpdated: (0, pg_core_1.timestamp)('last_updated', { withTimezone: true }).defaultNow(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
uniqueViewIdx: (0, pg_core_1.index)('idx_views_unique').on(table.mediaId, table.sessionId),
|
|
mediaIdx: (0, pg_core_1.index)('idx_views_media').on(table.mediaId),
|
|
}));
|
|
// ============================================
|
|
// User Management Tables
|
|
// ============================================
|
|
// V2 Users table (for authentication only)
|
|
// This matches the Prisma User model to allow Drizzle queries for auth
|
|
// Table name: 'users' (created by Prisma migrations)
|
|
exports.v2Users = (0, pg_core_1.pgTable)('users', {
|
|
id: (0, pg_core_1.text)('id').primaryKey(),
|
|
email: (0, pg_core_1.text)('email').notNull().unique(),
|
|
password: (0, pg_core_1.text)('password').notNull(),
|
|
name: (0, pg_core_1.text)('name'),
|
|
phone: (0, pg_core_1.text)('phone'),
|
|
role: (0, pg_core_1.text)('role').notNull().default('USER'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('ACTIVE'),
|
|
permissions: (0, pg_core_1.jsonb)('permissions'),
|
|
createdVia: (0, pg_core_1.text)('createdVia').notNull().default('STANDARD'),
|
|
expiresAt: (0, pg_core_1.timestamp)('expiresAt', { withTimezone: true }),
|
|
expireDays: (0, pg_core_1.integer)('expireDays'),
|
|
lastLoginAt: (0, pg_core_1.timestamp)('lastLoginAt', { withTimezone: true }),
|
|
emailVerified: (0, pg_core_1.boolean)('emailVerified').notNull().default(false),
|
|
createdAt: (0, pg_core_1.timestamp)('createdAt', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updatedAt', { withTimezone: true }).notNull().defaultNow(),
|
|
}, (table) => ({
|
|
emailIdx: (0, pg_core_1.index)('User_email_key').on(table.email),
|
|
}));
|
|
// Media-specific users table (for future media-api user management)
|
|
// NOTE: This is separate from v2Users and not currently used
|
|
// If/when media-api needs its own user system, this table can be created
|
|
exports.mediaUsers = (0, pg_core_1.pgTable)('media_users', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
username: (0, pg_core_1.text)('username').notNull().unique(),
|
|
email: (0, pg_core_1.text)('email').notNull().unique(),
|
|
passwordHash: (0, pg_core_1.text)('password_hash').notNull(),
|
|
role: (0, pg_core_1.text)('role').notNull().default('user'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
emailVerified: (0, pg_core_1.boolean)('email_verified').default(false),
|
|
linkedSessionId: (0, pg_core_1.text)('linked_session_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
approvedAt: (0, pg_core_1.timestamp)('approved_at', { withTimezone: true }),
|
|
approvedBy: (0, pg_core_1.integer)('approved_by'),
|
|
lastLoginAt: (0, pg_core_1.timestamp)('last_login_at', { withTimezone: true }),
|
|
// Trusted user fields
|
|
isTrusted: (0, pg_core_1.boolean)('is_trusted').default(false),
|
|
maxUploadDurationSeconds: (0, pg_core_1.integer)('max_upload_duration_seconds').default(60),
|
|
autoAcceptUploads: (0, pg_core_1.boolean)('auto_accept_uploads').default(false),
|
|
trustedAt: (0, pg_core_1.timestamp)('trusted_at', { withTimezone: true }),
|
|
trustedBy: (0, pg_core_1.integer)('trusted_by'),
|
|
defaultPlaylistId: (0, pg_core_1.integer)('default_playlist_id'),
|
|
// Social links visibility
|
|
socialLinksPublic: (0, pg_core_1.boolean)('social_links_public').default(false),
|
|
// Subscription fields
|
|
subscriptionStatus: (0, pg_core_1.text)('subscription_status').default('none'), // none, active, grace_period, delinquent, lifetime
|
|
subscriptionPlanId: (0, pg_core_1.integer)('subscription_plan_id'),
|
|
subscriptionEndDate: (0, pg_core_1.timestamp)('subscription_end_date', { withTimezone: true }),
|
|
lifetimeMember: (0, pg_core_1.boolean)('lifetime_member').default(false),
|
|
totalPaidCAD: (0, pg_core_1.integer)('total_paid_cad').default(0), // Lifetime total in cents
|
|
// Moderation tracking
|
|
lastModerationAction: (0, pg_core_1.timestamp)('last_moderation_action', { withTimezone: true }),
|
|
}, (table) => ({
|
|
emailIdx: (0, pg_core_1.index)('idx_media_users_email').on(table.email),
|
|
usernameIdx: (0, pg_core_1.index)('idx_media_users_username').on(table.username),
|
|
statusIdx: (0, pg_core_1.index)('idx_media_users_status').on(table.status),
|
|
trustedIdx: (0, pg_core_1.index)('idx_media_users_trusted').on(table.isTrusted),
|
|
subscriptionStatusIdx: (0, pg_core_1.index)('idx_media_users_subscription_status').on(table.subscriptionStatus),
|
|
}));
|
|
exports.authTokens = (0, pg_core_1.pgTable)('auth_tokens', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
token: (0, pg_core_1.text)('token').notNull().unique(),
|
|
type: (0, pg_core_1.text)('type').notNull(),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
tokenIdx: (0, pg_core_1.index)('idx_auth_tokens_token').on(table.token),
|
|
userIdx: (0, pg_core_1.index)('idx_auth_tokens_user').on(table.userId),
|
|
}));
|
|
exports.sessionBans = (0, pg_core_1.pgTable)('session_bans', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
sessionId: (0, pg_core_1.text)('session_id').notNull(),
|
|
reason: (0, pg_core_1.text)('reason'),
|
|
bannedBy: (0, pg_core_1.integer)('banned_by'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }), // null = permanent
|
|
}, (table) => ({
|
|
sessionIdx: (0, pg_core_1.index)('idx_session_bans_session').on(table.sessionId),
|
|
}));
|
|
exports.commentModeration = (0, pg_core_1.pgTable)('comment_moderation', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
commentId: (0, pg_core_1.integer)('comment_id').notNull(),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
moderatedBy: (0, pg_core_1.integer)('moderated_by'),
|
|
moderatedAt: (0, pg_core_1.timestamp)('moderated_at', { withTimezone: true }),
|
|
reason: (0, pg_core_1.text)('reason'),
|
|
}, (table) => ({
|
|
commentIdx: (0, pg_core_1.index)('idx_comment_moderation_comment').on(table.commentId),
|
|
statusIdx: (0, pg_core_1.index)('idx_comment_moderation_status').on(table.status),
|
|
}));
|
|
// Email Verification Tokens
|
|
exports.emailVerificationTokens = (0, pg_core_1.pgTable)('email_verification_tokens', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
token: (0, pg_core_1.text)('token').notNull().unique(),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
tokenIdx: (0, pg_core_1.index)('idx_email_verification_tokens_token').on(table.token),
|
|
userIdx: (0, pg_core_1.index)('idx_email_verification_tokens_user').on(table.userId),
|
|
}));
|
|
// Password Reset Tokens
|
|
exports.passwordResetTokens = (0, pg_core_1.pgTable)('password_reset_tokens', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
token: (0, pg_core_1.text)('token').notNull().unique(),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
usedAt: (0, pg_core_1.timestamp)('used_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
tokenIdx: (0, pg_core_1.index)('idx_password_reset_tokens_token').on(table.token),
|
|
userIdx: (0, pg_core_1.index)('idx_password_reset_tokens_user').on(table.userId),
|
|
}));
|
|
// Email Change Tokens
|
|
exports.emailChangeTokens = (0, pg_core_1.pgTable)('email_change_tokens', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
newEmail: (0, pg_core_1.text)('new_email').notNull(),
|
|
token: (0, pg_core_1.text)('token').notNull().unique(),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
tokenIdx: (0, pg_core_1.index)('idx_email_change_tokens_token').on(table.token),
|
|
userIdx: (0, pg_core_1.index)('idx_email_change_tokens_user').on(table.userId),
|
|
}));
|
|
// ============================================
|
|
// Achievement & Dashboard Tables
|
|
// ============================================
|
|
exports.userAchievements = (0, pg_core_1.pgTable)('user_achievements', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
achievementId: (0, pg_core_1.text)('achievement_id').notNull(),
|
|
unlockedAt: (0, pg_core_1.timestamp)('unlocked_at', { withTimezone: true }).notNull(),
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
notified: (0, pg_core_1.boolean)('notified').default(false),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_achievements_user').on(table.userId),
|
|
// Unique constraint for ON CONFLICT support in batch inserts
|
|
userAchievementUnique: (0, pg_core_1.uniqueIndex)('idx_user_achievements_unique').on(table.userId, table.achievementId),
|
|
}));
|
|
exports.userStats = (0, pg_core_1.pgTable)('user_stats', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().unique(),
|
|
totalWatchTimeSeconds: (0, pg_core_1.integer)('total_watch_time_seconds').default(0),
|
|
totalVideosWatched: (0, pg_core_1.integer)('total_videos_watched').default(0),
|
|
totalUpvotesGiven: (0, pg_core_1.integer)('total_upvotes_given').default(0),
|
|
totalCommentsMade: (0, pg_core_1.integer)('total_comments_made').default(0),
|
|
totalFinishes: (0, pg_core_1.integer)('total_finishes').default(0),
|
|
currentDayStreak: (0, pg_core_1.integer)('current_day_streak').default(0),
|
|
longestDayStreak: (0, pg_core_1.integer)('longest_day_streak').default(0),
|
|
lastActiveDate: (0, pg_core_1.text)('last_active_date'), // YYYY-MM-DD format
|
|
longestSingleSession: (0, pg_core_1.integer)('longest_single_session').default(0),
|
|
categoriesCompleted: (0, pg_core_1.jsonb)('categories_completed').$type(),
|
|
nightOwlCount: (0, pg_core_1.integer)('night_owl_count').default(0),
|
|
earlyBirdCount: (0, pg_core_1.integer)('early_bird_count').default(0),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
});
|
|
exports.userFinishes = (0, pg_core_1.pgTable)('user_finishes', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
mediaId: (0, pg_core_1.integer)('media_id'),
|
|
sessionId: (0, pg_core_1.text)('session_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_finishes_user').on(table.userId),
|
|
dateIdx: (0, pg_core_1.index)('idx_user_finishes_date').on(table.createdAt),
|
|
}));
|
|
// ============================================
|
|
// Video Reactions Table
|
|
// ============================================
|
|
exports.REACTION_TYPES = ['like', 'love', 'laugh', 'wow', 'sad', 'angry'];
|
|
exports.videoReactions = (0, pg_core_1.pgTable)('video_reactions', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
reactionType: (0, pg_core_1.text)('reaction_type').notNull(),
|
|
videoTimestamp: (0, pg_core_1.integer)('video_timestamp').notNull(), // seconds into the video when reaction was clicked
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull(),
|
|
}, (table) => ({
|
|
userMediaTypeIdx: (0, pg_core_1.index)('idx_video_reactions_user_media_type').on(table.userId, table.mediaId, table.reactionType),
|
|
mediaTimestampIdx: (0, pg_core_1.index)('idx_video_reactions_media_timestamp').on(table.mediaId, table.videoTimestamp),
|
|
mediaIdx: (0, pg_core_1.index)('idx_video_reactions_media').on(table.mediaId),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_video_reactions_created').on(table.createdAt),
|
|
}));
|
|
// ============================================
|
|
// Quickie Generation Cooldowns Table
|
|
// ============================================
|
|
exports.highlightCooldowns = (0, pg_core_1.pgTable)('highlight_cooldowns', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
lastGeneratedAt: (0, pg_core_1.timestamp)('last_generated_at', { withTimezone: true }).notNull(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_highlight_cooldowns_user').on(table.userId),
|
|
}));
|
|
exports.userDailyActivity = (0, pg_core_1.pgTable)('user_daily_activity', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
activityDate: (0, pg_core_1.text)('activity_date').notNull(), // YYYY-MM-DD
|
|
watchTimeSeconds: (0, pg_core_1.integer)('watch_time_seconds').default(0),
|
|
videosWatched: (0, pg_core_1.integer)('videos_watched').default(0),
|
|
firstActivityHour: (0, pg_core_1.integer)('first_activity_hour'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
uniqueIdx: (0, pg_core_1.index)('idx_user_daily_activity_unique').on(table.userId, table.activityDate),
|
|
}));
|
|
// ============================================
|
|
// Chat Thread Tracking Table
|
|
// ============================================
|
|
exports.chatThreadReadStatus = (0, pg_core_1.pgTable)('chat_thread_read_status', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
lastSeenAt: (0, pg_core_1.timestamp)('last_seen_at', { withTimezone: true }).notNull(),
|
|
}, (table) => ({
|
|
uniqueIdx: (0, pg_core_1.index)('idx_chat_thread_read_unique').on(table.userId, table.mediaId),
|
|
userIdx: (0, pg_core_1.index)('idx_chat_thread_read_user').on(table.userId),
|
|
}));
|
|
// ============================================
|
|
// App Settings Table
|
|
// ============================================
|
|
exports.appSettings = (0, pg_core_1.pgTable)('app_settings', {
|
|
key: (0, pg_core_1.text)('key').primaryKey(),
|
|
value: (0, pg_core_1.text)('value').notNull(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).defaultNow(),
|
|
});
|
|
// ============================================
|
|
// Rate Limiting Table (for persistent rate limits)
|
|
// ============================================
|
|
exports.rateLimits = (0, pg_core_1.pgTable)('rate_limits', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
key: (0, pg_core_1.text)('key').notNull().unique(),
|
|
count: (0, pg_core_1.integer)('count').notNull().default(1),
|
|
resetAt: (0, pg_core_1.timestamp)('reset_at', { withTimezone: true }).notNull(),
|
|
}, (table) => ({
|
|
keyIdx: (0, pg_core_1.uniqueIndex)('idx_rate_limits_key').on(table.key),
|
|
resetAtIdx: (0, pg_core_1.index)('idx_rate_limits_reset_at').on(table.resetAt),
|
|
}));
|
|
// ============================================
|
|
// Moderation Word Lists Table
|
|
// ============================================
|
|
exports.WORD_FILTER_LEVELS = ['low', 'medium', 'high', 'custom'];
|
|
exports.moderationWordLists = (0, pg_core_1.pgTable)('moderation_word_lists', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
level: (0, pg_core_1.text)('level').notNull(),
|
|
word: (0, pg_core_1.text)('word').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
createdBy: (0, pg_core_1.integer)('created_by'),
|
|
}, (table) => ({
|
|
levelIdx: (0, pg_core_1.index)('idx_moderation_word_lists_level').on(table.level),
|
|
wordIdx: (0, pg_core_1.index)('idx_moderation_word_lists_word').on(table.word),
|
|
}));
|
|
// ============================================
|
|
// Content Reports Table
|
|
// ============================================
|
|
exports.REPORT_TYPES = ['inappropriate', 'spam', 'copyright', 'illegal', 'false_info', 'other'];
|
|
exports.REPORT_STATUS = ['pending', 'reviewed', 'actioned', 'dismissed'];
|
|
exports.contentReports = (0, pg_core_1.pgTable)('content_reports', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
sessionId: (0, pg_core_1.text)('session_id'),
|
|
userId: (0, pg_core_1.integer)('user_id'),
|
|
reportType: (0, pg_core_1.text)('report_type').notNull(),
|
|
description: (0, pg_core_1.text)('description'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
resolvedBy: (0, pg_core_1.integer)('resolved_by'),
|
|
resolvedAt: (0, pg_core_1.timestamp)('resolved_at', { withTimezone: true }),
|
|
resolutionNotes: (0, pg_core_1.text)('resolution_notes'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
mediaIdx: (0, pg_core_1.index)('idx_content_reports_media').on(table.mediaId),
|
|
statusIdx: (0, pg_core_1.index)('idx_content_reports_status').on(table.status),
|
|
sessionIdx: (0, pg_core_1.index)('idx_content_reports_session').on(table.sessionId),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_content_reports_created').on(table.createdAt),
|
|
}));
|
|
// ============================================
|
|
// Playlist Tables
|
|
// ============================================
|
|
exports.playlists = (0, pg_core_1.pgTable)('playlists', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
description: (0, pg_core_1.text)('description'),
|
|
isPublic: (0, pg_core_1.boolean)('is_public').default(false),
|
|
shareToken: (0, pg_core_1.text)('share_token').unique(),
|
|
thumbnailMediaId: (0, pg_core_1.integer)('thumbnail_media_id'),
|
|
// Denormalized counters
|
|
videoCount: (0, pg_core_1.integer)('video_count').default(0),
|
|
totalDurationSeconds: (0, pg_core_1.integer)('total_duration_seconds').default(0),
|
|
viewCount: (0, pg_core_1.integer)('view_count').default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_playlists_user').on(table.userId),
|
|
publicIdx: (0, pg_core_1.index)('idx_playlists_public').on(table.isPublic),
|
|
shareTokenIdx: (0, pg_core_1.index)('idx_playlists_share_token').on(table.shareToken),
|
|
// Prevent duplicate playlist names per user (e.g., multiple "My Quickies")
|
|
userNameIdx: (0, pg_core_1.uniqueIndex)('idx_playlists_user_name').on(table.userId, table.name),
|
|
}));
|
|
exports.playlistVideos = (0, pg_core_1.pgTable)('playlist_videos', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
playlistId: (0, pg_core_1.integer)('playlist_id').notNull(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
position: (0, pg_core_1.integer)('position').notNull().default(0),
|
|
addedAt: (0, pg_core_1.timestamp)('added_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
playlistIdx: (0, pg_core_1.index)('idx_playlist_videos_playlist').on(table.playlistId),
|
|
mediaIdx: (0, pg_core_1.index)('idx_playlist_videos_media').on(table.mediaId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_playlist_videos_unique').on(table.playlistId, table.mediaId),
|
|
}));
|
|
exports.featuredPlaylists = (0, pg_core_1.pgTable)('featured_playlists', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
playlistId: (0, pg_core_1.integer)('playlist_id').notNull().unique(),
|
|
position: (0, pg_core_1.integer)('position').notNull().default(0),
|
|
featuredBy: (0, pg_core_1.integer)('featured_by'),
|
|
featuredAt: (0, pg_core_1.timestamp)('featured_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
positionIdx: (0, pg_core_1.index)('idx_featured_playlists_position').on(table.position),
|
|
}));
|
|
exports.playlistViews = (0, pg_core_1.pgTable)('playlist_views', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
playlistId: (0, pg_core_1.integer)('playlist_id').notNull(),
|
|
sessionId: (0, pg_core_1.text)('session_id').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
playlistIdx: (0, pg_core_1.index)('idx_playlist_views_playlist').on(table.playlistId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_playlist_views_unique').on(table.playlistId, table.sessionId),
|
|
}));
|
|
// ============================================
|
|
// Advertisement Tables
|
|
// ============================================
|
|
exports.ads = (0, pg_core_1.pgTable)('ads', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
type: (0, pg_core_1.text)('type').notNull(),
|
|
// For make_account: variant identifier (e.g., 'playlist_promo', 'chat_promo')
|
|
// For custom: null (uses imagePath instead)
|
|
variant: (0, pg_core_1.text)('variant'),
|
|
// Custom ad fields
|
|
imagePath: (0, pg_core_1.text)('image_path'),
|
|
linkUrl: (0, pg_core_1.text)('link_url'),
|
|
title: (0, pg_core_1.text)('title'), // For alt text and admin reference
|
|
// Visibility: 'everyone', 'members_only', 'non_members'
|
|
visibility: (0, pg_core_1.text)('visibility').notNull().default('everyone'),
|
|
// Status
|
|
isActive: (0, pg_core_1.boolean)('is_active').default(true),
|
|
// Ordering for rotation
|
|
position: (0, pg_core_1.integer)('position').default(0),
|
|
// Denormalized counters for performance
|
|
impressionCount: (0, pg_core_1.integer)('impression_count').default(0),
|
|
clickCount: (0, pg_core_1.integer)('click_count').default(0),
|
|
// Timestamps
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
typeIdx: (0, pg_core_1.index)('idx_ads_type').on(table.type),
|
|
isActiveIdx: (0, pg_core_1.index)('idx_ads_is_active').on(table.isActive),
|
|
visibilityIdx: (0, pg_core_1.index)('idx_ads_visibility').on(table.visibility),
|
|
}));
|
|
exports.adImpressions = (0, pg_core_1.pgTable)('ad_impressions', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
adId: (0, pg_core_1.integer)('ad_id').notNull().references(() => exports.ads.id, { onDelete: 'cascade' }),
|
|
sessionId: (0, pg_core_1.text)('session_id'),
|
|
userId: (0, pg_core_1.integer)('user_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
adIdx: (0, pg_core_1.index)('idx_ad_impressions_ad').on(table.adId),
|
|
sessionIdx: (0, pg_core_1.index)('idx_ad_impressions_session').on(table.sessionId),
|
|
dateIdx: (0, pg_core_1.index)('idx_ad_impressions_date').on(table.createdAt),
|
|
}));
|
|
exports.adClicks = (0, pg_core_1.pgTable)('ad_clicks', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
adId: (0, pg_core_1.integer)('ad_id').notNull().references(() => exports.ads.id, { onDelete: 'cascade' }),
|
|
sessionId: (0, pg_core_1.text)('session_id'),
|
|
userId: (0, pg_core_1.integer)('user_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
adIdx: (0, pg_core_1.index)('idx_ad_clicks_ad').on(table.adId),
|
|
sessionIdx: (0, pg_core_1.index)('idx_ad_clicks_session').on(table.sessionId),
|
|
dateIdx: (0, pg_core_1.index)('idx_ad_clicks_date').on(table.createdAt),
|
|
}));
|
|
// ============================================
|
|
// Friends & Social Tables
|
|
// ============================================
|
|
exports.FRIENDSHIP_STATUS = ['pending', 'accepted', 'declined'];
|
|
exports.friendships = (0, pg_core_1.pgTable)('friendships', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
friendId: (0, pg_core_1.integer)('friend_id').notNull(),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
acceptedAt: (0, pg_core_1.timestamp)('accepted_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userFriendIdx: (0, pg_core_1.index)('idx_friendships_user_friend').on(table.userId, table.friendId),
|
|
userIdx: (0, pg_core_1.index)('idx_friendships_user').on(table.userId),
|
|
friendIdx: (0, pg_core_1.index)('idx_friendships_friend').on(table.friendId),
|
|
statusIdx: (0, pg_core_1.index)('idx_friendships_status').on(table.status),
|
|
}));
|
|
exports.userBlocks = (0, pg_core_1.pgTable)('user_blocks', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
blockedUserId: (0, pg_core_1.integer)('blocked_user_id').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
uniqueBlockIdx: (0, pg_core_1.index)('idx_user_blocks_unique').on(table.userId, table.blockedUserId),
|
|
userIdx: (0, pg_core_1.index)('idx_user_blocks_user').on(table.userId),
|
|
}));
|
|
exports.pokes = (0, pg_core_1.pgTable)('pokes', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
fromUserId: (0, pg_core_1.integer)('from_user_id').notNull(),
|
|
toUserId: (0, pg_core_1.integer)('to_user_id').notNull(),
|
|
isRead: (0, pg_core_1.boolean)('is_read').default(false),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
toUserIdx: (0, pg_core_1.index)('idx_pokes_to_user').on(table.toUserId),
|
|
fromUserIdx: (0, pg_core_1.index)('idx_pokes_from_user').on(table.fromUserId),
|
|
}));
|
|
// Video recommendations - share videos with friends
|
|
exports.videoRecommendations = (0, pg_core_1.pgTable)('video_recommendations', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
fromUserId: (0, pg_core_1.integer)('from_user_id').notNull(),
|
|
toUserId: (0, pg_core_1.integer)('to_user_id').notNull(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
message: (0, pg_core_1.text)('message'),
|
|
isRead: (0, pg_core_1.boolean)('is_read').default(false),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
toUserIdx: (0, pg_core_1.index)('idx_video_recommendations_to_user').on(table.toUserId),
|
|
fromUserIdx: (0, pg_core_1.index)('idx_video_recommendations_from_user').on(table.fromUserId),
|
|
mediaIdx: (0, pg_core_1.index)('idx_video_recommendations_media').on(table.mediaId),
|
|
}));
|
|
exports.userPresence = (0, pg_core_1.pgTable)('user_presence', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().unique(),
|
|
isOnline: (0, pg_core_1.boolean)('is_online').default(false),
|
|
currentMediaId: (0, pg_core_1.integer)('current_media_id'),
|
|
lastActivityAt: (0, pg_core_1.timestamp)('last_activity_at', { withTimezone: true }),
|
|
lastVideoChangeAt: (0, pg_core_1.timestamp)('last_video_change_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
onlineIdx: (0, pg_core_1.index)('idx_user_presence_online').on(table.isOnline),
|
|
userIdx: (0, pg_core_1.index)('idx_user_presence_user').on(table.userId),
|
|
}));
|
|
exports.userGalleryImages = (0, pg_core_1.pgTable)('user_gallery_images', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
originalFilename: (0, pg_core_1.text)('original_filename'),
|
|
position: (0, pg_core_1.integer)('position').notNull().default(0),
|
|
uploadedAt: (0, pg_core_1.timestamp)('uploaded_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_gallery_user').on(table.userId),
|
|
positionIdx: (0, pg_core_1.index)('idx_user_gallery_position').on(table.userId, table.position),
|
|
}));
|
|
// Social link platforms
|
|
exports.SOCIAL_PLATFORMS = [
|
|
'twitter', 'instagram', 'onlyfans', 'fansly', 'reddit',
|
|
'discord', 'tiktok', 'youtube', 'twitch', 'snapchat',
|
|
'linktree', 'custom'
|
|
];
|
|
exports.userSocialLinks = (0, pg_core_1.pgTable)('user_social_links', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
platform: (0, pg_core_1.text)('platform').notNull(), // One of SOCIAL_PLATFORMS
|
|
url: (0, pg_core_1.text)('url').notNull(),
|
|
displayName: (0, pg_core_1.text)('display_name'), // Optional custom label
|
|
position: (0, pg_core_1.integer)('position').notNull().default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_social_links_user').on(table.userId),
|
|
positionIdx: (0, pg_core_1.index)('idx_user_social_links_position').on(table.userId, table.position),
|
|
}));
|
|
// ============================================
|
|
// Privacy Settings Tables
|
|
// ============================================
|
|
exports.privacySettings = (0, pg_core_1.pgTable)('privacy_settings', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().unique().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
// Online Status
|
|
showOnlineStatus: (0, pg_core_1.boolean)('show_online_status').default(true),
|
|
showCurrentlyWatching: (0, pg_core_1.boolean)('show_currently_watching').default(true),
|
|
// Activity Visibility
|
|
showInFriendActivity: (0, pg_core_1.boolean)('show_in_friend_activity').default(true),
|
|
anonymizePublicComments: (0, pg_core_1.boolean)('anonymize_public_comments').default(false),
|
|
hidePublicReactions: (0, pg_core_1.boolean)('hide_public_reactions').default(false),
|
|
hidePublicFinishes: (0, pg_core_1.boolean)('hide_public_finishes').default(false),
|
|
// Friend Requests
|
|
allowFriendRequests: (0, pg_core_1.boolean)('allow_friend_requests').default(true),
|
|
// Close Friends restrictions
|
|
closeFriendsOnlyWatching: (0, pg_core_1.boolean)('close_friends_only_watching').default(false),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_privacy_settings_user').on(table.userId),
|
|
}));
|
|
exports.closeFriends = (0, pg_core_1.pgTable)('close_friends', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
closeFriendId: (0, pg_core_1.integer)('close_friend_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
addedAt: (0, pg_core_1.timestamp)('added_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
uniqueIdx: (0, pg_core_1.uniqueIndex)('idx_close_friends_unique').on(table.userId, table.closeFriendId),
|
|
userIdx: (0, pg_core_1.index)('idx_close_friends_user').on(table.userId),
|
|
}));
|
|
// ============================================
|
|
// User Uploads (Trusted User Feature)
|
|
// ============================================
|
|
exports.USER_UPLOAD_STATUS = ['pending', 'approved', 'rejected'];
|
|
exports.userUploads = (0, pg_core_1.pgTable)('user_uploads', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
originalFilename: (0, pg_core_1.text)('original_filename'),
|
|
path: (0, pg_core_1.text)('path').notNull(),
|
|
durationSeconds: (0, pg_core_1.integer)('duration_seconds'),
|
|
quality: (0, pg_core_1.text)('quality'),
|
|
orientation: (0, pg_core_1.text)('orientation'),
|
|
fileSize: (0, pg_core_1.bigint)('file_size', { mode: 'number' }),
|
|
thumbnailPath: (0, pg_core_1.text)('thumbnail_path'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
reviewedBy: (0, pg_core_1.integer)('reviewed_by'),
|
|
reviewedAt: (0, pg_core_1.timestamp)('reviewed_at', { withTimezone: true }),
|
|
reviewNotes: (0, pg_core_1.text)('review_notes'),
|
|
publicMediaId: (0, pg_core_1.integer)('public_media_id'),
|
|
uploadInviteId: (0, pg_core_1.integer)('upload_invite_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_uploads_user').on(table.userId),
|
|
statusIdx: (0, pg_core_1.index)('idx_user_uploads_status').on(table.status),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_user_uploads_created').on(table.createdAt),
|
|
inviteIdx: (0, pg_core_1.index)('idx_user_uploads_invite').on(table.uploadInviteId),
|
|
}));
|
|
// ============================================
|
|
// Upload Invites (Shareable Upload Links)
|
|
// ============================================
|
|
exports.UPLOAD_INVITE_STATUS = ['active', 'inactive', 'expired'];
|
|
exports.uploadInvites = (0, pg_core_1.pgTable)('upload_invites', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
code: (0, pg_core_1.text)('code').notNull().unique(),
|
|
label: (0, pg_core_1.text)('label'),
|
|
createdBy: (0, pg_core_1.integer)('created_by').notNull(),
|
|
status: (0, pg_core_1.text)('status').notNull().default('active'),
|
|
maxUploads: (0, pg_core_1.integer)('max_uploads'),
|
|
uploadCount: (0, pg_core_1.integer)('upload_count').notNull().default(0),
|
|
expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
codeIdx: (0, pg_core_1.index)('idx_upload_invites_code').on(table.code),
|
|
statusIdx: (0, pg_core_1.index)('idx_upload_invites_status').on(table.status),
|
|
createdByIdx: (0, pg_core_1.index)('idx_upload_invites_created_by').on(table.createdBy),
|
|
}));
|
|
// ============================================
|
|
// Tag System Tables
|
|
// ============================================
|
|
exports.tagCategories = (0, pg_core_1.pgTable)('tag_categories', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
name: (0, pg_core_1.text)('name').notNull().unique(),
|
|
displayOrder: (0, pg_core_1.integer)('display_order').notNull().default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
displayOrderIdx: (0, pg_core_1.index)('idx_tag_categories_display_order').on(table.displayOrder),
|
|
}));
|
|
exports.tags = (0, pg_core_1.pgTable)('tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
categoryId: (0, pg_core_1.integer)('category_id').notNull(),
|
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
displayOrder: (0, pg_core_1.integer)('display_order').notNull().default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
categoryIdx: (0, pg_core_1.index)('idx_tags_category').on(table.categoryId),
|
|
displayOrderIdx: (0, pg_core_1.index)('idx_tags_display_order').on(table.displayOrder),
|
|
uniqueNamePerCategory: (0, pg_core_1.index)('idx_tags_unique_name').on(table.categoryId, table.name),
|
|
}));
|
|
exports.publicMediaTags = (0, pg_core_1.pgTable)('public_media_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
tagId: (0, pg_core_1.integer)('tag_id').notNull(),
|
|
addedAt: (0, pg_core_1.timestamp)('added_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
mediaIdx: (0, pg_core_1.index)('idx_public_media_tags_media').on(table.mediaId),
|
|
tagIdx: (0, pg_core_1.index)('idx_public_media_tags_tag').on(table.tagId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_public_media_tags_unique').on(table.mediaId, table.tagId),
|
|
}));
|
|
exports.userUploadSuggestedTags = (0, pg_core_1.pgTable)('user_upload_suggested_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
uploadId: (0, pg_core_1.integer)('upload_id').notNull(),
|
|
tagId: (0, pg_core_1.integer)('tag_id').notNull(),
|
|
suggestedAt: (0, pg_core_1.timestamp)('suggested_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
uploadIdx: (0, pg_core_1.index)('idx_user_upload_suggested_tags_upload').on(table.uploadId),
|
|
tagIdx: (0, pg_core_1.index)('idx_user_upload_suggested_tags_tag').on(table.tagId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_user_upload_suggested_tags_unique').on(table.uploadId, table.tagId),
|
|
}));
|
|
exports.userTagPreferences = (0, pg_core_1.pgTable)('user_tag_preferences', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull(),
|
|
tagId: (0, pg_core_1.integer)('tag_id').notNull(),
|
|
savedAt: (0, pg_core_1.timestamp)('saved_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_user_tag_preferences_user').on(table.userId),
|
|
tagIdx: (0, pg_core_1.index)('idx_user_tag_preferences_tag').on(table.tagId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_user_tag_preferences_unique').on(table.userId, table.tagId),
|
|
}));
|
|
// ============================================
|
|
// Public Media Performers Junction
|
|
// ============================================
|
|
/**
|
|
* Links public media items to performers for "Browse Actors" feature.
|
|
*/
|
|
exports.publicMediaPerformers = (0, pg_core_1.pgTable)('public_media_performers', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull(),
|
|
performerId: (0, pg_core_1.integer)('performer_id').notNull(),
|
|
addedAt: (0, pg_core_1.timestamp)('added_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
mediaIdx: (0, pg_core_1.index)('idx_public_media_performers_media').on(table.mediaId),
|
|
performerIdx: (0, pg_core_1.index)('idx_public_media_performers_performer').on(table.performerId),
|
|
uniqueIdx: (0, pg_core_1.uniqueIndex)('idx_public_media_performers_unique').on(table.mediaId, table.performerId),
|
|
}));
|
|
// ============================================
|
|
// Video Digest Tables
|
|
// ============================================
|
|
exports.DIGEST_STATUS = [
|
|
'pending', 'scene_detection', 'extracting', 'analyzing',
|
|
'face_detection', 'transcribing', 'segmenting', 'synthesizing',
|
|
'completed', 'failed', 'cancelled'
|
|
];
|
|
exports.videoDigests = (0, pg_core_1.pgTable)('video_digests', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
jobId: (0, pg_core_1.integer)('job_id'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
frameCount: (0, pg_core_1.integer)('frame_count'),
|
|
config: (0, pg_core_1.jsonb)('config').$type(),
|
|
frameAnalyses: (0, pg_core_1.jsonb)('frame_analyses').$type(),
|
|
transcript: (0, pg_core_1.jsonb)('transcript').$type(),
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
suggestedClips: (0, pg_core_1.jsonb)('suggested_clips').$type(),
|
|
adCutSpec: (0, pg_core_1.jsonb)('ad_cut_spec').$type(),
|
|
// Per-frame stage results for observability (persisted during analysis)
|
|
stageResults: (0, pg_core_1.jsonb)('stage_results').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
error: (0, pg_core_1.text)('error'),
|
|
}, (table) => ({
|
|
videoIdx: (0, pg_core_1.index)('idx_video_digests_video').on(table.videoId),
|
|
statusIdx: (0, pg_core_1.index)('idx_video_digests_status').on(table.status),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_video_digests_created').on(table.createdAt),
|
|
}));
|
|
exports.digestVideoTags = (0, pg_core_1.pgTable)('digest_video_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
category: (0, pg_core_1.text)('category').notNull(),
|
|
value: (0, pg_core_1.text)('value').notNull(),
|
|
confidence: (0, pg_core_1.integer)('confidence'),
|
|
source: (0, pg_core_1.text)('source').default('digest'),
|
|
evidence: (0, pg_core_1.jsonb)('evidence').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_video_tags_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_video_tags_video').on(table.videoId),
|
|
categoryIdx: (0, pg_core_1.index)('idx_digest_video_tags_category').on(table.category),
|
|
valueIdx: (0, pg_core_1.index)('idx_digest_video_tags_value').on(table.value),
|
|
categoryValueIdx: (0, pg_core_1.index)('idx_digest_video_tags_cat_val').on(table.category, table.value),
|
|
}));
|
|
// ============================================
|
|
// Digest Clip Selection & Generation Tables
|
|
// ============================================
|
|
exports.CLIP_TYPES = ['hook', 'intro', 'action', 'climax', 'highlight'];
|
|
exports.CLIP_STATUS = ['pending', 'processing', 'completed', 'failed'];
|
|
exports.CLIP_SOURCES = ['machine', 'manual'];
|
|
// Stage 1: Selected clips from digest analysis (metadata only, no extraction yet)
|
|
exports.digestSelectedClips = (0, pg_core_1.pgTable)('digest_selected_clips', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
clipType: (0, pg_core_1.text)('clip_type').notNull(),
|
|
startTime: (0, pg_core_1.integer)('start_time').notNull(), // seconds
|
|
endTime: (0, pg_core_1.integer)('end_time').notNull(),
|
|
duration: (0, pg_core_1.integer)('duration').notNull(),
|
|
reason: (0, pg_core_1.text)('reason'),
|
|
interestScore: (0, pg_core_1.integer)('interest_score'),
|
|
position: (0, pg_core_1.text)('position'), // For action clips (e.g., 'doggy style')
|
|
transcriptHint: (0, pg_core_1.text)('transcript_hint'), // For climax clips (keyword that triggered selection)
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
source: (0, pg_core_1.text)('source').notNull().default('machine'),
|
|
isIncluded: (0, pg_core_1.integer)('is_included').notNull().default(1), // 1 = included in generation, 0 = excluded
|
|
isHook: (0, pg_core_1.integer)('is_hook').notNull().default(0), // 1 = this clip is the cinematic hook (teaser at start)
|
|
sequenceOrder: (0, pg_core_1.integer)('sequence_order').notNull().default(0), // Order in final compilation (0-based)
|
|
hookSourceClipId: (0, pg_core_1.integer)('hook_source_clip_id'), // If set, this is a hook copy cloned from the referenced clip
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_selected_clips_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_selected_clips_video').on(table.videoId),
|
|
clipTypeIdx: (0, pg_core_1.index)('idx_digest_selected_clips_type').on(table.clipType),
|
|
sourceIdx: (0, pg_core_1.index)('idx_digest_selected_clips_source').on(table.source),
|
|
sequenceOrderIdx: (0, pg_core_1.index)('idx_digest_selected_clips_sequence').on(table.sequenceOrder),
|
|
isHookIdx: (0, pg_core_1.index)('idx_digest_selected_clips_is_hook').on(table.isHook),
|
|
hookSourceIdx: (0, pg_core_1.index)('idx_digest_selected_clips_hook_source').on(table.hookSourceClipId),
|
|
}));
|
|
// Stage 2: Generated clips (after FFmpeg extraction)
|
|
exports.digestGeneratedClips = (0, pg_core_1.pgTable)('digest_generated_clips', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
selectedClipId: (0, pg_core_1.integer)('selected_clip_id').notNull(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
folderId: (0, pg_core_1.integer)('folder_id'), // References digestOutputFolders.id
|
|
clipPath: (0, pg_core_1.text)('clip_path'),
|
|
gifPath: (0, pg_core_1.text)('gif_path'), // Only set if duration <= 60s
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
error: (0, pg_core_1.text)('error'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
// Publishing tracking
|
|
publishedToPublicMediaId: (0, pg_core_1.integer)('published_to_public_media_id'),
|
|
publishedAt: (0, pg_core_1.timestamp)('published_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
selectedClipIdx: (0, pg_core_1.index)('idx_digest_generated_clips_selected').on(table.selectedClipId),
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_generated_clips_digest').on(table.digestId),
|
|
statusIdx: (0, pg_core_1.index)('idx_digest_generated_clips_status').on(table.status),
|
|
folderIdx: (0, pg_core_1.index)('idx_digest_generated_clips_folder').on(table.folderId),
|
|
}));
|
|
// Caption style type for digest compilations
|
|
exports.CAPTION_POSITIONS = ['bottom', 'top'];
|
|
exports.CAPTION_SIZES = ['small', 'medium', 'large'];
|
|
// Stage 2: Compilation videos
|
|
exports.digestCompilations = (0, pg_core_1.pgTable)('digest_compilations', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
folderId: (0, pg_core_1.integer)('folder_id'), // References digestOutputFolders.id
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
name: (0, pg_core_1.text)('name'), // Optional user-provided name for the compilation
|
|
path: (0, pg_core_1.text)('path').notNull(),
|
|
durationSeconds: (0, pg_core_1.integer)('duration_seconds'),
|
|
orientation: (0, pg_core_1.text)('orientation'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
error: (0, pg_core_1.text)('error'),
|
|
// Caption burning options
|
|
hasCaptions: (0, pg_core_1.integer)('has_captions').notNull().default(0), // 1 = captions burned in
|
|
captionStyle: (0, pg_core_1.jsonb)('caption_style').$type(),
|
|
// Closing ad frame options
|
|
closingAdPath: (0, pg_core_1.text)('closing_ad_path'), // Path to uploaded ad frame image
|
|
closingAdDuration: (0, pg_core_1.integer)('closing_ad_duration'), // Duration in seconds (3-10)
|
|
// Aggregated tags from constituent segments
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_compilations_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_compilations_video').on(table.videoId),
|
|
statusIdx: (0, pg_core_1.index)('idx_digest_compilations_status').on(table.status),
|
|
folderIdx: (0, pg_core_1.index)('idx_digest_compilations_folder').on(table.folderId),
|
|
}));
|
|
// ============================================
|
|
// Digest Output Folders Table
|
|
// ============================================
|
|
exports.digestOutputFolders = (0, pg_core_1.pgTable)('digest_output_folders', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
folderPath: (0, pg_core_1.text)('folder_path').notNull(),
|
|
folderName: (0, pg_core_1.text)('folder_name').notNull(),
|
|
folderType: (0, pg_core_1.text)('folder_type').notNull().default('clips'), // 'clips' | 'scenes' | 'compilation'
|
|
clipCount: (0, pg_core_1.integer)('clip_count').default(0),
|
|
compilationCount: (0, pg_core_1.integer)('compilation_count').default(0),
|
|
totalSize: (0, pg_core_1.integer)('total_size'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_output_folders_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_output_folders_video').on(table.videoId),
|
|
folderPathIdx: (0, pg_core_1.index)('idx_digest_output_folders_path').on(table.folderPath),
|
|
}));
|
|
// ============================================
|
|
// Digest Generated Scenes Table
|
|
// ============================================
|
|
// Tracks individual scene files extracted from digests with derived tags
|
|
exports.digestGeneratedScenes = (0, pg_core_1.pgTable)('digest_generated_scenes', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
folderId: (0, pg_core_1.integer)('folder_id'), // References digestOutputFolders.id
|
|
sceneNumber: (0, pg_core_1.integer)('scene_number').notNull(),
|
|
scenePath: (0, pg_core_1.text)('scene_path'),
|
|
startTime: (0, pg_core_1.real)('start_time').notNull(),
|
|
endTime: (0, pg_core_1.real)('end_time').notNull(),
|
|
duration: (0, pg_core_1.real)('duration').notNull(),
|
|
// Tags derived from videoTagTimeline for this time range
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
dominantPosition: (0, pg_core_1.text)('dominant_position'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
// Publishing tracking
|
|
publishedToPublicMediaId: (0, pg_core_1.integer)('published_to_public_media_id'),
|
|
publishedAt: (0, pg_core_1.timestamp)('published_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_generated_scenes_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_generated_scenes_video').on(table.videoId),
|
|
folderIdx: (0, pg_core_1.index)('idx_digest_generated_scenes_folder').on(table.folderId),
|
|
scenePathIdx: (0, pg_core_1.index)('idx_digest_generated_scenes_path').on(table.scenePath),
|
|
}));
|
|
// Caches detected scene cuts per video for clean clip transitions
|
|
// Extended for combined multi-detector scene detection
|
|
exports.videoSceneCuts = (0, pg_core_1.pgTable)('video_scene_cuts', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
// Array of cut timestamps in seconds (includes 0 and video end)
|
|
cuts: (0, pg_core_1.jsonb)('cuts').$type().notNull(),
|
|
// Number of scenes detected
|
|
sceneCount: (0, pg_core_1.integer)('scene_count').notNull(),
|
|
// Video duration at time of detection
|
|
duration: (0, pg_core_1.real)('duration').notNull(),
|
|
// Detection configuration
|
|
detector: (0, pg_core_1.text)('detector').notNull().default('content'), // 'content', 'adaptive', 'transnetv2', 'combined'
|
|
threshold: (0, pg_core_1.real)('threshold').notNull().default(27.0),
|
|
// ============================================
|
|
// Combined Detection Fields (for multi-detector merging)
|
|
// ============================================
|
|
// Individual detector results for debugging/analysis
|
|
transnetCuts: (0, pg_core_1.jsonb)('transnet_cuts').$type(),
|
|
pyscenedetectCuts: (0, pg_core_1.jsonb)('pyscenedetect_cuts').$type(),
|
|
clipCuts: (0, pg_core_1.jsonb)('clip_cuts').$type(),
|
|
// Detailed merged cuts with source attribution
|
|
mergedCutsDetailed: (0, pg_core_1.jsonb)('merged_cuts_detailed').$type(),
|
|
// Merge configuration
|
|
mergeStrategy: (0, pg_core_1.text)('merge_strategy').default('weighted'), // 'union' | 'intersection' | 'weighted' | 'majority'
|
|
toleranceSeconds: (0, pg_core_1.real)('tolerance_seconds').default(0.75),
|
|
// Which detectors were used (for cache validation)
|
|
detectorsUsed: (0, pg_core_1.jsonb)('detectors_used').$type(),
|
|
// Total processing time for combined detection
|
|
processingTimeMs: (0, pg_core_1.integer)('processing_time_ms'),
|
|
// Timestamps
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
videoIdx: (0, pg_core_1.index)('idx_video_scene_cuts_video').on(table.videoId),
|
|
}));
|
|
// ============================================
|
|
// Video Segmentation Tables (Multi-Method Analysis)
|
|
// ============================================
|
|
// Segment types for multi-method clip detection
|
|
exports.SEGMENT_TYPES = ['scene', 'tag_change', 'vocal', 'fixed_interval'];
|
|
// Vocal moment categories for transcript-based segments
|
|
exports.VOCAL_CATEGORIES = ['dialogue', 'dirty_talk', 'climax', 'interview', 'moan'];
|
|
// Stores JoyTag results at regular intervals for change detection
|
|
exports.videoTagTimeline = (0, pg_core_1.pgTable)('video_tag_timeline', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
timestamp: (0, pg_core_1.real)('timestamp').notNull(), // Seconds from video start
|
|
// JoyTag results at this timestamp
|
|
tags: (0, pg_core_1.jsonb)('tags').$type().notNull(),
|
|
// Derived/computed fields for efficient querying
|
|
dominantPosition: (0, pg_core_1.text)('dominant_position'), // Most confident sexual position tag
|
|
performerCount: (0, pg_core_1.integer)('performer_count'),
|
|
intensity: (0, pg_core_1.integer)('intensity'), // 0-10 score based on tag analysis
|
|
hasDialogue: (0, pg_core_1.boolean)('has_dialogue').default(false),
|
|
hasEyeContact: (0, pg_core_1.boolean)('has_eye_contact').default(false),
|
|
// Frame reference
|
|
framePath: (0, pg_core_1.text)('frame_path'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
videoIdx: (0, pg_core_1.index)('idx_video_tag_timeline_video').on(table.videoId),
|
|
digestIdx: (0, pg_core_1.index)('idx_video_tag_timeline_digest').on(table.digestId),
|
|
timestampIdx: (0, pg_core_1.index)('idx_video_tag_timeline_timestamp').on(table.videoId, table.timestamp),
|
|
positionIdx: (0, pg_core_1.index)('idx_video_tag_timeline_position').on(table.dominantPosition),
|
|
}));
|
|
// Stores detected segments by type (scene, tag_change, vocal, fixed_interval)
|
|
exports.videoSegments = (0, pg_core_1.pgTable)('video_segments', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
segmentType: (0, pg_core_1.text)('segment_type').notNull().$type(),
|
|
startTime: (0, pg_core_1.real)('start_time').notNull(), // Seconds
|
|
endTime: (0, pg_core_1.real)('end_time').notNull(), // Seconds
|
|
duration: (0, pg_core_1.real)('duration').notNull(), // Seconds
|
|
// Segment metadata (varies by type)
|
|
tags: (0, pg_core_1.jsonb)('tags').$type(),
|
|
transcriptSnippet: (0, pg_core_1.text)('transcript_snippet'),
|
|
vocalCategory: (0, pg_core_1.text)('vocal_category').$type(),
|
|
positionChange: (0, pg_core_1.text)('position_change'), // "oral → doggystyle"
|
|
dominantPosition: (0, pg_core_1.text)('dominant_position'),
|
|
// Scoring for auto-sequence building
|
|
interestScore: (0, pg_core_1.real)('interest_score').default(0),
|
|
hasEyeContact: (0, pg_core_1.boolean)('has_eye_contact').default(false),
|
|
hasFullSentence: (0, pg_core_1.boolean)('has_full_sentence').default(false),
|
|
isClimaxMoment: (0, pg_core_1.boolean)('is_climax_moment').default(false),
|
|
// Caption/title generation
|
|
captioned: (0, pg_core_1.boolean)('captioned').default(false),
|
|
captionText: (0, pg_core_1.text)('caption_text'),
|
|
title: (0, pg_core_1.text)('title'),
|
|
// Frame reference for thumbnail
|
|
keyframePath: (0, pg_core_1.text)('keyframe_path'),
|
|
keyframeTimestamp: (0, pg_core_1.real)('keyframe_timestamp'),
|
|
// Inclusion in sequence (for Abstract)
|
|
isIncluded: (0, pg_core_1.boolean)('is_included').default(true),
|
|
sequenceOrder: (0, pg_core_1.integer)('sequence_order'),
|
|
isHook: (0, pg_core_1.boolean)('is_hook').default(false),
|
|
// Inclusion for clip generation (for Clips tab, independent from Abstract)
|
|
includeInClips: (0, pg_core_1.boolean)('include_in_clips').default(true),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_video_segments_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_video_segments_video').on(table.videoId),
|
|
typeIdx: (0, pg_core_1.index)('idx_video_segments_type').on(table.segmentType),
|
|
timeIdx: (0, pg_core_1.index)('idx_video_segments_time').on(table.videoId, table.startTime),
|
|
includedIdx: (0, pg_core_1.index)('idx_video_segments_included').on(table.digestId, table.isIncluded),
|
|
sequenceIdx: (0, pg_core_1.index)('idx_video_segments_sequence').on(table.digestId, table.sequenceOrder),
|
|
clipsIdx: (0, pg_core_1.index)('idx_video_segments_clips').on(table.digestId, table.includeInClips),
|
|
}));
|
|
// ============================================
|
|
// Video Tag Mapping Tables (Digest → Tag System)
|
|
// ============================================
|
|
// Links videos to existing tags (from digest or manual)
|
|
exports.videoTags = (0, pg_core_1.pgTable)('video_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
tagId: (0, pg_core_1.integer)('tag_id').notNull(),
|
|
confidence: (0, pg_core_1.integer)('confidence'), // 0-100, null for manual
|
|
source: (0, pg_core_1.text)('source').notNull().default('manual'),
|
|
digestId: (0, pg_core_1.integer)('digest_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
videoIdx: (0, pg_core_1.index)('idx_video_tags_video').on(table.videoId),
|
|
tagIdx: (0, pg_core_1.index)('idx_video_tags_tag').on(table.tagId),
|
|
sourceIdx: (0, pg_core_1.index)('idx_video_tags_source').on(table.source),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_video_tags_unique').on(table.videoId, table.tagId),
|
|
}));
|
|
exports.SUGGESTED_TAG_STATUS = ['pending', 'approved', 'rejected', 'mapped'];
|
|
// Unmatched tags from digest that need admin review
|
|
exports.digestSuggestedTags = (0, pg_core_1.pgTable)('digest_suggested_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
suggestedCategory: (0, pg_core_1.text)('suggested_category').notNull(),
|
|
suggestedValue: (0, pg_core_1.text)('suggested_value').notNull(),
|
|
confidence: (0, pg_core_1.integer)('confidence'), // 0-100
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending'),
|
|
mappedTagId: (0, pg_core_1.integer)('mapped_tag_id'),
|
|
reviewedBy: (0, pg_core_1.integer)('reviewed_by'),
|
|
reviewedAt: (0, pg_core_1.timestamp)('reviewed_at', { withTimezone: true }),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_digest_suggested_tags_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_digest_suggested_tags_video').on(table.videoId),
|
|
statusIdx: (0, pg_core_1.index)('idx_digest_suggested_tags_status').on(table.status),
|
|
categoryValueIdx: (0, pg_core_1.index)('idx_digest_suggested_tags_cat_val').on(table.suggestedCategory, table.suggestedValue),
|
|
}));
|
|
// Clip-level tag assignments (links clips to formal tags)
|
|
exports.digestClipTags = (0, pg_core_1.pgTable)('digest_clip_tags', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
clipId: (0, pg_core_1.integer)('clip_id').notNull(),
|
|
tagId: (0, pg_core_1.integer)('tag_id').notNull(),
|
|
source: (0, pg_core_1.text)('source').notNull().default('auto'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
clipIdx: (0, pg_core_1.index)('idx_digest_clip_tags_clip').on(table.clipId),
|
|
tagIdx: (0, pg_core_1.index)('idx_digest_clip_tags_tag').on(table.tagId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_digest_clip_tags_unique').on(table.clipId, table.tagId),
|
|
}));
|
|
// ============================================
|
|
// Watch Party Tables
|
|
// ============================================
|
|
exports.WATCH_PARTY_STATUS = ['waiting', 'active', 'paused', 'ended'];
|
|
exports.LOOP_MODE = ['none', 'video', 'queue'];
|
|
exports.watchPartySessions = (0, pg_core_1.pgTable)('watch_party_sessions', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
hostId: (0, pg_core_1.integer)('host_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
mediaId: (0, pg_core_1.integer)('media_id').notNull().references(() => exports.publicMedia.id, { onDelete: 'cascade' }),
|
|
inviteCode: (0, pg_core_1.text)('invite_code').notNull().unique(),
|
|
status: (0, pg_core_1.text)('status').notNull().default('waiting'),
|
|
currentTime: (0, pg_core_1.integer)('current_time').default(0), // playback position in seconds
|
|
isPlaying: (0, pg_core_1.boolean)('is_playing').default(false),
|
|
maxParticipants: (0, pg_core_1.integer)('max_participants').default(20),
|
|
allowChat: (0, pg_core_1.boolean)('allow_chat').default(true),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
endedAt: (0, pg_core_1.timestamp)('ended_at', { withTimezone: true }),
|
|
// Post-party summary stats (generated when party ends)
|
|
summaryStats: (0, pg_core_1.jsonb)('summary_stats').$type(),
|
|
}, (table) => ({
|
|
hostIdx: (0, pg_core_1.index)('idx_watch_party_sessions_host').on(table.hostId),
|
|
inviteCodeIdx: (0, pg_core_1.index)('idx_watch_party_sessions_invite').on(table.inviteCode),
|
|
statusIdx: (0, pg_core_1.index)('idx_watch_party_sessions_status').on(table.status),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_watch_party_sessions_created').on(table.createdAt),
|
|
}));
|
|
exports.watchPartyParticipants = (0, pg_core_1.pgTable)('watch_party_participants', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
partyId: (0, pg_core_1.integer)('party_id').notNull().references(() => exports.watchPartySessions.id, { onDelete: 'cascade' }),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
isConnected: (0, pg_core_1.boolean)('is_connected').default(false),
|
|
isKicked: (0, pg_core_1.boolean)('is_kicked').default(false),
|
|
joinedAt: (0, pg_core_1.timestamp)('joined_at', { withTimezone: true }).defaultNow(),
|
|
leftAt: (0, pg_core_1.timestamp)('left_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
partyIdx: (0, pg_core_1.index)('idx_watch_party_participants_party').on(table.partyId),
|
|
userIdx: (0, pg_core_1.index)('idx_watch_party_participants_user').on(table.userId),
|
|
uniqueIdx: (0, pg_core_1.index)('idx_watch_party_participants_unique').on(table.partyId, table.userId),
|
|
}));
|
|
exports.watchPartyChatMessages = (0, pg_core_1.pgTable)('watch_party_chat_messages', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
partyId: (0, pg_core_1.integer)('party_id').notNull().references(() => exports.watchPartySessions.id, { onDelete: 'cascade' }),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
content: (0, pg_core_1.text)('content').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
partyIdx: (0, pg_core_1.index)('idx_watch_party_chat_party').on(table.partyId),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_watch_party_chat_created').on(table.createdAt),
|
|
}));
|
|
// Watch Party Reactions (persisted for summary stats)
|
|
exports.watchPartyReactions = (0, pg_core_1.pgTable)('watch_party_reactions', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
partyId: (0, pg_core_1.integer)('party_id').notNull().references(() => exports.watchPartySessions.id, { onDelete: 'cascade' }),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
reactionType: (0, pg_core_1.text)('reaction_type').notNull(),
|
|
videoTimestamp: (0, pg_core_1.integer)('video_timestamp'), // seconds into video when reaction was sent
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
partyIdx: (0, pg_core_1.index)('idx_watch_party_reactions_party').on(table.partyId),
|
|
userIdx: (0, pg_core_1.index)('idx_watch_party_reactions_user').on(table.userId),
|
|
}));
|
|
// Watch Party Invites
|
|
exports.INVITE_STATUS = ['pending', 'accepted', 'declined', 'expired'];
|
|
exports.watchPartyInvites = (0, pg_core_1.pgTable)('watch_party_invites', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
partyId: (0, pg_core_1.integer)('party_id').notNull().references(() => exports.watchPartySessions.id, { onDelete: 'cascade' }),
|
|
fromUserId: (0, pg_core_1.integer)('from_user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
toUserId: (0, pg_core_1.integer)('to_user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
respondedAt: (0, pg_core_1.timestamp)('responded_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
partyIdx: (0, pg_core_1.index)('idx_watch_party_invites_party').on(table.partyId),
|
|
toUserIdx: (0, pg_core_1.index)('idx_watch_party_invites_to_user').on(table.toUserId),
|
|
fromUserIdx: (0, pg_core_1.index)('idx_watch_party_invites_from_user').on(table.fromUserId),
|
|
statusIdx: (0, pg_core_1.index)('idx_watch_party_invites_status').on(table.status),
|
|
}));
|
|
exports.tagGenerationJobs = (0, pg_core_1.pgTable)('tag_generation_jobs', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
status: (0, pg_core_1.text)('status').$type().default('pending').notNull(),
|
|
contentType: (0, pg_core_1.text)('content_type').$type().notNull(),
|
|
contentIds: (0, pg_core_1.jsonb)('content_ids').$type().notNull(),
|
|
options: (0, pg_core_1.jsonb)('options').$type(),
|
|
results: (0, pg_core_1.jsonb)('results').$type(),
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
currentItem: (0, pg_core_1.integer)('current_item').default(0),
|
|
totalItems: (0, pg_core_1.integer)('total_items').default(0),
|
|
error: (0, pg_core_1.text)('error'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
statusIdx: (0, pg_core_1.index)('idx_tag_gen_status').on(table.status),
|
|
contentTypeIdx: (0, pg_core_1.index)('idx_tag_gen_content_type').on(table.contentType),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_tag_gen_created_at').on(table.createdAt),
|
|
}));
|
|
// ============================================================================
|
|
// Performer Face Recognition
|
|
// ============================================================================
|
|
/**
|
|
* Named performers that can be associated with multiple detected faces.
|
|
* Integrates with tag system for public gallery filtering.
|
|
*/
|
|
exports.creators = (0, pg_core_1.pgTable)('creators', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
name: (0, pg_core_1.text)('name').notNull().unique(),
|
|
slug: (0, pg_core_1.text)('slug').notNull().unique(), // URL-friendly name for public browsing
|
|
gender: (0, pg_core_1.integer)('gender'), // 0=female, 1=male, null=unknown
|
|
primaryFaceId: (0, pg_core_1.integer)('primary_face_id'), // Best face image for display thumbnail
|
|
faceCount: (0, pg_core_1.integer)('face_count').default(0), // Denormalized count of linked faces
|
|
videoCount: (0, pg_core_1.integer)('video_count').default(0), // Videos featuring this performer
|
|
tagId: (0, pg_core_1.integer)('tag_id'), // Link to performers tag category for filtering
|
|
isPublic: (0, pg_core_1.boolean)('is_public').default(false), // Visible in public Browse Actors
|
|
// Centroid embedding for efficient matching (512-D averaged, quality-filtered)
|
|
centroidEmbedding: (0, pg_core_1.jsonb)('centroid_embedding').$type(),
|
|
centroidUpdatedAt: (0, pg_core_1.timestamp)('centroid_updated_at', { withTimezone: true }),
|
|
centroidFaceCount: (0, pg_core_1.integer)('centroid_face_count').default(0), // Faces used in centroid computation
|
|
// Quality metrics for adaptive matching
|
|
embeddingVariance: (0, pg_core_1.real)('embedding_variance'), // How spread out the embeddings are
|
|
avgIntraClusterSimilarity: (0, pg_core_1.real)('avg_intra_cluster_similarity'), // Average similarity within performer's faces
|
|
avgAge: (0, pg_core_1.real)('avg_age'), // Average detected age of performer
|
|
ageStdDev: (0, pg_core_1.real)('age_std_dev'), // Standard deviation of detected ages
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
nameIdx: (0, pg_core_1.index)('idx_creators_name').on(table.name),
|
|
slugIdx: (0, pg_core_1.index)('idx_creators_slug').on(table.slug),
|
|
genderIdx: (0, pg_core_1.index)('idx_creators_gender').on(table.gender),
|
|
tagIdx: (0, pg_core_1.index)('idx_creators_tag').on(table.tagId),
|
|
publicIdx: (0, pg_core_1.index)('idx_creators_public').on(table.isPublic),
|
|
}));
|
|
// Face status for tracking review state
|
|
exports.FACE_STATUS = ['pending', 'assigned', 'ignored', 'skipped'];
|
|
/**
|
|
* Stores face embeddings for performer recognition.
|
|
* Each row represents a single detected face that can be linked to a performer.
|
|
*/
|
|
exports.performerFaces = (0, pg_core_1.pgTable)('performer_faces', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
// Performer identification - FK to performers table (null if unidentified)
|
|
performerId: (0, pg_core_1.integer)('performer_id'), // Links to creators.id
|
|
performerName: (0, pg_core_1.text)('performer_name'), // Legacy field, kept for migration
|
|
// 512-dimensional face embedding vector from ArcFace
|
|
// Stored as JSON array for compatibility (consider pgvector for production)
|
|
faceEmbedding: (0, pg_core_1.jsonb)('face_embedding').$type().notNull(),
|
|
// Face attributes from InsightFace
|
|
gender: (0, pg_core_1.integer)('gender'), // 0=female, 1=male
|
|
age: (0, pg_core_1.integer)('age'),
|
|
detectionScore: (0, pg_core_1.real)('detection_score'), // Confidence of face detection
|
|
// Bounding box in source frame [x1, y1, x2, y2]
|
|
bbox: (0, pg_core_1.jsonb)('bbox').$type(),
|
|
// Face crop image path
|
|
faceCropPath: (0, pg_core_1.text)('face_crop_path'), // Path to cropped face thumbnail
|
|
// Source information
|
|
sourceFrame: (0, pg_core_1.text)('source_frame'), // Path to frame this was extracted from
|
|
sourceVideo: (0, pg_core_1.text)('source_video'), // Path to source video
|
|
sourceDigestId: (0, pg_core_1.integer)('source_digest_id'), // Link to video digest
|
|
frameTimestamp: (0, pg_core_1.real)('frame_timestamp'), // Timestamp in video
|
|
frameIndex: (0, pg_core_1.integer)('frame_index'), // Index of frame in digest
|
|
// Face grouping metadata
|
|
facesInFrame: (0, pg_core_1.integer)('faces_in_frame').default(1), // How many faces were in this frame
|
|
status: (0, pg_core_1.text)('status').$type().default('pending'), // Review status
|
|
// Matching metadata
|
|
matchConfidence: (0, pg_core_1.real)('match_confidence'), // Confidence of performer match (if matched)
|
|
isVerified: (0, pg_core_1.boolean)('is_verified').default(false), // Human-verified match
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
performerIdIdx: (0, pg_core_1.index)('idx_performer_faces_performer').on(table.performerId),
|
|
performerNameIdx: (0, pg_core_1.index)('idx_performer_faces_name').on(table.performerName),
|
|
sourceVideoIdx: (0, pg_core_1.index)('idx_performer_faces_video').on(table.sourceVideo),
|
|
genderIdx: (0, pg_core_1.index)('idx_performer_faces_gender').on(table.gender),
|
|
digestIdx: (0, pg_core_1.index)('idx_performer_faces_digest').on(table.sourceDigestId),
|
|
cropPathIdx: (0, pg_core_1.index)('idx_performer_faces_crop').on(table.faceCropPath),
|
|
statusIdx: (0, pg_core_1.index)('idx_performer_faces_status').on(table.status),
|
|
facesInFrameIdx: (0, pg_core_1.index)('idx_performer_faces_faces_in_frame').on(table.facesInFrame),
|
|
detectionScoreIdx: (0, pg_core_1.index)('idx_performer_faces_det_score').on(table.detectionScore),
|
|
}));
|
|
// Discrepancy types for filename vs face detection mismatches
|
|
exports.DISCREPANCY_TYPES = [
|
|
'no_face_match', // Parsed name has no matching face
|
|
'low_confidence', // Face match confidence below threshold
|
|
'name_mismatch', // Filename says X but face matches Y
|
|
'gender_mismatch', // Gender of face doesn't match expected
|
|
'unknown_performer', // Parsed name not in performers database
|
|
'extra_face', // Detected face not in parsed performers
|
|
];
|
|
// Resolution status for discrepancies
|
|
exports.DISCREPANCY_STATUS = ['pending', 'confirmed', 'rejected', 'merged', 'ignored'];
|
|
// Resolution types
|
|
exports.DISCREPANCY_RESOLUTIONS = ['use_filename', 'use_face', 'create_alias', 'ignore', 'assign_face'];
|
|
/**
|
|
* Tracks mismatches between filename-parsed performers and face detection results.
|
|
* Used for admin review and resolution of performer identification issues.
|
|
*/
|
|
exports.performerDiscrepancies = (0, pg_core_1.pgTable)('performer_discrepancies', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
// Source info
|
|
videoId: (0, pg_core_1.integer)('video_id').references(() => exports.videos.id, { onDelete: 'cascade' }),
|
|
digestId: (0, pg_core_1.integer)('digest_id').references(() => exports.videoDigests.id, { onDelete: 'cascade' }),
|
|
// What filename parsing said
|
|
parsedPerformerName: (0, pg_core_1.text)('parsed_performer_name').notNull(),
|
|
parsedMethod: (0, pg_core_1.text)('parsed_method'), // 'llm', 'regex', 'manual'
|
|
parsedConfidence: (0, pg_core_1.real)('parsed_confidence'),
|
|
// What face detection found
|
|
detectedPerformerId: (0, pg_core_1.integer)('detected_performer_id').references(() => exports.creators.id, { onDelete: 'set null' }),
|
|
detectedPerformerName: (0, pg_core_1.text)('detected_performer_name'),
|
|
faceMatchConfidence: (0, pg_core_1.real)('face_match_confidence'),
|
|
faceSimilarity: (0, pg_core_1.real)('face_similarity'),
|
|
// Discrepancy details
|
|
discrepancyType: (0, pg_core_1.text)('discrepancy_type').$type().notNull(),
|
|
// Resolution
|
|
status: (0, pg_core_1.text)('status').$type().default('pending').notNull(),
|
|
resolvedBy: (0, pg_core_1.integer)('resolved_by').references(() => exports.mediaUsers.id, { onDelete: 'set null' }),
|
|
resolvedAt: (0, pg_core_1.timestamp)('resolved_at', { withTimezone: true }),
|
|
resolution: (0, pg_core_1.text)('resolution').$type(),
|
|
adminNotes: (0, pg_core_1.text)('admin_notes'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
}, (table) => ({
|
|
videoIdx: (0, pg_core_1.index)('idx_discrepancies_video').on(table.videoId),
|
|
digestIdx: (0, pg_core_1.index)('idx_discrepancies_digest').on(table.digestId),
|
|
statusIdx: (0, pg_core_1.index)('idx_discrepancies_status').on(table.status),
|
|
typeIdx: (0, pg_core_1.index)('idx_discrepancies_type').on(table.discrepancyType),
|
|
detectedPerformerIdx: (0, pg_core_1.index)('idx_discrepancies_detected_performer').on(table.detectedPerformerId),
|
|
}));
|
|
// ============================================
|
|
// Pipeline Management System
|
|
// ============================================
|
|
// Pipeline status lifecycle
|
|
exports.PIPELINE_STATUS = ['pending', 'queued', 'running', 'paused', 'completed', 'failed', 'cancelled'];
|
|
// Pipeline step status
|
|
exports.PIPELINE_STEP_STATUS = ['pending', 'waiting_dependency', 'waiting_resource', 'running', 'completed', 'failed', 'skipped'];
|
|
// Pipeline definition - a multi-step workflow
|
|
exports.pipelines = (0, pg_core_1.pgTable)('pipelines', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
description: (0, pg_core_1.text)('description'),
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending').$type(),
|
|
priority: (0, pg_core_1.integer)('priority').default(5),
|
|
// Context for the entire pipeline (e.g., videoId, digestId)
|
|
context: (0, pg_core_1.jsonb)('context').$type(),
|
|
// Resource requirements aggregated from steps
|
|
totalVramRequired: (0, pg_core_1.integer)('total_vram_required').default(0),
|
|
estimatedDurationMs: (0, pg_core_1.integer)('estimated_duration_ms'),
|
|
// Progress tracking
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
currentStepId: (0, pg_core_1.integer)('current_step_id'),
|
|
completedSteps: (0, pg_core_1.integer)('completed_steps').default(0),
|
|
totalSteps: (0, pg_core_1.integer)('total_steps').default(0),
|
|
// Error handling
|
|
error: (0, pg_core_1.text)('error'),
|
|
retryCount: (0, pg_core_1.integer)('retry_count').default(0),
|
|
maxRetries: (0, pg_core_1.integer)('max_retries').default(3),
|
|
// Timestamps
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
queuedAt: (0, pg_core_1.timestamp)('queued_at', { withTimezone: true }),
|
|
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
// Crash recovery
|
|
lastHeartbeat: (0, pg_core_1.timestamp)('last_heartbeat', { withTimezone: true }),
|
|
runningOnNode: (0, pg_core_1.text)('running_on_node'), // For future multi-node support
|
|
}, (table) => ({
|
|
statusIdx: (0, pg_core_1.index)('idx_pipelines_status').on(table.status),
|
|
priorityIdx: (0, pg_core_1.index)('idx_pipelines_priority').on(table.priority, table.createdAt),
|
|
heartbeatIdx: (0, pg_core_1.index)('idx_pipelines_heartbeat').on(table.lastHeartbeat),
|
|
}));
|
|
// Individual steps within a pipeline
|
|
exports.pipelineSteps = (0, pg_core_1.pgTable)('pipeline_steps', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
pipelineId: (0, pg_core_1.integer)('pipeline_id').notNull().references(() => exports.pipelines.id, { onDelete: 'cascade' }),
|
|
// Step definition
|
|
name: (0, pg_core_1.text)('name').notNull(),
|
|
stepType: (0, pg_core_1.text)('step_type').notNull().$type(),
|
|
sequenceOrder: (0, pg_core_1.integer)('sequence_order').notNull(),
|
|
// Dependencies (array of step IDs that must complete first)
|
|
dependsOn: (0, pg_core_1.jsonb)('depends_on').$type().default([]),
|
|
// Resource requirements
|
|
resourceCategory: (0, pg_core_1.text)('resource_category').default('cpu').$type(),
|
|
vramRequired: (0, pg_core_1.integer)('vram_required').default(0),
|
|
requiredContainers: (0, pg_core_1.jsonb)('required_containers').$type().default([]),
|
|
// Step configuration
|
|
config: (0, pg_core_1.jsonb)('config').$type(),
|
|
// Execution state
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending').$type(),
|
|
progress: (0, pg_core_1.integer)('progress').default(0),
|
|
result: (0, pg_core_1.jsonb)('result').$type(),
|
|
error: (0, pg_core_1.text)('error'),
|
|
// Timing
|
|
estimatedDurationMs: (0, pg_core_1.integer)('estimated_duration_ms'),
|
|
startedAt: (0, pg_core_1.timestamp)('started_at', { withTimezone: true }),
|
|
completedAt: (0, pg_core_1.timestamp)('completed_at', { withTimezone: true }),
|
|
durationMs: (0, pg_core_1.integer)('duration_ms'),
|
|
}, (table) => ({
|
|
pipelineIdx: (0, pg_core_1.index)('idx_pipeline_steps_pipeline').on(table.pipelineId),
|
|
sequenceIdx: (0, pg_core_1.index)('idx_pipeline_steps_sequence').on(table.pipelineId, table.sequenceOrder),
|
|
statusIdx: (0, pg_core_1.index)('idx_pipeline_steps_status').on(table.status),
|
|
}));
|
|
// Step-level progress events for detailed observability
|
|
exports.pipelineStepEvents = (0, pg_core_1.pgTable)('pipeline_step_events', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
stepId: (0, pg_core_1.integer)('step_id').notNull().references(() => exports.pipelineSteps.id, { onDelete: 'cascade' }),
|
|
eventType: (0, pg_core_1.text)('event_type').notNull(), // 'stage_start', 'progress', 'prompt_call', 'stage_complete', 'log', 'error'
|
|
eventData: (0, pg_core_1.jsonb)('event_data').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
stepIdx: (0, pg_core_1.index)('idx_pipeline_step_events_step').on(table.stepId),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_pipeline_step_events_created').on(table.createdAt),
|
|
}));
|
|
// Real-time GPU/resource tracking snapshots
|
|
exports.resourceSnapshots = (0, pg_core_1.pgTable)('resource_snapshots', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
// GPU metrics (from nvidia-smi)
|
|
gpuIndex: (0, pg_core_1.integer)('gpu_index').default(0),
|
|
gpuName: (0, pg_core_1.text)('gpu_name'),
|
|
vramUsedMb: (0, pg_core_1.integer)('vram_used_mb'),
|
|
vramTotalMb: (0, pg_core_1.integer)('vram_total_mb'),
|
|
gpuUtilization: (0, pg_core_1.integer)('gpu_utilization'), // 0-100%
|
|
gpuTemperature: (0, pg_core_1.integer)('gpu_temperature'), // Celsius
|
|
// Active processes using GPU
|
|
activeProcesses: (0, pg_core_1.jsonb)('active_processes').$type(),
|
|
// Linked to pipeline/step if applicable
|
|
pipelineId: (0, pg_core_1.integer)('pipeline_id'),
|
|
stepId: (0, pg_core_1.integer)('step_id'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
createdAtIdx: (0, pg_core_1.index)('idx_resource_snapshots_created').on(table.createdAt),
|
|
pipelineIdx: (0, pg_core_1.index)('idx_resource_snapshots_pipeline').on(table.pipelineId),
|
|
}));
|
|
// Pipeline templates for common workflows
|
|
exports.pipelineTemplates = (0, pg_core_1.pgTable)('pipeline_templates', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
name: (0, pg_core_1.text)('name').notNull().unique(),
|
|
displayName: (0, pg_core_1.text)('display_name').notNull(),
|
|
description: (0, pg_core_1.text)('description'),
|
|
// Template definition - array of step configurations
|
|
steps: (0, pg_core_1.jsonb)('steps').$type().notNull(),
|
|
// Default context schema (for validation)
|
|
contextSchema: (0, pg_core_1.jsonb)('context_schema').$type(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
nameIdx: (0, pg_core_1.index)('idx_pipeline_templates_name').on(table.name),
|
|
}));
|
|
// Subscription Plans - Available subscription tiers
|
|
exports.subscriptionPlans = (0, pg_core_1.pgTable)('subscription_plans', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
name: (0, pg_core_1.text)('name').notNull().unique(), // 'monthly', 'quarterly', 'yearly', 'lifetime'
|
|
displayName: (0, pg_core_1.text)('display_name').notNull(), // 'Monthly Subscription'
|
|
description: (0, pg_core_1.text)('description'),
|
|
priceCAD: (0, pg_core_1.integer)('price_cad').notNull(), // Price in cents (e.g., 1000 = $10.00)
|
|
durationDays: (0, pg_core_1.integer)('duration_days'), // null for lifetime
|
|
isActive: (0, pg_core_1.boolean)('is_active').default(true),
|
|
sortOrder: (0, pg_core_1.integer)('sort_order').default(0),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
});
|
|
// User Subscriptions - Tracks individual user subscription status
|
|
exports.userSubscriptions = (0, pg_core_1.pgTable)('user_subscriptions', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
planId: (0, pg_core_1.integer)('plan_id').references(() => exports.subscriptionPlans.id),
|
|
status: (0, pg_core_1.text)('status').notNull().default('inactive').$type(),
|
|
startDate: (0, pg_core_1.timestamp)('start_date', { withTimezone: true }),
|
|
endDate: (0, pg_core_1.timestamp)('end_date', { withTimezone: true }), // null for lifetime
|
|
gracePeriodEndDate: (0, pg_core_1.timestamp)('grace_period_end_date', { withTimezone: true }),
|
|
lastPaymentId: (0, pg_core_1.integer)('last_payment_id'),
|
|
autoRenew: (0, pg_core_1.boolean)('auto_renew').default(true),
|
|
notes: (0, pg_core_1.text)('notes'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.uniqueIndex)('idx_user_subscriptions_user').on(table.userId),
|
|
statusIdx: (0, pg_core_1.index)('idx_user_subscriptions_status').on(table.status),
|
|
endDateIdx: (0, pg_core_1.index)('idx_user_subscriptions_end_date').on(table.endDate),
|
|
}));
|
|
// Invoices - Generated invoices for users
|
|
exports.invoices = (0, pg_core_1.pgTable)('invoices', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
planId: (0, pg_core_1.integer)('plan_id').references(() => exports.subscriptionPlans.id),
|
|
invoiceNumber: (0, pg_core_1.text)('invoice_number').notNull().unique(), // INV-2026-00001
|
|
amountCAD: (0, pg_core_1.integer)('amount_cad').notNull(), // Amount in cents
|
|
status: (0, pg_core_1.text)('status').notNull().default('pending').$type(),
|
|
dueDate: (0, pg_core_1.timestamp)('due_date', { withTimezone: true }).notNull(),
|
|
paidAt: (0, pg_core_1.timestamp)('paid_at', { withTimezone: true }),
|
|
paymentId: (0, pg_core_1.integer)('payment_id'),
|
|
description: (0, pg_core_1.text)('description'),
|
|
notes: (0, pg_core_1.text)('notes'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_invoices_user').on(table.userId),
|
|
statusIdx: (0, pg_core_1.index)('idx_invoices_status').on(table.status),
|
|
dueDateIdx: (0, pg_core_1.index)('idx_invoices_due_date').on(table.dueDate),
|
|
invoiceNumberIdx: (0, pg_core_1.index)('idx_invoices_number').on(table.invoiceNumber),
|
|
}));
|
|
// Payments - Records of actual payments received (e-transfers)
|
|
exports.payments = (0, pg_core_1.pgTable)('payments', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
invoiceId: (0, pg_core_1.integer)('invoice_id').references(() => exports.invoices.id),
|
|
userId: (0, pg_core_1.integer)('user_id').references(() => exports.mediaUsers.id, { onDelete: 'set null' }),
|
|
// E-transfer details
|
|
senderEmail: (0, pg_core_1.text)('sender_email').notNull(),
|
|
senderName: (0, pg_core_1.text)('sender_name'),
|
|
amountCAD: (0, pg_core_1.integer)('amount_cad').notNull(), // Amount in cents
|
|
referenceNumber: (0, pg_core_1.text)('reference_number').unique(), // Interac reference
|
|
customMessage: (0, pg_core_1.text)('custom_message'), // Contains username hint
|
|
// Matching/processing status
|
|
status: (0, pg_core_1.text)('status').notNull().default('unmatched').$type(),
|
|
matchConfidence: (0, pg_core_1.real)('match_confidence'), // 0.0-1.0 confidence score
|
|
matchMethod: (0, pg_core_1.text)('match_method'), // 'email_exact', 'username_message', 'manual', 'llm'
|
|
// Raw email data for audit trail
|
|
rawEmailJson: (0, pg_core_1.jsonb)('raw_email_json').$type(),
|
|
receivedAt: (0, pg_core_1.timestamp)('received_at', { withTimezone: true }).notNull(),
|
|
processedAt: (0, pg_core_1.timestamp)('processed_at', { withTimezone: true }),
|
|
processedBy: (0, pg_core_1.integer)('processed_by'), // Admin who manually matched
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_payments_user').on(table.userId),
|
|
invoiceIdx: (0, pg_core_1.index)('idx_payments_invoice').on(table.invoiceId),
|
|
statusIdx: (0, pg_core_1.index)('idx_payments_status').on(table.status),
|
|
senderEmailIdx: (0, pg_core_1.index)('idx_payments_sender_email').on(table.senderEmail),
|
|
referenceIdx: (0, pg_core_1.index)('idx_payments_reference').on(table.referenceNumber),
|
|
receivedAtIdx: (0, pg_core_1.index)('idx_payments_received_at').on(table.receivedAt),
|
|
}));
|
|
// Payment Audit Log - Audit trail for all payment operations
|
|
exports.paymentAuditLog = (0, pg_core_1.pgTable)('payment_audit_log', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
paymentId: (0, pg_core_1.integer)('payment_id').references(() => exports.payments.id),
|
|
invoiceId: (0, pg_core_1.integer)('invoice_id').references(() => exports.invoices.id),
|
|
userId: (0, pg_core_1.integer)('user_id').references(() => exports.mediaUsers.id),
|
|
action: (0, pg_core_1.text)('action').notNull(), // 'created', 'matched', 'applied', 'refunded', 'manual_override', etc.
|
|
previousState: (0, pg_core_1.jsonb)('previous_state').$type(),
|
|
newState: (0, pg_core_1.jsonb)('new_state').$type(),
|
|
performedBy: (0, pg_core_1.integer)('performed_by'), // Admin user ID
|
|
notes: (0, pg_core_1.text)('notes'),
|
|
ipAddress: (0, pg_core_1.text)('ip_address'),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
paymentIdx: (0, pg_core_1.index)('idx_payment_audit_payment').on(table.paymentId),
|
|
invoiceIdx: (0, pg_core_1.index)('idx_payment_audit_invoice').on(table.invoiceId),
|
|
userIdx: (0, pg_core_1.index)('idx_payment_audit_user').on(table.userId),
|
|
actionIdx: (0, pg_core_1.index)('idx_payment_audit_action').on(table.action),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_payment_audit_created').on(table.createdAt),
|
|
}));
|
|
// ============================================
|
|
// Notification System Tables
|
|
// ============================================
|
|
// Notification types enum
|
|
exports.NOTIFICATION_TYPES = [
|
|
'friend_request',
|
|
'friend_request_accepted',
|
|
'poke',
|
|
'video_recommendation',
|
|
'watch_party_invite',
|
|
'achievement_unlocked',
|
|
'comment_reply',
|
|
'digest_completed',
|
|
'digest_failed',
|
|
'upload_approved',
|
|
'upload_rejected',
|
|
'system',
|
|
];
|
|
// Main notifications table
|
|
exports.notifications = (0, pg_core_1.pgTable)('notifications', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
type: (0, pg_core_1.text)('type').notNull().$type(),
|
|
title: (0, pg_core_1.text)('title').notNull(),
|
|
message: (0, pg_core_1.text)('message'),
|
|
referenceType: (0, pg_core_1.text)('reference_type'), // 'user', 'media', 'watch_party', 'digest', 'upload', etc.
|
|
referenceId: (0, pg_core_1.integer)('reference_id'),
|
|
actorId: (0, pg_core_1.integer)('actor_id').references(() => exports.mediaUsers.id, { onDelete: 'set null' }),
|
|
metadata: (0, pg_core_1.jsonb)('metadata').$type(),
|
|
isRead: (0, pg_core_1.boolean)('is_read').default(false),
|
|
actionUrl: (0, pg_core_1.text)('action_url'),
|
|
groupKey: (0, pg_core_1.text)('group_key'), // For aggregation (e.g., "poke_123" to group multiple pokes from same user)
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
readAt: (0, pg_core_1.timestamp)('read_at', { withTimezone: true }),
|
|
}, (table) => ({
|
|
userUnreadIdx: (0, pg_core_1.index)('idx_notifications_user_unread').on(table.userId, table.isRead),
|
|
userCreatedIdx: (0, pg_core_1.index)('idx_notifications_user_created').on(table.userId, table.createdAt),
|
|
typeIdx: (0, pg_core_1.index)('idx_notifications_type').on(table.type),
|
|
createdAtIdx: (0, pg_core_1.index)('idx_notifications_created').on(table.createdAt),
|
|
actorIdx: (0, pg_core_1.index)('idx_notifications_actor').on(table.actorId),
|
|
}));
|
|
// User notification preferences table
|
|
exports.notificationPreferences = (0, pg_core_1.pgTable)('notification_preferences', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
userId: (0, pg_core_1.integer)('user_id').notNull().unique().references(() => exports.mediaUsers.id, { onDelete: 'cascade' }),
|
|
// Email toggles per notification type
|
|
emailFriendRequest: (0, pg_core_1.boolean)('email_friend_request').default(true),
|
|
emailFriendRequestAccepted: (0, pg_core_1.boolean)('email_friend_request_accepted').default(true),
|
|
emailPoke: (0, pg_core_1.boolean)('email_poke').default(true),
|
|
emailVideoRecommendation: (0, pg_core_1.boolean)('email_video_recommendation').default(true),
|
|
emailWatchPartyInvite: (0, pg_core_1.boolean)('email_watch_party_invite').default(true),
|
|
emailAchievement: (0, pg_core_1.boolean)('email_achievement').default(false),
|
|
emailDigestComplete: (0, pg_core_1.boolean)('email_digest_complete').default(true),
|
|
emailDigestFailed: (0, pg_core_1.boolean)('email_digest_failed').default(true),
|
|
emailUploadApproved: (0, pg_core_1.boolean)('email_upload_approved').default(true),
|
|
emailUploadRejected: (0, pg_core_1.boolean)('email_upload_rejected').default(true),
|
|
// In-app toggles per notification type (default all on)
|
|
inAppFriendRequest: (0, pg_core_1.boolean)('inapp_friend_request').default(true),
|
|
inAppFriendRequestAccepted: (0, pg_core_1.boolean)('inapp_friend_request_accepted').default(true),
|
|
inAppPoke: (0, pg_core_1.boolean)('inapp_poke').default(true),
|
|
inAppVideoRecommendation: (0, pg_core_1.boolean)('inapp_video_recommendation').default(true),
|
|
inAppWatchPartyInvite: (0, pg_core_1.boolean)('inapp_watch_party_invite').default(true),
|
|
inAppAchievement: (0, pg_core_1.boolean)('inapp_achievement').default(true),
|
|
inAppDigestComplete: (0, pg_core_1.boolean)('inapp_digest_complete').default(true),
|
|
inAppDigestFailed: (0, pg_core_1.boolean)('inapp_digest_failed').default(true),
|
|
inAppUploadApproved: (0, pg_core_1.boolean)('inapp_upload_approved').default(true),
|
|
inAppUploadRejected: (0, pg_core_1.boolean)('inapp_upload_rejected').default(true),
|
|
inAppCommentReply: (0, pg_core_1.boolean)('inapp_comment_reply').default(true),
|
|
inAppSystem: (0, pg_core_1.boolean)('inapp_system').default(true),
|
|
// Quiet hours settings
|
|
quietHoursEnabled: (0, pg_core_1.boolean)('quiet_hours_enabled').default(false),
|
|
quietHoursStart: (0, pg_core_1.text)('quiet_hours_start'), // "22:00" (24-hour format)
|
|
quietHoursEnd: (0, pg_core_1.text)('quiet_hours_end'), // "08:00"
|
|
updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
userIdx: (0, pg_core_1.index)('idx_notification_preferences_user').on(table.userId),
|
|
}));
|
|
// ============================================
|
|
// Geo-Blocking Tables
|
|
// ============================================
|
|
exports.GEO_BLOCKING_MODES = ['disabled', 'blocklist', 'allowlist'];
|
|
exports.geoBlockingRules = (0, pg_core_1.pgTable)('geo_blocking_rules', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
country: (0, pg_core_1.text)('country').notNull(), // ISO Alpha-2 code (required)
|
|
countryName: (0, pg_core_1.text)('country_name'), // Display name
|
|
region: (0, pg_core_1.text)('region'), // Optional - state/province ISO code
|
|
city: (0, pg_core_1.text)('city'), // Optional - city name
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
createdBy: (0, pg_core_1.integer)('created_by').references(() => exports.mediaUsers.id),
|
|
note: (0, pg_core_1.text)('note'), // Admin note explaining why blocked
|
|
}, (table) => ({
|
|
countryIdx: (0, pg_core_1.index)('idx_geo_blocking_country').on(table.country),
|
|
uniqueRule: (0, pg_core_1.uniqueIndex)('idx_geo_blocking_unique').on(table.country, table.region, table.city),
|
|
}));
|
|
// ============================================
|
|
// Inbox Publishing Tracking
|
|
// ============================================
|
|
exports.INBOX_FILE_TYPES = ['clip', 'scene', 'video'];
|
|
exports.publishedInboxFiles = (0, pg_core_1.pgTable)('published_inbox_files', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
sourceInboxPath: (0, pg_core_1.text)('source_inbox_path').notNull(), // Original path in inbox
|
|
filename: (0, pg_core_1.text)('filename').notNull(),
|
|
publishedAt: (0, pg_core_1.timestamp)('published_at', { withTimezone: true }).defaultNow(),
|
|
publishedToPath: (0, pg_core_1.text)('published_to_path'), // Destination path in public directory
|
|
publicMediaId: (0, pg_core_1.integer)('public_media_id').references(() => exports.publicMedia.id, { onDelete: 'set null' }),
|
|
fileType: (0, pg_core_1.text)('file_type').$type().notNull(),
|
|
}, (table) => ({
|
|
sourcePathIdx: (0, pg_core_1.index)('idx_published_inbox_source').on(table.sourceInboxPath),
|
|
fileTypeIdx: (0, pg_core_1.index)('idx_published_inbox_file_type').on(table.fileType),
|
|
}));
|
|
// ============================================
|
|
// Video OCR Results Table
|
|
// ============================================
|
|
exports.OCR_TEXT_TYPES = ['credits', 'title_card', 'overlay', 'unknown'];
|
|
exports.videoOcrResults = (0, pg_core_1.pgTable)('video_ocr_results', {
|
|
id: (0, pg_core_1.serial)('id').primaryKey(),
|
|
digestId: (0, pg_core_1.integer)('digest_id').notNull(),
|
|
videoId: (0, pg_core_1.integer)('video_id').notNull(),
|
|
frameIndex: (0, pg_core_1.integer)('frame_index').notNull(),
|
|
timestamp: (0, pg_core_1.real)('timestamp').notNull(),
|
|
extractedText: (0, pg_core_1.text)('extracted_text').notNull(),
|
|
confidence: (0, pg_core_1.real)('confidence'),
|
|
textType: (0, pg_core_1.text)('text_type').$type(),
|
|
sourceFrame: (0, pg_core_1.text)('source_frame').notNull(),
|
|
createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).defaultNow(),
|
|
}, (table) => ({
|
|
digestIdx: (0, pg_core_1.index)('idx_video_ocr_results_digest').on(table.digestId),
|
|
videoIdx: (0, pg_core_1.index)('idx_video_ocr_results_video').on(table.videoId),
|
|
}));
|
|
//# sourceMappingURL=schema.js.map
|