Initial commit
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function validateDatabaseIntegrity() {
|
||||
console.log('🔍 Running comprehensive database integrity check...');
|
||||
|
||||
try {
|
||||
// 1. Check Video table integrity
|
||||
console.log('\n📹 Checking Video table...');
|
||||
const videos = await prisma.video.findMany({
|
||||
include: {
|
||||
playlist: true,
|
||||
uploader: true,
|
||||
_count: {
|
||||
select: {
|
||||
comments: true,
|
||||
likes: true,
|
||||
progress: true,
|
||||
unlocks: true,
|
||||
watchSegments: true,
|
||||
videoCourses: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`✅ Found ${videos.length} videos`);
|
||||
|
||||
// 2. Check for videos with missing playlists
|
||||
const videosWithMissingPlaylists = videos.filter(v => !v.playlist);
|
||||
if (videosWithMissingPlaylists.length > 0) {
|
||||
console.log(`⚠️ Found ${videosWithMissingPlaylists.length} videos with missing playlists:`);
|
||||
videosWithMissingPlaylists.forEach(v => {
|
||||
console.log(` - Video ID: ${v.id}, Title: "${v.title}", Playlist ID: ${v.playlistId}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check for playlists with no videos
|
||||
console.log('\n📋 Checking Playlist table...');
|
||||
const playlists = await prisma.playlist.findMany({
|
||||
include: {
|
||||
videos: true,
|
||||
course: true,
|
||||
_count: {
|
||||
select: {
|
||||
videos: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`✅ Found ${playlists.length} playlists`);
|
||||
|
||||
const emptyPlaylists = playlists.filter(p => p.videos.length === 0);
|
||||
if (emptyPlaylists.length > 0) {
|
||||
console.log(`⚠️ Found ${emptyPlaylists.length} empty playlists:`);
|
||||
emptyPlaylists.forEach(p => {
|
||||
console.log(` - Playlist ID: ${p.id}, Title: "${p.title}", Course: ${p.course?.title || 'Unknown'}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Check VideoProgress integrity
|
||||
console.log('\n📊 Checking VideoProgress table...');
|
||||
const progressCount = await prisma.videoProgress.count();
|
||||
console.log(`✅ Found ${progressCount} progress records`);
|
||||
|
||||
// Check for progress records with invalid video references
|
||||
const invalidProgress = await prisma.$queryRaw`
|
||||
SELECT vp.id, vp."videoId", vp."userId"
|
||||
FROM "VideoProgress" vp
|
||||
LEFT JOIN "Video" v ON vp."videoId" = v.id
|
||||
WHERE v.id IS NULL
|
||||
` as Array<{id: string, videoId: string, userId: string}>;
|
||||
|
||||
if (invalidProgress.length > 0) {
|
||||
console.log(`❌ Found ${invalidProgress.length} progress records with invalid video references:`);
|
||||
invalidProgress.forEach(p => {
|
||||
console.log(` - Progress ID: ${p.id}, Video ID: ${p.videoId}, User ID: ${p.userId}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Check VideoWatchSegment integrity
|
||||
console.log('\n⏱️ Checking VideoWatchSegment table...');
|
||||
const segmentsCount = await prisma.videoWatchSegment.count();
|
||||
console.log(`✅ Found ${segmentsCount} watch segments`);
|
||||
|
||||
// Check for segments with invalid video references
|
||||
const invalidSegments = await prisma.$queryRaw`
|
||||
SELECT vws.id, vws."videoId", vws."userId"
|
||||
FROM "VideoWatchSegment" vws
|
||||
LEFT JOIN "Video" v ON vws."videoId" = v.id
|
||||
WHERE v.id IS NULL
|
||||
` as Array<{id: string, videoId: string, userId: string}>;
|
||||
|
||||
if (invalidSegments.length > 0) {
|
||||
console.log(`❌ Found ${invalidSegments.length} watch segments with invalid video references:`);
|
||||
invalidSegments.forEach(s => {
|
||||
console.log(` - Segment ID: ${s.id}, Video ID: ${s.videoId}, User ID: ${s.userId}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Check for duplicate video indexes within playlists
|
||||
console.log('\n🔄 Checking for duplicate video indexes...');
|
||||
for (const playlist of playlists) {
|
||||
if (playlist.videos.length > 0) {
|
||||
const indexes = playlist.videos.map(v => v.index);
|
||||
const uniqueIndexes = [...new Set(indexes)];
|
||||
if (indexes.length !== uniqueIndexes.length) {
|
||||
console.log(`⚠️ Playlist "${playlist.title}" has duplicate video indexes:`);
|
||||
const duplicates = indexes.filter((item, index) => indexes.indexOf(item) !== index);
|
||||
console.log(` - Duplicate indexes: ${duplicates.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Check Enrollment integrity
|
||||
console.log('\n📚 Checking Enrollment table...');
|
||||
const enrollments = await prisma.enrollment.findMany({
|
||||
include: {
|
||||
user: true,
|
||||
course: true
|
||||
}
|
||||
});
|
||||
console.log(`✅ Found ${enrollments.length} enrollments`);
|
||||
|
||||
const enrollmentsWithMissingUser = enrollments.filter(e => !e.user);
|
||||
const enrollmentsWithMissingCourse = enrollments.filter(e => !e.course);
|
||||
|
||||
if (enrollmentsWithMissingUser.length > 0) {
|
||||
console.log(`❌ Found ${enrollmentsWithMissingUser.length} enrollments with missing users`);
|
||||
}
|
||||
if (enrollmentsWithMissingCourse.length > 0) {
|
||||
console.log(`❌ Found ${enrollmentsWithMissingCourse.length} enrollments with missing courses`);
|
||||
}
|
||||
|
||||
// 8. Summary
|
||||
console.log('\n📋 Database Integrity Summary:');
|
||||
console.log(` Videos: ${videos.length}`);
|
||||
console.log(` Playlists: ${playlists.length} (${emptyPlaylists.length} empty)`);
|
||||
console.log(` Progress Records: ${progressCount}`);
|
||||
console.log(` Watch Segments: ${segmentsCount}`);
|
||||
console.log(` Enrollments: ${enrollments.length}`);
|
||||
|
||||
const hasIssues = videosWithMissingPlaylists.length > 0 ||
|
||||
invalidProgress.length > 0 ||
|
||||
invalidSegments.length > 0 ||
|
||||
enrollmentsWithMissingUser.length > 0 ||
|
||||
enrollmentsWithMissingCourse.length > 0;
|
||||
|
||||
if (!hasIssues) {
|
||||
console.log('✨ All integrity checks passed! Database appears healthy.');
|
||||
} else {
|
||||
console.log('⚠️ Some integrity issues found. See details above.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during integrity check:', error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the integrity check
|
||||
validateDatabaseIntegrity()
|
||||
.then(() => {
|
||||
console.log('🔧 Database integrity check completed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Integrity check failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function cleanupCompletedProgress() {
|
||||
console.log('🧹 Cleaning up progress data for completed videos...');
|
||||
|
||||
try {
|
||||
// Find all completed video progress records
|
||||
const completedProgress = await prisma.videoProgress.findMany({
|
||||
where: { completed: true },
|
||||
select: { id: true, userId: true, videoId: true },
|
||||
});
|
||||
|
||||
console.log(`📊 Found ${completedProgress.length} completed videos`);
|
||||
|
||||
if (completedProgress.length === 0) {
|
||||
console.log('✅ No completed videos to clean up');
|
||||
return;
|
||||
}
|
||||
|
||||
let totalSegmentsDeleted = 0;
|
||||
|
||||
// For each completed video, delete its watch segments
|
||||
// (we keep the VideoProgress record for history, but clean up the granular segment data)
|
||||
for (const progress of completedProgress) {
|
||||
const deletedSegments = await prisma.videoWatchSegment.deleteMany({
|
||||
where: {
|
||||
userId: progress.userId,
|
||||
videoId: progress.videoId,
|
||||
},
|
||||
});
|
||||
|
||||
totalSegmentsDeleted += deletedSegments.count;
|
||||
|
||||
if (deletedSegments.count > 0) {
|
||||
console.log(
|
||||
` ✓ Deleted ${deletedSegments.count} segments for video ${progress.videoId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📈 Cleanup Summary:`);
|
||||
console.log(` - Completed videos processed: ${completedProgress.length}`);
|
||||
console.log(` - Watch segments deleted: ${totalSegmentsDeleted}`);
|
||||
console.log(`✅ Cleanup complete!`);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('❌ Error during cleanup:', err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
cleanupCompletedProgress().catch((err) => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function cleanupOrphanedData() {
|
||||
console.log('🔍 Checking for orphaned video references...');
|
||||
|
||||
try {
|
||||
// Get all existing video IDs
|
||||
const existingVideos = await prisma.video.findMany({
|
||||
select: { id: true }
|
||||
});
|
||||
const existingVideoIds = new Set(existingVideos.map(v => v.id));
|
||||
console.log(`📹 Found ${existingVideoIds.size} existing videos`);
|
||||
|
||||
let totalCleaned = 0;
|
||||
|
||||
// 1. Clean orphaned VideoProgress records
|
||||
const allProgress = await prisma.videoProgress.findMany({
|
||||
select: { id: true, videoId: true, userId: true }
|
||||
});
|
||||
const orphanedProgress = allProgress.filter(p => !existingVideoIds.has(p.videoId));
|
||||
|
||||
if (orphanedProgress.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedProgress.length} orphaned VideoProgress records`);
|
||||
await prisma.videoProgress.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedProgress.map(p => p.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedProgress.length} orphaned VideoProgress records`);
|
||||
totalCleaned += orphanedProgress.length;
|
||||
}
|
||||
|
||||
// 2. Clean orphaned VideoWatchSegment records
|
||||
const allSegments = await prisma.videoWatchSegment.findMany({
|
||||
select: { id: true, videoId: true, userId: true }
|
||||
});
|
||||
const orphanedSegments = allSegments.filter(s => !existingVideoIds.has(s.videoId));
|
||||
|
||||
if (orphanedSegments.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedSegments.length} orphaned VideoWatchSegment records`);
|
||||
await prisma.videoWatchSegment.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedSegments.map(s => s.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedSegments.length} orphaned VideoWatchSegment records`);
|
||||
totalCleaned += orphanedSegments.length;
|
||||
}
|
||||
|
||||
// 3. Clean orphaned VideoUnlock records
|
||||
const allUnlocks = await prisma.videoUnlock.findMany({
|
||||
select: { id: true, videoId: true, userId: true }
|
||||
});
|
||||
const orphanedUnlocks = allUnlocks.filter(u => !existingVideoIds.has(u.videoId));
|
||||
|
||||
if (orphanedUnlocks.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedUnlocks.length} orphaned VideoUnlock records`);
|
||||
await prisma.videoUnlock.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedUnlocks.map(u => u.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedUnlocks.length} orphaned VideoUnlock records`);
|
||||
totalCleaned += orphanedUnlocks.length;
|
||||
}
|
||||
|
||||
// 4. Clean orphaned VideoLike records
|
||||
const allLikes = await prisma.videoLike.findMany({
|
||||
select: { id: true, videoId: true, userId: true }
|
||||
});
|
||||
const orphanedLikes = allLikes.filter(l => !existingVideoIds.has(l.videoId));
|
||||
|
||||
if (orphanedLikes.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedLikes.length} orphaned VideoLike records`);
|
||||
await prisma.videoLike.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedLikes.map(l => l.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedLikes.length} orphaned VideoLike records`);
|
||||
totalCleaned += orphanedLikes.length;
|
||||
}
|
||||
|
||||
// 5. Clean orphaned Comment records
|
||||
const allComments = await prisma.comment.findMany({
|
||||
select: { id: true, videoId: true, userId: true }
|
||||
});
|
||||
const orphanedComments = allComments.filter(c => !existingVideoIds.has(c.videoId));
|
||||
|
||||
if (orphanedComments.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedComments.length} orphaned Comment records`);
|
||||
await prisma.comment.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedComments.map(c => c.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedComments.length} orphaned Comment records`);
|
||||
totalCleaned += orphanedComments.length;
|
||||
}
|
||||
|
||||
// 6. Clean orphaned VideoCourse records
|
||||
const allVideoCourses = await prisma.videoCourse.findMany({
|
||||
select: { id: true, videoId: true, courseId: true }
|
||||
});
|
||||
const orphanedVideoCourses = allVideoCourses.filter(vc => !existingVideoIds.has(vc.videoId));
|
||||
|
||||
if (orphanedVideoCourses.length > 0) {
|
||||
console.log(`🧹 Found ${orphanedVideoCourses.length} orphaned VideoCourse records`);
|
||||
await prisma.videoCourse.deleteMany({
|
||||
where: {
|
||||
id: { in: orphanedVideoCourses.map(vc => vc.id) }
|
||||
}
|
||||
});
|
||||
console.log(`✅ Deleted ${orphanedVideoCourses.length} orphaned VideoCourse records`);
|
||||
totalCleaned += orphanedVideoCourses.length;
|
||||
}
|
||||
|
||||
// 7. Report summary
|
||||
if (totalCleaned === 0) {
|
||||
console.log('✨ No orphaned video references found! Database is clean.');
|
||||
} else {
|
||||
console.log(`🎉 Cleanup complete! Removed ${totalCleaned} total orphaned records.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during cleanup:', error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the cleanup
|
||||
cleanupOrphanedData()
|
||||
.then(() => {
|
||||
console.log('🔧 Cleanup script completed');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// scripts/update-thumbnail-urls.ts
|
||||
// Update all existing thumbnail URLs from /uploads/thumbnails/ to /api/thumbnails/
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log("Updating thumbnail URLs...");
|
||||
|
||||
const result = await prisma.$executeRawUnsafe(
|
||||
`UPDATE "Video"
|
||||
SET thumbnail = REPLACE(thumbnail, '/uploads/thumbnails/', '/api/thumbnails/')
|
||||
WHERE thumbnail IS NOT NULL
|
||||
AND thumbnail LIKE '/uploads/thumbnails/%'`
|
||||
);
|
||||
|
||||
console.log(`✓ Updated ${result} video records`);
|
||||
} catch (error) {
|
||||
console.error("Error updating thumbnail URLs:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user