فهرست منبع

Add most views of all time queries (#16)

Rafał Pawłow 3 سال پیش
والد
کامیت
3b473e0d45
6فایلهای تغییر یافته به همراه199 افزوده شده و 6 حذف شده
  1. 2 0
      .gitignore
  2. 9 0
      schema.graphql
  3. 35 3
      src/aggregates/views.ts
  4. 36 0
      src/resolvers/viewsInfo.ts
  5. 45 1
      tests/queries/views.ts
  6. 72 2
      tests/views.test.ts

+ 2 - 0
.gitignore

@@ -5,3 +5,5 @@ node_modules/
 dist/
 
 globalConfig.json
+
+.idea

+ 9 - 0
schema.graphql

@@ -43,12 +43,21 @@ type Query {
   """Get most viewed list of categories"""
   mostViewedCategories(limit: Int, period: Int!): [EntityViewsInfo!]
 
+  """Get most viewed list of categories of all time"""
+  mostViewedCategoriesAllTime(limit: Int!): [EntityViewsInfo!]
+
   """Get most viewed list of channels"""
   mostViewedChannels(limit: Int, period: Int!): [EntityViewsInfo!]
 
+  """Get most viewed list of channels of all time"""
+  mostViewedChannelsAllTime(limit: Int!): [EntityViewsInfo!]
+
   """Get most viewed list of videos"""
   mostViewedVideos(limit: Int, period: Int!): [EntityViewsInfo!]
 
+  """Get most viewed list of videos of all time"""
+  mostViewedVideosAllTime(limit: Int!): [EntityViewsInfo!]
+
   """Get views count for a single video"""
   videoViews(videoId: ID!): EntityViewsInfo
 }

+ 35 - 3
src/aggregates/views.ts

@@ -1,4 +1,5 @@
 import { UnsequencedVideoEvent, VideoEvent, VideoEventsBucketModel, VideoEventType } from '../models/VideoEvent'
+import { EntityViewsInfo } from '../entities/EntityViewsInfo'
 
 type VideoEventsAggregationResult = {
   events?: VideoEvent[]
@@ -9,6 +10,15 @@ export class ViewsAggregate {
   private channelViewsMap: Record<string, number> = {}
   private categoryViewsMap: Record<string, number> = {}
   private allViewsEvents: Partial<UnsequencedVideoEvent>[] = []
+  private allVideoViews: EntityViewsInfo[] = []
+  private allChannelViews: EntityViewsInfo[] = []
+  private allCategoryViews: EntityViewsInfo[] = []
+
+  private addOrUpdateViews(array: EntityViewsInfo[], id: string): void {
+    const i = array.findIndex((element) => element.id === id)
+    if (i > -1) array[i].views = array[i].views + 1
+    else array.push({ id, views: 1 })
+  }
 
   public videoViews(videoId: string): number | null {
     return this.videoViewsMap[videoId] ?? null
@@ -30,6 +40,18 @@ export class ViewsAggregate {
     return Object.freeze(this.channelViewsMap)
   }
 
+  public getAllVideoViews() {
+    return this.allVideoViews
+  }
+
+  public getAllChannelViews() {
+    return this.allChannelViews
+  }
+
+  public getAllCategoryViews() {
+    return this.allCategoryViews
+  }
+
   public static async Build() {
     const aggregation: VideoEventsAggregationResult = await VideoEventsBucketModel.aggregate([
       { $unwind: '$events' },
@@ -51,11 +73,21 @@ export class ViewsAggregate {
     const currentVideoViews = videoId ? this.videoViewsMap[videoId] || 0 : 0
     const currentChannelViews = channelId ? this.channelViewsMap[channelId] || 0 : 0
     const currentCategoryViews = categoryId ? this.categoryViewsMap[categoryId] || 0 : 0
+
     switch (type) {
       case VideoEventType.AddView:
-        if (videoId) this.videoViewsMap[videoId] = currentVideoViews + 1
-        if (channelId) this.channelViewsMap[channelId] = currentChannelViews + 1
-        if (categoryId) this.categoryViewsMap[categoryId] = currentCategoryViews + 1
+        if (videoId) {
+          this.addOrUpdateViews(this.allVideoViews, videoId)
+          this.videoViewsMap[videoId] = currentVideoViews + 1
+        }
+        if (channelId) {
+          this.addOrUpdateViews(this.allChannelViews, channelId)
+          this.channelViewsMap[channelId] = currentChannelViews + 1
+        }
+        if (categoryId) {
+          this.addOrUpdateViews(this.allCategoryViews, categoryId)
+          this.categoryViewsMap[categoryId] = currentCategoryViews + 1
+        }
         this.allViewsEvents = [...this.allViewsEvents, { videoId, channelId, categoryId, timestamp }]
         break
       default:

+ 36 - 0
src/resolvers/viewsInfo.ts

@@ -74,6 +74,14 @@ class AddVideoViewArgs {
   categoryId?: string
 }
 
+@ArgsType()
+class MostViewedAllTimeArgs {
+  @Min(1)
+  @Max(200)
+  @Field(() => Int)
+  limit: number
+}
+
 @Resolver()
 export class VideoViewsInfosResolver {
   @Query(() => EntityViewsInfo, { nullable: true, description: 'Get views count for a single video' })
@@ -97,6 +105,14 @@ export class VideoViewsInfosResolver {
     return mapMostViewedArray(buildMostViewedVideosArray(ctx, period), limit)
   }
 
+  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of videos of all time' })
+  async mostViewedVideosAllTime(
+    @Args() { limit }: MostViewedAllTimeArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<EntityViewsInfo[]> {
+    return sortAndLimitViews(ctx.viewsAggregate.getAllVideoViews(), limit)
+  }
+
   @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of channels' })
   async mostViewedChannels(
     @Args() { period, limit }: MostViewedChannelArgs,
@@ -105,6 +121,14 @@ export class VideoViewsInfosResolver {
     return mapMostViewedArray(buildMostViewedChannelsArray(ctx, period), limit)
   }
 
+  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of channels of all time' })
+  async mostViewedChannelsAllTime(
+    @Args() { limit }: MostViewedAllTimeArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<EntityViewsInfo[]> {
+    return sortAndLimitViews(ctx.viewsAggregate.getAllChannelViews(), limit)
+  }
+
   @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of categories' })
   async mostViewedCategories(
     @Args() { period, limit }: MostViewedCategoriesArgs,
@@ -113,6 +137,14 @@ export class VideoViewsInfosResolver {
     return mapMostViewedArray(buildMostViewedCategoriesArray(ctx, period), limit)
   }
 
+  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of categories of all time' })
+  async mostViewedCategoriesAllTime(
+    @Args() { limit }: MostViewedAllTimeArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<EntityViewsInfo[]> {
+    return sortAndLimitViews(ctx.viewsAggregate.getAllCategoryViews(), limit)
+  }
+
   @Query(() => EntityViewsInfo, { nullable: true, description: 'Get views count for a single channel' })
   async channelViews(
     @Args() { channelId }: ChannelViewsArgs,
@@ -158,6 +190,10 @@ const mapMostViewedArray = (views: Record<string, number>, limit?: number) =>
         .slice(0, limit)
     : []
 
+const sortAndLimitViews = (views: EntityViewsInfo[], limit: number) => {
+  return views.sort((a, b) => (a.views > b.views ? -1 : 1)).slice(0, limit)
+}
+
 const filterAllViewsByPeriod = (ctx: OrionContext, period: number): Partial<UnsequencedVideoEvent>[] => {
   const views = ctx.viewsAggregate.getAllViewsEvents()
   const filteredViews = []

+ 45 - 1
tests/queries/views.ts

@@ -18,6 +18,15 @@ export const GET_MOST_VIEWED_VIDEOS = gql`
     }
   }
 `
+
+export const GET_MOST_VIEWED_VIDEOS_ALL_TIME = gql`
+  query GetMostViewedVideosAllTime($limit: Int!) {
+    mostViewedVideosAllTime(limit: $limit) {
+      id
+      views
+    }
+  }
+`
 export type GetVideoViews = {
   videoViews: EntityViewsInfo | null
 }
@@ -30,6 +39,12 @@ export type GetVideoViewsArgs = {
 export type GetMostViewedVideosArgs = {
   period: number
 }
+export type GetMostViewedVideosAllTimeArgs = {
+  limit: number
+}
+export type GetMostViewedVideosAllTime = {
+  mostViewedVideosAllTime: EntityViewsInfo[]
+}
 
 export const GET_CHANNEL_VIEWS = gql`
   query GetChannelViews($channelId: ID!) {
@@ -48,6 +63,15 @@ export const GET_MOST_VIEWED_CHANNELS = gql`
     }
   }
 `
+
+export const GET_MOST_VIEWED_CHANNELS_ALL_TIME = gql`
+  query GetMostViewedVideosAllTime($limit: Int!) {
+    mostViewedChannelsAllTime(limit: $limit) {
+      id
+      views
+    }
+  }
+`
 export type GetChannelViews = {
   channelViews: EntityViewsInfo | null
 }
@@ -60,6 +84,12 @@ export type GetChannelViewsArgs = {
 export type GetMostViewedChannelsArgs = {
   period: number
 }
+export type GetMostViewedChannelsAllTimeArgs = {
+  limit: number
+}
+export type GetMostViewedChannelsAllTime = {
+  mostViewedChannelsAllTime: EntityViewsInfo[]
+}
 
 export const GET_MOST_VIEWED_CATEGORIES = gql`
   query GetMostViewedCategories($period: Int!) {
@@ -70,12 +100,26 @@ export const GET_MOST_VIEWED_CATEGORIES = gql`
   }
 `
 
+export const GET_MOST_VIEWED_CATEGORIES_ALL_TIME = gql`
+  query GetMostViewedVideosAllTime($limit: Int!) {
+    mostViewedCategoriesAllTime(limit: $limit) {
+      id
+      views
+    }
+  }
+`
 export type GetMostViewedCategories = {
   mostViewedCategories: EntityViewsInfo[]
 }
-export type GetMostViewedCategoriessArgs = {
+export type GetMostViewedCategoriesArgs = {
   period: number
 }
+export type GetMostViewedCategoriesAllTimeArgs = {
+  limit: number
+}
+export type GetMostViewedCategoriesAllTime = {
+  mostViewedCategoriesAllTime: EntityViewsInfo[]
+}
 
 export const ADD_VIDEO_VIEW = gql`
   mutation AddVideoView($videoId: ID!, $channelId: ID!, $categoryId: ID) {

+ 72 - 2
tests/views.test.ts

@@ -10,19 +10,28 @@ import {
   AddVideoViewArgs,
   GET_CHANNEL_VIEWS,
   GET_MOST_VIEWED_CHANNELS,
+  GET_MOST_VIEWED_CHANNELS_ALL_TIME,
   GET_VIDEO_VIEWS,
   GET_MOST_VIEWED_VIDEOS,
+  GET_MOST_VIEWED_VIDEOS_ALL_TIME,
   GET_MOST_VIEWED_CATEGORIES,
+  GET_MOST_VIEWED_CATEGORIES_ALL_TIME,
   GetChannelViews,
   GetChannelViewsArgs,
   GetVideoViews,
   GetVideoViewsArgs,
   GetMostViewedVideosArgs,
+  GetMostViewedVideosAllTimeArgs,
   GetMostViewedChannelsArgs,
+  GetMostViewedChannelsAllTimeArgs,
   GetMostViewedVideos,
+  GetMostViewedVideosAllTime,
   GetMostViewedChannels,
-  GetMostViewedCategoriessArgs,
+  GetMostViewedChannelsAllTime,
+  GetMostViewedCategoriesArgs,
+  GetMostViewedCategoriesAllTimeArgs,
   GetMostViewedCategories,
+  GetMostViewedCategoriesAllTime,
 } from './queries/views'
 import { EntityViewsInfo } from '../src/entities/EntityViewsInfo'
 import { VideoEventsBucketModel } from '../src/models/VideoEvent'
@@ -83,6 +92,15 @@ describe('Video and channel views resolver', () => {
     return mostViewedVideosResponse.data?.mostViewedVideos
   }
 
+  const getMostViewedVideosAllTime = async (limit: number) => {
+    const mostViewedVideosAllTimeResponse = await query<GetMostViewedVideosAllTime, GetMostViewedVideosAllTimeArgs>({
+      query: GET_MOST_VIEWED_VIDEOS_ALL_TIME,
+      variables: { limit },
+    })
+    expect(mostViewedVideosAllTimeResponse.errors).toBeUndefined()
+    return mostViewedVideosAllTimeResponse.data?.mostViewedVideosAllTime
+  }
+
   const getChannelViews = async (channelId: string) => {
     const channelViewsResponse = await query<GetChannelViews, GetChannelViewsArgs>({
       query: GET_CHANNEL_VIEWS,
@@ -101,8 +119,20 @@ describe('Video and channel views resolver', () => {
     return mostViewedChannelsResponse.data?.mostViewedChannels
   }
 
+  const getMostViewedChannelsAllTime = async (limit: number) => {
+    const mostViewedChannelsAllTimeResponse = await query<
+      GetMostViewedChannelsAllTime,
+      GetMostViewedChannelsAllTimeArgs
+    >({
+      query: GET_MOST_VIEWED_CHANNELS_ALL_TIME,
+      variables: { limit },
+    })
+    expect(mostViewedChannelsAllTimeResponse.errors).toBeUndefined()
+    return mostViewedChannelsAllTimeResponse.data?.mostViewedChannelsAllTime
+  }
+
   const getMostViewedCategories = async (period: number) => {
-    const mostViewedCategoriesResponse = await query<GetMostViewedCategories, GetMostViewedCategoriessArgs>({
+    const mostViewedCategoriesResponse = await query<GetMostViewedCategories, GetMostViewedCategoriesArgs>({
       query: GET_MOST_VIEWED_CATEGORIES,
       variables: { period },
     })
@@ -110,18 +140,36 @@ describe('Video and channel views resolver', () => {
     return mostViewedCategoriesResponse.data?.mostViewedCategories
   }
 
+  const getMostViewedCategoriesAllTime = async (limit: number) => {
+    const mostViewedCategoriesAllTimeResponse = await query<
+      GetMostViewedCategoriesAllTime,
+      GetMostViewedCategoriesAllTimeArgs
+    >({
+      query: GET_MOST_VIEWED_CATEGORIES_ALL_TIME,
+      variables: { limit },
+    })
+    expect(mostViewedCategoriesAllTimeResponse.errors).toBeUndefined()
+    return mostViewedCategoriesAllTimeResponse.data?.mostViewedCategoriesAllTime
+  }
+
   it('should return null for unknown video, channel and category views', async () => {
     const videoViews = await getVideoViews(FIRST_VIDEO_ID)
     const mostViewedVideos = await getMostViewedVideos(30)
+    const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
     const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
     const mostViewedChannels = await getMostViewedChannels(30)
+    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
     const mostViewedCategories = await getMostViewedCategories(30)
+    const mostViewedCategoriesAllTime = await getMostViewedCategoriesAllTime(10)
 
     expect(videoViews).toBeNull()
     expect(mostViewedVideos).toHaveLength(0)
+    expect(mostViewedVideosAllTime).toHaveLength(0)
     expect(channelViews).toBeNull()
     expect(mostViewedChannels).toHaveLength(0)
+    expect(mostViewedChannelsAllTime).toHaveLength(0)
     expect(mostViewedCategories).toHaveLength(0)
+    expect(mostViewedCategoriesAllTime).toHaveLength(0)
   })
 
   it('should properly save video and channel views', async () => {
@@ -140,15 +188,21 @@ describe('Video and channel views resolver', () => {
     const checkViews = async () => {
       const videoViews = await getVideoViews(FIRST_VIDEO_ID)
       const mostViewedVideos = await getMostViewedVideos(30)
+      const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
       const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
       const mostViewedChannels = await getMostViewedChannels(30)
+      const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
       const mostViewedCategories = await getMostViewedCategories(30)
+      const mostViewedCategoriesAllTime = await getMostViewedCategoriesAllTime(10)
 
       expect(videoViews).toEqual(expectedVideoViews)
       expect(mostViewedVideos).toEqual([expectedVideoViews])
+      expect(mostViewedVideosAllTime).toEqual([expectedVideoViews])
       expect(channelViews).toEqual(expectedChannelViews)
       expect(mostViewedChannels).toEqual([expectedChannelViews])
+      expect(mostViewedChannelsAllTime).toEqual([expectedChannelViews])
       expect(mostViewedCategories).toEqual([expectedCategoryViews])
+      expect(mostViewedCategoriesAllTime).toEqual([expectedCategoryViews])
     }
 
     let addVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
@@ -189,10 +243,12 @@ describe('Video and channel views resolver', () => {
     const firstVideoViews = await getVideoViews(FIRST_VIDEO_ID)
     const secondVideoViews = await getVideoViews(SECOND_VIDEO_ID)
     const mostViewedVideos = await getMostViewedVideos(30)
+    const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
 
     expect(firstVideoViews).toEqual(expectedFirstVideoViews)
     expect(secondVideoViews).toEqual(expectedSecondVideoViews)
     expect(mostViewedVideos).toEqual([expectedFirstVideoViews, expectedSecondVideoViews])
+    expect(mostViewedVideosAllTime).toEqual([expectedFirstVideoViews, expectedSecondVideoViews])
   })
 
   it('should distinct views of separate channels', async () => {
@@ -211,10 +267,12 @@ describe('Video and channel views resolver', () => {
     const firstChannelViews = await getChannelViews(FIRST_CHANNEL_ID)
     const secondChannelViews = await getChannelViews(SECOND_CHANNEL_ID)
     const mostViewedChannels = await getMostViewedChannels(30)
+    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
 
     expect(firstChannelViews).toEqual(expectedFirstChanelViews)
     expect(secondChannelViews).toEqual(expectedSecondChannelViews)
     expect(mostViewedChannels).toEqual([expectedFirstChanelViews, expectedSecondChannelViews])
+    expect(mostViewedChannelsAllTime).toEqual([expectedFirstChanelViews, expectedSecondChannelViews])
   })
 
   it('should properly aggregate views of a channel', async () => {
@@ -228,9 +286,11 @@ describe('Video and channel views resolver', () => {
 
     const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
     const mostViewedChannels = await getMostViewedChannels(30)
+    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
 
     expect(channelViews).toEqual(expectedChannelViews)
     expect(mostViewedChannels).toEqual([expectedChannelViews])
+    expect(mostViewedChannelsAllTime).toEqual([expectedChannelViews])
   })
 
   it('should properly aggregate views of a category', async () => {
@@ -243,8 +303,10 @@ describe('Video and channel views resolver', () => {
     await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
 
     const mostViewedCategories = await getMostViewedCategories(30)
+    const mostViewedCategoriesAllTime = await getMostViewedCategoriesAllTime(10)
 
     expect(mostViewedCategories).toEqual([expectedChannelViews])
+    expect(mostViewedCategoriesAllTime).toEqual([expectedChannelViews])
   })
 
   it('should properly rebuild the aggregate', async () => {
@@ -270,15 +332,21 @@ describe('Video and channel views resolver', () => {
       const secondVideoViews = await getVideoViews(SECOND_VIDEO_ID)
       const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
       const mostViewedVideos = await getMostViewedVideos(30)
+      const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
       const mostViewedChannels = await getMostViewedChannels(30)
+      const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
       const mostViewedCategories = await getMostViewedCategories(30)
+      const mostViewedCategoriesAllTime = await getMostViewedCategoriesAllTime(10)
 
       expect(firstVideoViews).toEqual(expectedFirstVideoViews)
       expect(secondVideoViews).toEqual(expectedSecondVideoViews)
       expect(mostViewedVideos).toEqual([expectedSecondVideoViews, expectedFirstVideoViews])
+      expect(mostViewedVideosAllTime).toEqual([expectedSecondVideoViews, expectedFirstVideoViews])
       expect(channelViews).toEqual(expectedChannelViews)
       expect(mostViewedChannels).toEqual([expectedChannelViews])
+      expect(mostViewedChannelsAllTime).toEqual([expectedChannelViews])
       expect(mostViewedCategories).toEqual([expectedCategoryViews])
+      expect(mostViewedCategoriesAllTime).toEqual([expectedCategoryViews])
     }
 
     await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
@@ -315,7 +383,9 @@ describe('Video and channel views resolver', () => {
 
     const videoViews = await getVideoViews(FIRST_VIDEO_ID)
     const mostViewedVideos = await getMostViewedVideos(30)
+    const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
     expect(videoViews).toEqual(expectedVideoViews)
     expect(mostViewedVideos).toEqual([expectedVideoViews])
+    expect(mostViewedVideosAllTime).toEqual([expectedVideoViews])
   })
 })