// box-nestjs-monorepo/apps/box-mgnt-api/src/dev/services/video-cache-coverage.service.ts import { Injectable, Logger } from '@nestjs/common'; import { RedisService } from '@box/db/redis/redis.service'; import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service'; import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider'; /** * Response structure for video cache coverage check */ export interface VideoCacheCoverageResponse { totalChannels: number; totalCategories: number; totalTags: number; missingCategoryVideoLists: string[]; missingTagVideoLists: string[]; missingTagMetadataLists: string[]; } /** * Service for checking video cache coverage. * Dev-only: verifies that all expected cache keys exist in Redis. * * Compares database structure (channels, categories, tags) with actual Redis keys * to identify missing cache entries. * * Smart detection: * - Only reports missing category video list if videos exist and Redis key is missing * - Only reports missing tag video list if videos exist and Redis key is missing * - Tag metadata lists always checked (expected if category has tags) */ @Injectable() export class VideoCacheCoverageService { private readonly logger = new Logger(VideoCacheCoverageService.name); constructor( private readonly redis: RedisService, private readonly mongoPrisma: MongoPrismaService, ) {} /** * Check video cache coverage across all channels, categories, and tags. * Identifies missing category lists, tag lists, and tag metadata lists. * * SMART MISSING-KEY DETECTION: * - Only reports category video list as missing if: videoCount > 0 AND Redis key missing * - Only reports tag video list as missing if: videoCount > 0 AND Redis key missing * - Tag metadata lists are checked regardless (always expected if tags exist) * * This avoids false positives for empty categories/tags with no videos. */ async checkCoverage(): Promise { this.logger.log('[Coverage] Starting video cache coverage check'); const missingCategoryVideoLists: string[] = []; const missingTagVideoLists: string[] = []; const missingTagMetadataLists: string[] = []; let totalChannels = 0; let totalCategories = 0; let totalTags = 0; // Get all channels const channels = await this.mongoPrisma.channel.findMany(); totalChannels = channels.length; this.logger.debug(`[Coverage] Found ${totalChannels} channels`); for (const channel of channels) { // Get all enabled categories (no longer tied to channel) const categories = await this.mongoPrisma.category.findMany({ where: { status: 1 }, }); for (const category of categories) { totalCategories++; // Count videos in this category (using same filter as builder) const categoryVideoCount = await this.mongoPrisma.videoMedia.count({ where: { categoryIds: { has: category.id }, status: 'Completed', // listStatus: 1, // Only "on shelf" videos }, }); // Check category video list cache key ONLY if there are videos const categoryListKey = tsCacheKeys.video.categoryList(category.id); const categoryListExists = await this.redis.exists(categoryListKey); if (categoryVideoCount > 0 && !categoryListExists) { missingCategoryVideoLists.push(categoryListKey); this.logger.warn( `[Coverage] Missing category list: ${categoryListKey} (videos: ${categoryVideoCount})`, ); } else if (categoryVideoCount === 0) { this.logger.debug( `[Coverage] Empty category OK (no videos, no cache required): ${categoryListKey}`, ); } // Check tag metadata list cache key for this category const tagMetadataKey = tsCacheKeys.tag.metadataByCategory(category.id); const tagMetadataExists = await this.redis.exists(tagMetadataKey); if (!tagMetadataExists) { missingTagMetadataLists.push(tagMetadataKey); this.logger.warn( `[Coverage] Missing tag metadata list: ${tagMetadataKey}`, ); } // Get all enabled tags for this category (no longer filtered by channel) const tags = await this.mongoPrisma.tag.findMany({ where: { status: 1, categoryId: category.id, }, }); for (const tag of tags) { totalTags++; // Count videos with this tag in this category (same filter as builder) const tagVideoCount = await this.mongoPrisma.videoMedia.count({ where: { categoryIds: { has: category.id }, status: 'Completed', // listStatus: 1, // Only "on shelf" videos // tagIds: { has: tag.id }, // Has this specific tag }, }); // Check tag-filtered video list cache key ONLY if there are videos const tagListKey = tsCacheKeys.video.tagList(category.id, tag.id); const tagListExists = await this.redis.exists(tagListKey); if (tagVideoCount > 0 && !tagListExists) { missingTagVideoLists.push(tagListKey); this.logger.warn( `[Coverage] Missing tag video list: ${tagListKey} (videos: ${tagVideoCount})`, ); } else if (tagVideoCount === 0) { this.logger.debug( `[Coverage] Empty tag OK (no videos, no cache required): ${tagListKey}`, ); } } } } const totalMissing = missingCategoryVideoLists.length + missingTagVideoLists.length + missingTagMetadataLists.length; this.logger.log( `[Coverage] Complete: channels=${totalChannels} categories=${totalCategories} tags=${totalTags} missing=${totalMissing}`, ); if (totalMissing > 0) { this.logger.warn( `[Coverage] Found ${totalMissing} missing cache keys (categoryLists: ${missingCategoryVideoLists.length}, tagLists: ${missingTagVideoLists.length}, tagMetadata: ${missingTagMetadataLists.length})`, ); } else { this.logger.log('[Coverage] All expected cache keys exist'); } return { totalChannels, totalCategories, totalTags, missingCategoryVideoLists, missingTagVideoLists, missingTagMetadataLists, }; } }