Преглед изворни кода

Extend add addVideoView by categoryId, add most viewed categories query

Rafal Pawlow пре 3 година
родитељ
комит
2e1671e7fa
6 измењених фајлова са 119 додато и 23 уклоњено
  1. 4 1
      schema.graphql
  2. 6 3
      src/aggregates/views.ts
  3. 3 0
      src/models/VideoEvent.ts
  4. 33 5
      src/resolvers/viewsInfo.ts
  5. 19 2
      tests/queries/views.ts
  6. 54 12
      tests/views.test.ts

+ 4 - 1
schema.graphql

@@ -15,7 +15,7 @@ type EntityViewsInfo {
 
 type Mutation {
   """Add a single view to the target video's count"""
-  addVideoView(channelId: ID!, videoId: ID!): EntityViewsInfo!
+  addVideoView(categoryId: ID, channelId: ID!, videoId: ID!): EntityViewsInfo!
 
   """Add a single follow to the target channel"""
   followChannel(channelId: ID!): ChannelFollowsInfo!
@@ -40,6 +40,9 @@ type Query {
   """Get views count for a single channel"""
   channelViews(channelId: ID!): EntityViewsInfo
 
+  """Get most viewed list of categories"""
+  mostViewedCategories(limit: Int, period: Int): [EntityViewsInfo!]
+
   """Get most viewed list of channels"""
   mostViewedChannels(limit: Int, period: Int): [EntityViewsInfo!]
 

+ 6 - 3
src/aggregates/views.ts

@@ -7,6 +7,7 @@ type VideoEventsAggregationResult = {
 export class ViewsAggregate {
   private videoViewsMap: Record<string, number> = {}
   private channelViewsMap: Record<string, number> = {}
+  private categoryViewsMap: Record<string, number> = {}
   private allViewsEvents: Partial<UnsequencedVideoEvent>[] = []
 
   public videoViews(videoId: string): number | null {
@@ -46,15 +47,17 @@ export class ViewsAggregate {
   }
 
   public applyEvent(event: UnsequencedVideoEvent) {
-    const { videoId, channelId, timestamp } = event
+    const { videoId, channelId, categoryId, timestamp } = event
     const currentVideoViews = this.videoViewsMap[event.videoId] || 0
     const currentChannelViews = this.channelViewsMap[event.channelId] || 0
-
+    const currentCategoryViews =
+      event.categoryId && this.categoryViewsMap[event.categoryId] ? this.categoryViewsMap[event.categoryId] : 0
     switch (event.type) {
       case VideoEventType.AddView:
         this.videoViewsMap[event.videoId] = currentVideoViews + 1
         this.channelViewsMap[event.channelId] = currentChannelViews + 1
-        this.allViewsEvents = [...this.allViewsEvents, { videoId, channelId, timestamp }]
+        if (event.categoryId) this.categoryViewsMap[event.categoryId] = currentCategoryViews + 1
+        this.allViewsEvents = [...this.allViewsEvents, { videoId, channelId, categoryId, timestamp }]
         break
       default:
         console.error(`Parsing unknown video event: ${event.type}`)

+ 3 - 0
src/models/VideoEvent.ts

@@ -12,6 +12,9 @@ export class VideoEvent extends GenericEvent {
   @prop({ required: true, index: true })
   channelId: string
 
+  @prop({ required: false, index: true })
+  categoryId?: string
+
   @prop({ required: true, index: true, enum: VideoEventType })
   type: VideoEventType
 }

+ 33 - 5
src/resolvers/viewsInfo.ts

@@ -46,6 +46,15 @@ class BatchedChannelViewsArgs {
   channelIdList: string[]
 }
 
+@ArgsType()
+class MostViewedCategoriesArgs {
+  @Field(() => Int, { nullable: true })
+  period?: number
+
+  @Field(() => Int, { nullable: true })
+  limit?: number
+}
+
 @ArgsType()
 class AddVideoViewArgs {
   @Field(() => ID)
@@ -53,6 +62,9 @@ class AddVideoViewArgs {
 
   @Field(() => ID)
   channelId: string
+
+  @Field(() => ID, { nullable: true })
+  categoryId?: string
 }
 
 @Resolver()
@@ -86,6 +98,14 @@ export class VideoViewsInfosResolver {
     return mapMostViewedArray(buildMostViewedChannelsArray(ctx, period), limit)
   }
 
+  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get most viewed list of categories' })
+  async mostViewedCategories(
+    @Args() { period, limit }: MostViewedCategoriesArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<EntityViewsInfo[]> {
+    return mapMostViewedArray(buildMostViewedCategoriesArray(ctx, period), limit)
+  }
+
   @Query(() => EntityViewsInfo, { nullable: true, description: 'Get views count for a single channel' })
   async channelViews(
     @Args() { channelId }: ChannelViewsArgs,
@@ -104,12 +124,13 @@ export class VideoViewsInfosResolver {
 
   @Mutation(() => EntityViewsInfo, { description: "Add a single view to the target video's count" })
   async addVideoView(
-    @Args() { videoId, channelId }: AddVideoViewArgs,
+    @Args() { videoId, channelId, categoryId }: AddVideoViewArgs,
     @Ctx() ctx: OrionContext
   ): Promise<EntityViewsInfo> {
     const event: UnsequencedVideoEvent = {
       videoId,
       channelId,
+      categoryId,
       type: VideoEventType.AddView,
       timestamp: new Date(),
       actorId: ctx.remoteHost,
@@ -123,10 +144,12 @@ export class VideoViewsInfosResolver {
 }
 
 const mapMostViewedArray = (views: Record<string, number>, limit?: number) =>
-  Object.keys(views)
-    .map((id) => ({ id, views: views[id] }))
-    .sort((a, b) => (a.views > b.views ? -1 : 1))
-    .slice(0, limit)
+  views
+    ? Object.keys(views)
+        .map((id) => ({ id, views: views[id] }))
+        .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()
@@ -149,6 +172,11 @@ const buildMostViewedChannelsArray = (ctx: OrionContext, period?: number) =>
     {}
   )
 
+const buildMostViewedCategoriesArray = (ctx: OrionContext, period?: number) =>
+  filterAllViewsByPeriod(ctx, period).reduce((entity: Record<string, number>, { categoryId }) => {
+    return categoryId ? { ...entity, [categoryId]: (entity[categoryId] || 0) + 1 } : entity
+  }, {})
+
 const buildViewsObject = (id: string, views: number | null): EntityViewsInfo | null => {
   if (views != null) {
     return {

+ 19 - 2
tests/queries/views.ts

@@ -61,9 +61,25 @@ export type GetMostViewedChannelsArgs = {
   period?: number
 }
 
+export const GET_MOST_VIEWED_CATEGORIES = gql`
+  query GetMostViewedCategories($period: Int) {
+    mostViewedCategories(period: $period) {
+      id
+      views
+    }
+  }
+`
+
+export type GetMostViewedCategories = {
+  mostViewedCategories: EntityViewsInfo[]
+}
+export type GetMostViewedCategoriessArgs = {
+  period?: number
+}
+
 export const ADD_VIDEO_VIEW = gql`
-  mutation AddVideoView($videoId: ID!, $channelId: ID!) {
-    addVideoView(videoId: $videoId, channelId: $channelId) {
+  mutation AddVideoView($videoId: ID!, $channelId: ID!, $categoryId: ID) {
+    addVideoView(videoId: $videoId, channelId: $channelId, categoryId: $categoryId) {
       id
       views
     }
@@ -75,4 +91,5 @@ export type AddVideoView = {
 export type AddVideoViewArgs = {
   videoId: string
   channelId: string
+  categoryId?: string
 }

+ 54 - 12
tests/views.test.ts

@@ -12,6 +12,7 @@ import {
   GET_MOST_VIEWED_CHANNELS,
   GET_VIDEO_VIEWS,
   GET_MOST_VIEWED_VIDEOS,
+  GET_MOST_VIEWED_CATEGORIES,
   GetChannelViews,
   GetChannelViewsArgs,
   GetVideoViews,
@@ -20,6 +21,8 @@ import {
   GetMostViewedChannelsArgs,
   GetMostViewedVideos,
   GetMostViewedChannels,
+  GetMostViewedCategoriessArgs,
+  GetMostViewedCategories,
 } from './queries/views'
 import { EntityViewsInfo } from '../src/entities/EntityViewsInfo'
 import { VideoEventsBucketModel } from '../src/models/VideoEvent'
@@ -29,6 +32,7 @@ const FIRST_VIDEO_ID = '12'
 const SECOND_VIDEO_ID = '13'
 const FIRST_CHANNEL_ID = '22'
 const SECOND_CHANNEL_ID = '23'
+const FIRST_CATEGORY_ID = '32'
 
 describe('Video and channel views resolver', () => {
   let server: ApolloServer
@@ -52,10 +56,10 @@ describe('Video and channel views resolver', () => {
     await mongoose.disconnect()
   })
 
-  const addVideoView = async (videoId: string, channelId: string) => {
+  const addVideoView = async (videoId: string, channelId: string, categoryId?: string) => {
     const addVideoViewResponse = await mutate<AddVideoView, AddVideoViewArgs>({
       mutation: ADD_VIDEO_VIEW,
-      variables: { videoId, channelId },
+      variables: { videoId, channelId, categoryId },
     })
     expect(addVideoViewResponse.errors).toBeUndefined()
     return addVideoViewResponse.data?.addVideoView
@@ -97,16 +101,27 @@ describe('Video and channel views resolver', () => {
     return mostViewedChannelsResponse.data?.mostViewedChannels
   }
 
-  it('should return null for unknown video and channel views', async () => {
+  const getMostViewedCategories = async (period?: number) => {
+    const mostViewedCategoriesResponse = await query<GetMostViewedCategories, GetMostViewedCategoriessArgs>({
+      query: GET_MOST_VIEWED_CATEGORIES,
+      variables: { period },
+    })
+    expect(mostViewedCategoriesResponse.errors).toBeUndefined()
+    return mostViewedCategoriesResponse.data?.mostViewedCategories
+  }
+
+  it('should return null for unknown video, channel and category views', async () => {
     const videoViews = await getVideoViews(FIRST_VIDEO_ID)
     const mostViewedVideos = await getMostViewedVideos()
     const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
     const mostViewedChannels = await getMostViewedChannels()
+    const mostViewedCategories = await getMostViewedCategories()
 
     expect(videoViews).toBeNull()
     expect(mostViewedVideos).toHaveLength(0)
     expect(channelViews).toBeNull()
     expect(mostViewedChannels).toHaveLength(0)
+    expect(mostViewedCategories).toHaveLength(0)
   })
 
   it('should properly save video and channel views', async () => {
@@ -118,27 +133,34 @@ describe('Video and channel views resolver', () => {
       id: FIRST_CHANNEL_ID,
       views: 1,
     }
+    const expectedCategoryViews: EntityViewsInfo = {
+      id: FIRST_CATEGORY_ID,
+      views: 1,
+    }
     const checkViews = async () => {
       const videoViews = await getVideoViews(FIRST_VIDEO_ID)
       const mostViewedVideos = await getMostViewedVideos()
       const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
       const mostViewedChannels = await getMostViewedChannels()
+      const mostViewedCategories = await getMostViewedCategories()
 
       expect(videoViews).toEqual(expectedVideoViews)
       expect(mostViewedVideos).toEqual([expectedVideoViews])
       expect(channelViews).toEqual(expectedChannelViews)
       expect(mostViewedChannels).toEqual([expectedChannelViews])
+      expect(mostViewedCategories).toEqual([expectedCategoryViews])
     }
 
-    let addVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
+    let addVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
     expect(addVideoViewData).toEqual(expectedVideoViews)
 
     await checkViews()
 
     expectedVideoViews.views++
     expectedChannelViews.views++
+    expectedCategoryViews.views++
 
-    addVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
+    addVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
     expect(addVideoViewData).toEqual(expectedVideoViews)
 
     await checkViews()
@@ -211,6 +233,20 @@ describe('Video and channel views resolver', () => {
     expect(mostViewedChannels).toEqual([expectedChannelViews])
   })
 
+  it('should properly aggregate views of a category', async () => {
+    const expectedChannelViews: EntityViewsInfo = {
+      id: FIRST_CATEGORY_ID,
+      views: 2,
+    }
+
+    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+
+    const mostViewedCategories = await getMostViewedCategories()
+
+    expect(mostViewedCategories).toEqual([expectedChannelViews])
+  })
+
   it('should properly rebuild the aggregate', async () => {
     const expectedFirstVideoViews: EntityViewsInfo = {
       id: FIRST_VIDEO_ID,
@@ -224,6 +260,10 @@ describe('Video and channel views resolver', () => {
       id: FIRST_CHANNEL_ID,
       views: 7,
     }
+    const expectedCategoryViews: EntityViewsInfo = {
+      id: FIRST_CATEGORY_ID,
+      views: 7,
+    }
 
     const checkViews = async () => {
       const firstVideoViews = await getVideoViews(FIRST_VIDEO_ID)
@@ -231,22 +271,24 @@ describe('Video and channel views resolver', () => {
       const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
       const mostViewedVideos = await getMostViewedVideos()
       const mostViewedChannels = await getMostViewedChannels()
+      const mostViewedCategories = await getMostViewedCategories()
 
       expect(firstVideoViews).toEqual(expectedFirstVideoViews)
       expect(secondVideoViews).toEqual(expectedSecondVideoViews)
       expect(mostViewedVideos).toEqual([expectedSecondVideoViews, expectedFirstVideoViews])
       expect(channelViews).toEqual(expectedChannelViews)
       expect(mostViewedChannels).toEqual([expectedChannelViews])
+      expect(mostViewedCategories).toEqual([expectedCategoryViews])
     }
 
-    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
-    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
-    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
+    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
 
-    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
-    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
-    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
-    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
+    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
+    await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID, FIRST_CATEGORY_ID)
 
     await checkViews()