import { ApolloError, UserInputError } from 'apollo-server'; import { PlatformId } from '../../../database/types/members'; import { memcache, Videos } from '../../modules'; import { ChannelId } from '../../modules/types/youtube'; import { VideoStatus } from '../../server/apis/youtube/types'; import { cutGroupString, escapeRegex, firstField, getCacheKey, getNextToken, parseOrganization, parseToken, Sort } from './consts'; interface SortBy { published?: Sort; scheduled?: Sort; start?: Sort; } interface VideoQuery { channel_id: ChannelId[]; status: VideoStatus[]; title: string; organizations: string[]; exclude_organizations: string[]; platforms: PlatformId[]; max_upcoming_mins: number; order_by: SortBy; page_token: string; limit: number; } export async function videos(_, query: VideoQuery) { try { const { channel_id = [], status = [], title, organizations = [], exclude_organizations = [], platforms = [], max_upcoming_mins, page_token = '', limit } = query; if (limit < 1 || limit > 50) { return new UserInputError('limit must be between 1-50 inclusive.'); } if (max_upcoming_mins < 0 || max_upcoming_mins > 2880) { return new UserInputError('max_upcoming_mins must be between 0-2880 inclusive.'); } if (organizations.length && exclude_organizations.length) { return new UserInputError('Setting both organizations and exclude_organizations is redundant. Only choose one.'); } const EXCLUDE_ORG = !organizations.length; const MAX_UPCOMING = max_upcoming_mins * 6e4; const TITLE = title && escapeRegex(title); const ORGANIZATIONS = parseOrganization(EXCLUDE_ORG ? exclude_organizations : organizations); const [ORDER_BY, ORDER_BY_KEY] = firstField(query.order_by); const [ORDER_KEY, ORDER_VALUE] = Object.entries(ORDER_BY)[0]; const orderBy = { [`time.${ORDER_KEY}`]: ORDER_VALUE }; const CACHE_KEY = getCacheKey(`VIDS:${+EXCLUDE_ORG}${cutGroupString(ORGANIZATIONS)}${channel_id}${status}${TITLE}${platforms}${max_upcoming_mins}${ORDER_BY_KEY}${limit}${page_token}`); const cached = await memcache.get(CACHE_KEY); if (cached) return cached; const QUERY: any = { // any because typescript gets mad for some reason. status: status[0] ? { $in: status } : { $ne: 'missing' }, ...channel_id[0] && { channel_id: { $in: channel_id } }, ...TITLE && { title: { $regex: TITLE, $options: 'i' } }, ...ORGANIZATIONS[0] && { organization: { ...EXCLUDE_ORG ? { $not: { $regex: ORGANIZATIONS, $options: 'i' } } : { $regex: ORGANIZATIONS, $options: 'i' } } }, ...platforms[0] && { platform_id: { $in: platforms } }, ...max_upcoming_mins && { 'time.scheduled': { $lte: Date.now() + MAX_UPCOMING } } }; const getVideoCount = Videos.countDocuments(QUERY); const getUncachedVideos = Videos .find({ ...QUERY, ...page_token && { [Object.keys(orderBy)[0]]: { [ORDER_VALUE === 'asc' ? '$gte' : '$lte']: parseToken(page_token) } }, }) .sort(orderBy) .limit(limit + 1) .lean() .exec(); const [videoCount, uncachedVideos] = await Promise.all([getVideoCount, getUncachedVideos]); const results = { items: uncachedVideos, next_page_token: null, page_info: { total_results: videoCount, results_per_page: limit } }; const hasNextPage = uncachedVideos.length > limit && results.items.pop(); if (hasNextPage) { const token = hasNextPage.time[ORDER_KEY]; results.next_page_token = getNextToken(token); } memcache.set(CACHE_KEY, results, 60); return results; } catch(err) { throw new ApolloError(err); } }