Переглянути джерело

add support for featured content (#23)

* add support for video hero

* update packages

* add support for featured videos

* update existing tests to work with upgraded packages

* use ID type for IDs

* add tests for featured content resolver

* update linting packages, disable null assertion rule
Klaudiusz Dembler 3 роки тому
батько
коміт
1c927f077c

+ 2 - 4
.eslintrc.js

@@ -1,7 +1,8 @@
 module.exports = {
-  extends: ['@joystream/eslint-config'],
+  extends: ['@joystream/eslint-config', 'plugin:jest/recommended'],
   rules: {
     '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
     '@typescript-eslint/naming-convention': [
       'error',
       {
@@ -37,7 +38,4 @@ module.exports = {
     ],
   },
   plugins: ['jest'],
-  env: {
-    'jest/globals': true,
-  },
 }

+ 1 - 0
.gitignore

@@ -8,3 +8,4 @@ globalConfig.json
 
 .idea
 db/
+.env

+ 25 - 26
package.json

@@ -30,44 +30,43 @@
     ]
   },
   "dependencies": {
-    "@typegoose/auto-increment": "^0.6.0",
-    "@typegoose/typegoose": "^7.4.1",
-    "apollo-server-express": "^2.19.1",
-    "class-validator": "^0.12.2",
-    "date-fns": "^2.22.1",
-    "dotenv": "^8.2.0",
+    "@typegoose/auto-increment": "^1.0.0",
+    "@typegoose/typegoose": "^9.1.0",
+    "apollo-server-core": "^3.4.0",
+    "apollo-server-express": "^3.4.0",
+    "class-validator": "^0.13.1",
+    "date-fns": "^2.25.0",
+    "dotenv": "^10.0.0",
     "express": "^4.17.1",
     "graphql": "15",
-    "mongodb": "^3.6.3",
-    "mongoose": "^5.10.7",
+    "mongodb": "^4.1.3",
+    "mongoose": "^6.0.10",
     "reflect-metadata": "^0.1.13",
-    "type-graphql": "^1.0.0"
+    "type-graphql": "^1.1.1"
   },
   "devDependencies": {
     "@joystream/eslint-config": "^1.0.0",
     "@joystream/prettier-config": "^1.0.0",
-    "@shelf/jest-mongodb": "^1.2.3",
-    "@types/express": "^4.17.8",
-    "@types/jest": "^26.0.19",
-    "@types/mongoose": "^5.7.36",
+    "@shelf/jest-mongodb": "^2.1.0",
+    "@types/express": "^4.17.13",
+    "@types/jest": "^27.0.2",
     "@types/node": "^14.11.2",
-    "@typescript-eslint/eslint-plugin": "^4.10.0",
-    "@typescript-eslint/parser": "^4.10.0",
-    "apollo-server-testing": "^2.19.1",
-    "eslint": "^7.10.0",
-    "eslint-plugin-jest": "^24.1.3",
+    "@typescript-eslint/eslint-plugin": "^5.0.0",
+    "@typescript-eslint/parser": "^5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-jest": "^25.2.1",
     "husky": "^4.3.0",
-    "jest": "^26.6.3",
+    "jest": "^27.2.5",
     "lint-staged": "^10.4.0",
-    "prettier": "^2.1.2",
-    "ts-jest": "^26.4.4",
-    "ts-node": "^9.0.0",
-    "ts-node-dev": "^1.0.0-pre.63",
-    "typescript": "^4.0.3"
+    "prettier": "^2.4.1",
+    "ts-jest": "^27.0.5",
+    "ts-node": "^10.3.0",
+    "ts-node-dev": "^1.1.8",
+    "typescript": "^4.4.4"
   },
   "resolutions": {
-    "@typescript-eslint/eslint-plugin": "^4.10.0",
-    "@typescript-eslint/parser": "^4.10.0"
+    "@typescript-eslint/eslint-plugin": "^5.0.0",
+    "@typescript-eslint/parser": "^5.0.0"
   },
   "engines": {
     "node": ">=12"

+ 32 - 0
schema.graphql

@@ -3,6 +3,11 @@
 # !!!   DO NOT MODIFY THIS FILE BY YOURSELF   !!!
 # -----------------------------------------------
 
+type CategoryFeaturedVideos {
+  categoryId: ID!
+  videos: [FeaturedVideo!]!
+}
+
 type ChannelFollowsInfo {
   follows: Int!
   id: ID!
@@ -13,18 +18,33 @@ type EntityViewsInfo {
   views: Int!
 }
 
+type FeaturedVideo {
+  videoCutUrl: String
+  videoId: ID!
+}
+
+input FeaturedVideoInput {
+  videoCutUrl: String
+  videoId: ID!
+}
+
 type Mutation {
   """Add a single view to the target video's count"""
   addVideoView(categoryId: ID, channelId: ID!, videoId: ID!): EntityViewsInfo!
 
   """Add a single follow to the target channel"""
   followChannel(channelId: ID!): ChannelFollowsInfo!
+  setCategoryFeaturedVideos(categoryId: ID!, videos: [FeaturedVideoInput!]!): [FeaturedVideo!]!
+  setVideoHero(heroTitle: String!, heroVideoCutUrl: String!, videoId: ID!): VideoHero!
 
   """Remove a single follow from the target channel"""
   unfollowChannel(channelId: ID!): ChannelFollowsInfo!
 }
 
 type Query {
+  """Get featured videos for all categories"""
+  allCategoriesFeaturedVideos: [CategoryFeaturedVideos!]!
+
   """Get follows counts for a list of channels"""
   batchedChannelFollows(channelIdList: [ID!]!): [ChannelFollowsInfo]!
 
@@ -34,6 +54,9 @@ type Query {
   """Get views counts for a list of videos"""
   batchedVideoViews(videoIdList: [ID!]!): [EntityViewsInfo]!
 
+  """Get featured videos for a given video category"""
+  categoryFeaturedVideos(categoryId: ID!): [FeaturedVideo!]!
+
   """Get follows count for a single channel"""
   channelFollows(channelId: ID!): ChannelFollowsInfo
 
@@ -84,6 +107,15 @@ type Query {
   """Get list of most viewed videos of all time"""
   mostViewedVideosAllTime(limit: Int!): [EntityViewsInfo!]
 
+  """Get current video hero"""
+  videoHero: VideoHero!
+
   """Get views count for a single video"""
   videoViews(videoId: ID!): EntityViewsInfo
 }
+
+type VideoHero {
+  heroTitle: String!
+  heroVideoCutUrl: String!
+  videoId: ID!
+}

+ 8 - 1
src/config.ts

@@ -6,7 +6,7 @@ type LoadEnvVarOpts = {
   defaultValue?: string
   devDefaultValue?: string
 }
-const loadEnvVar = (name: string, { defaultValue, devDefaultValue }: LoadEnvVarOpts): string => {
+const loadEnvVar = (name: string, { defaultValue, devDefaultValue }: LoadEnvVarOpts = {}): string => {
   const value = process.env[name]
   if (value) {
     return value
@@ -27,6 +27,7 @@ export class Config {
   private _port: number
   private _bucketSize: number
   private _mongoDBUri: string
+  private _featuredContentSecret: string
 
   get port(): number {
     return this._port
@@ -40,6 +41,10 @@ export class Config {
     return this._mongoDBUri
   }
 
+  get featuredContentSecret(): string {
+    return this._featuredContentSecret
+  }
+
   loadConfig() {
     dotenv.config()
 
@@ -54,6 +59,8 @@ export class Config {
     const mongoDatabase = loadEnvVar('ORION_MONGO_DATABASE', { defaultValue: 'orion' })
 
     this._mongoDBUri = `mongodb://${mongoHostname}:${rawMongoPort}/${mongoDatabase}`
+
+    this._featuredContentSecret = loadEnvVar('FEATURED_CONTENT_SECRET')
   }
 }
 

+ 11 - 0
src/entities/CategoryFeaturedVideos.ts

@@ -0,0 +1,11 @@
+import { Field, ID, ObjectType } from 'type-graphql'
+import { FeaturedVideo } from '../models/FeaturedContent'
+
+@ObjectType()
+export class CategoryFeaturedVideos {
+  @Field(() => ID)
+  categoryId!: string
+
+  @Field(() => [FeaturedVideo])
+  videos!: FeaturedVideo[]
+}

+ 7 - 0
src/helpers/auth.ts

@@ -0,0 +1,7 @@
+import { AuthChecker } from 'type-graphql'
+import { OrionContext } from '../types'
+import config from '../config'
+
+export const customAuthChecker: AuthChecker<OrionContext> = ({ context }) => {
+  return context.authorization === config.featuredContentSecret
+}

+ 1 - 0
src/main.ts

@@ -12,6 +12,7 @@ const main = async () => {
   const aggregates = await wrapTask('Rebuilding aggregates', buildAggregates)
 
   const server = await createServer(mongoose, aggregates)
+  await server.start()
   const app = Express()
   server.applyMiddleware({ app })
 

+ 59 - 0
src/models/FeaturedContent.ts

@@ -0,0 +1,59 @@
+import { DocumentType, getModelForClass, prop } from '@typegoose/typegoose'
+import { ArgsType, Field, ID, ObjectType } from 'type-graphql'
+import { WhatIsIt } from '@typegoose/typegoose/lib/internal/constants'
+
+@ObjectType()
+@ArgsType()
+export class VideoHero {
+  @prop({ required: true })
+  @Field(() => ID)
+  videoId!: string
+
+  @prop({ required: true })
+  @Field()
+  heroTitle!: string
+
+  @prop({ required: true })
+  @Field()
+  heroVideoCutUrl!: string
+}
+
+@ObjectType()
+export class FeaturedVideo {
+  @prop({ required: true })
+  @Field(() => ID)
+  videoId!: string
+
+  @prop()
+  @Field({ nullable: true })
+  videoCutUrl?: string
+}
+
+export class FeaturedContent {
+  @prop({ required: true })
+  videoHero!: VideoHero
+
+  @prop({ required: true, type: () => [FeaturedVideo], _id: false }, WhatIsIt.MAP)
+  featuredVideosPerCategory!: Map<string, FeaturedVideo[]>
+}
+
+export const FeaturedContentModel = getModelForClass(FeaturedContent, {
+  schemaOptions: { collection: 'featuredContent' },
+})
+
+export const DEFAULT_FEATURED_CONTENT_DOC: FeaturedContent = {
+  videoHero: {
+    videoId: '0',
+    heroTitle: 'Change Me',
+    heroVideoCutUrl: 'https://google.com',
+  },
+  featuredVideosPerCategory: new Map<string, FeaturedVideo[]>(),
+}
+
+export const getFeaturedContentDoc = async (): Promise<DocumentType<FeaturedContent>> => {
+  const document = await FeaturedContentModel.findOne()
+  if (!document) {
+    return await FeaturedContentModel.create(DEFAULT_FEATURED_CONTENT_DOC)
+  }
+  return document
+}

+ 70 - 0
src/resolvers/featuredContent.ts

@@ -0,0 +1,70 @@
+import { Arg, Args, ArgsType, Authorized, Field, ID, InputType, Mutation, Query, Resolver } from 'type-graphql'
+import { FeaturedVideo, getFeaturedContentDoc, VideoHero } from '../models/FeaturedContent'
+import { CategoryFeaturedVideos } from '../entities/CategoryFeaturedVideos'
+
+@InputType()
+class FeaturedVideoInput implements FeaturedVideo {
+  @Field(() => ID)
+  videoId!: string
+
+  @Field({ nullable: true })
+  videoCutUrl?: string
+}
+
+@ArgsType()
+class SetCategoryFeaturedVideoArgs {
+  @Field(() => ID)
+  categoryId!: string
+
+  @Field(() => [FeaturedVideoInput])
+  videos!: FeaturedVideoInput[]
+}
+
+@Resolver()
+export class FeaturedContentResolver {
+  @Query(() => VideoHero, { nullable: false, description: 'Get current video hero' })
+  async videoHero() {
+    return (await getFeaturedContentDoc()).videoHero
+  }
+
+  @Query(() => [FeaturedVideo], { nullable: false, description: 'Get featured videos for a given video category' })
+  async categoryFeaturedVideos(@Arg('categoryId', () => ID) categoryId: string) {
+    const featuredContent = await getFeaturedContentDoc()
+    return featuredContent.featuredVideosPerCategory.get(categoryId) || []
+  }
+
+  @Query(() => [CategoryFeaturedVideos], { nullable: false, description: 'Get featured videos for all categories' })
+  async allCategoriesFeaturedVideos() {
+    const featuredContent = await getFeaturedContentDoc()
+
+    const categoriesList: CategoryFeaturedVideos[] = []
+    featuredContent.featuredVideosPerCategory.forEach((videos, categoryId) => {
+      categoriesList.push({
+        categoryId,
+        videos,
+      })
+    })
+
+    return categoriesList
+  }
+
+  @Mutation(() => VideoHero, { nullable: false })
+  @Authorized()
+  async setVideoHero(@Args() newVideoHero: VideoHero) {
+    const featuredContent = await getFeaturedContentDoc()
+    featuredContent.videoHero = newVideoHero
+    await featuredContent.save()
+
+    return newVideoHero
+  }
+
+  @Mutation(() => [FeaturedVideo], { nullable: false })
+  @Authorized()
+  async setCategoryFeaturedVideos(@Args() { categoryId, videos }: SetCategoryFeaturedVideoArgs) {
+    const featuredContent = await getFeaturedContentDoc()
+    featuredContent.featuredVideosPerCategory.set(categoryId, videos)
+    await featuredContent.save()
+
+    return videos
+  }
+}

+ 8 - 7
src/server.ts

@@ -1,19 +1,22 @@
 import 'reflect-metadata'
 import { ApolloServer } from 'apollo-server-express'
 import { ExpressContext } from 'apollo-server-express/dist/ApolloServer'
-import { ContextFunction } from 'apollo-server-core'
+import { ContextFunction, ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
 import { connect, Mongoose } from 'mongoose'
 import { buildSchema } from 'type-graphql'
 
 import { FollowsAggregate, ViewsAggregate } from './aggregates'
 import { ChannelFollowsInfosResolver, VideoViewsInfosResolver } from './resolvers'
 import { Aggregates, OrionContext } from './types'
+import { FeaturedContentResolver } from './resolvers/featuredContent'
+import { customAuthChecker } from './helpers/auth'
 
 export const createServer = async (mongoose: Mongoose, aggregates: Aggregates) => {
   await mongoose.connection
 
   const schema = await buildSchema({
-    resolvers: [VideoViewsInfosResolver, ChannelFollowsInfosResolver],
+    resolvers: [VideoViewsInfosResolver, ChannelFollowsInfosResolver, FeaturedContentResolver],
+    authChecker: customAuthChecker,
     emitSchemaFile: 'schema.graphql',
     validate: true,
   })
@@ -21,20 +24,18 @@ export const createServer = async (mongoose: Mongoose, aggregates: Aggregates) =
   const contextFn: ContextFunction<ExpressContext, OrionContext> = ({ req }) => ({
     ...aggregates,
     remoteHost: req?.ip,
+    authorization: req?.header('Authorization'),
   })
 
   return new ApolloServer({
     schema,
     context: contextFn,
+    plugins: [ApolloServerPluginLandingPageGraphQLPlayground],
   })
 }
 
 export const connectMongoose = async (connectionUri: string) => {
-  const mongoose = await connect(connectionUri, {
-    useUnifiedTopology: true,
-    useNewUrlParser: true,
-    useCreateIndex: true,
-  })
+  const mongoose = await connect(connectionUri)
   await mongoose.connection
   return mongoose
 }

+ 1 - 0
src/types.d.ts → src/types.ts

@@ -7,4 +7,5 @@ export type Aggregates = {
 
 export type OrionContext = {
   remoteHost?: string
+  authorization?: string
 } & Aggregates

+ 149 - 0
tests/featuredContent.test.ts

@@ -0,0 +1,149 @@
+import { ApolloServer } from 'apollo-server-express'
+import { Mongoose } from 'mongoose'
+import { Aggregates } from '../src/types'
+import { createMutationFn, createQueryFn, MutationFn, QueryFn } from './helpers'
+import { buildAggregates, connectMongoose, createServer } from '../src/server'
+import {
+  GET_ALL_CATEGORIES_FEATURED_VIDEOS,
+  GET_CATEGORY_FEATURED_VIDEOS,
+  GET_VIDEO_HERO,
+  GetAllCategoriesFeaturedVideos,
+  GetCategoryFeaturedVideos,
+  GetCategoryFeaturedVideosArgs,
+  GetVideoHero,
+  SET_CATEGORY_FEATURED_VIDEOS,
+  SET_VIDEO_HERO,
+  SetCategoryFeaturedVideos,
+  SetCategoryFeaturedVideosArgs,
+  SetVideoHero,
+  SetVideoHeroArgs,
+} from './queries/featuredContent'
+import {
+  DEFAULT_FEATURED_CONTENT_DOC,
+  FeaturedContentModel,
+  FeaturedVideo,
+  VideoHero,
+} from '../src/models/FeaturedContent'
+
+describe('Featured content resolver', () => {
+  let server: ApolloServer
+  let mongoose: Mongoose
+  let aggregates: Aggregates
+  let query: QueryFn
+  let mutate: MutationFn
+
+  beforeEach(async () => {
+    mongoose = await connectMongoose(process.env.MONGO_URL!)
+    aggregates = await buildAggregates()
+    server = await createServer(mongoose, aggregates)
+    await server.start()
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
+  })
+
+  afterEach(async () => {
+    await server.stop()
+    await FeaturedContentModel.deleteMany({})
+    await mongoose.disconnect()
+  })
+
+  const getVideoHero = async () => {
+    const result = await query<GetVideoHero>({
+      query: GET_VIDEO_HERO,
+    })
+    expect(result.errors).toBeUndefined()
+    return result.data?.videoHero
+  }
+
+  const getCategoryFeaturedVideos = async (categoryId: string) => {
+    const result = await query<GetCategoryFeaturedVideos, GetCategoryFeaturedVideosArgs>({
+      query: GET_CATEGORY_FEATURED_VIDEOS,
+      variables: { categoryId },
+    })
+    expect(result.errors).toBeUndefined()
+    return result.data?.categoryFeaturedVideos
+  }
+
+  const getAllCategoriesFeaturedVideos = async () => {
+    const result = await query<GetAllCategoriesFeaturedVideos>({
+      query: GET_ALL_CATEGORIES_FEATURED_VIDEOS,
+    })
+    expect(result.errors).toBeUndefined()
+    return result.data?.allCategoriesFeaturedVideos
+  }
+
+  it("should return default video hero if it wasn't set", async () => {
+    const videoHero = await getVideoHero()
+    expect(videoHero).toEqual(DEFAULT_FEATURED_CONTENT_DOC.videoHero)
+  })
+
+  it('should return empty array of featured videos for unknown category id', async () => {
+    const featuredVideos = await getCategoryFeaturedVideos('1')
+    expect(featuredVideos).toHaveLength(0)
+  })
+
+  it('should return empty array for list of all categories with featured videos', async () => {
+    const allCategoriesFeaturedVideos = await getAllCategoriesFeaturedVideos()
+    expect(allCategoriesFeaturedVideos).toHaveLength(0)
+  })
+
+  it('should set video hero', async () => {
+    const newVideoHero: VideoHero = {
+      videoId: '1111',
+      heroTitle: 'Hello darkness my old friend',
+      heroVideoCutUrl: 'example_url',
+    }
+    await mutate<SetVideoHero, SetVideoHeroArgs>({
+      mutation: SET_VIDEO_HERO,
+      variables: { ...newVideoHero },
+    })
+
+    const videoHero = await getVideoHero()
+    expect(videoHero).toEqual(newVideoHero)
+  })
+
+  it('should set featured videos for a given category', async () => {
+    const newFeaturedVideos: FeaturedVideo[] = [
+      { videoId: '1', videoCutUrl: 'test_url' },
+      { videoId: '2', videoCutUrl: 'another_url' },
+    ]
+    await mutate<SetCategoryFeaturedVideos, SetCategoryFeaturedVideosArgs>({
+      mutation: SET_CATEGORY_FEATURED_VIDEOS,
+      variables: { categoryId: '3', videos: newFeaturedVideos },
+    })
+
+    const featuredVideos = await getCategoryFeaturedVideos('3')
+    expect(featuredVideos).toEqual(newFeaturedVideos)
+  })
+
+  it('should return all categories that have featured videos set', async () => {
+    const category1FeaturedVideos: FeaturedVideo[] = [
+      { videoId: '1', videoCutUrl: 'test_url' },
+      { videoId: '2', videoCutUrl: 'another_url' },
+    ]
+    const category2FeaturedVideos: FeaturedVideo[] = [
+      { videoId: '3', videoCutUrl: 'url_test' },
+      { videoId: '4', videoCutUrl: 'url_another' },
+    ]
+    await mutate<SetCategoryFeaturedVideos, SetCategoryFeaturedVideosArgs>({
+      mutation: SET_CATEGORY_FEATURED_VIDEOS,
+      variables: { categoryId: '1', videos: category1FeaturedVideos },
+    })
+    await mutate<SetCategoryFeaturedVideos, SetCategoryFeaturedVideosArgs>({
+      mutation: SET_CATEGORY_FEATURED_VIDEOS,
+      variables: { categoryId: '2', videos: category2FeaturedVideos },
+    })
+
+    const allCategoriesFeaturedVideos = await getAllCategoriesFeaturedVideos()
+    expect(allCategoriesFeaturedVideos).toEqual([
+      {
+        categoryId: '1',
+        videos: category1FeaturedVideos,
+      },
+      {
+        categoryId: '2',
+        videos: category2FeaturedVideos,
+      },
+    ])
+  })
+})

+ 8 - 10
tests/follows.test.ts

@@ -1,9 +1,7 @@
 import { ApolloServer } from 'apollo-server-express'
 import { Mongoose } from 'mongoose'
 import { Aggregates } from '../src/types'
-import { ApolloServerTestClient } from 'apollo-server-testing/dist/createTestClient'
 import { buildAggregates, connectMongoose, createServer } from '../src/server'
-import { createTestClient } from 'apollo-server-testing'
 import {
   FOLLOW_CHANNEL,
   FollowChannel,
@@ -24,6 +22,7 @@ import {
 import { ChannelFollowsInfo } from '../src/entities/ChannelFollowsInfo'
 import { ChannelEventsBucketModel } from '../src/models/ChannelEvent'
 import { TEST_BUCKET_SIZE } from './setup'
+import { createMutationFn, createQueryFn, MutationFn, QueryFn } from './helpers'
 
 const FIRST_CHANNEL_ID = '22'
 const SECOND_CHANNEL_ID = '23'
@@ -32,16 +31,16 @@ describe('Channel follows resolver', () => {
   let server: ApolloServer
   let mongoose: Mongoose
   let aggregates: Aggregates
-  let query: ApolloServerTestClient['query']
-  let mutate: ApolloServerTestClient['mutate']
+  let query: QueryFn
+  let mutate: MutationFn
 
   beforeEach(async () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
     server = await createServer(mongoose, aggregates)
-    const testClient = createTestClient(server)
-    query = testClient.query
-    mutate = testClient.mutate
+    await server.start()
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
   })
 
   afterEach(async () => {
@@ -259,9 +258,8 @@ describe('Channel follows resolver', () => {
     await server.stop()
     aggregates = await buildAggregates()
     server = await createServer(mongoose, aggregates)
-    const testClient = createTestClient(server)
-    query = testClient.query
-    mutate = testClient.mutate
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
 
     await checkFollows()
   })

+ 33 - 0
tests/helpers.ts

@@ -0,0 +1,33 @@
+import { ApolloServer } from 'apollo-server-express'
+import { GraphQLResponse } from 'apollo-server-core'
+
+type TypedGraphQLResponse<TResult> = GraphQLResponse & {
+  data?: TResult | null
+}
+
+export const createQueryFn = (server: ApolloServer) => {
+  type QueryOpts<TVars> = {
+    query: Parameters<typeof server.executeOperation>[0]['query']
+    // eslint-disable-next-line @typescript-eslint/ban-types
+  } & (TVars extends undefined ? {} : { variables: TVars })
+
+  return async <TResult, TVars = undefined>(opts: QueryOpts<TVars>) => {
+    const result = await server.executeOperation(opts)
+    return result as TypedGraphQLResponse<TResult>
+  }
+}
+
+export const createMutationFn = (server: ApolloServer) => {
+  type MutationOpts<TVars> = {
+    mutation: Parameters<typeof server.executeOperation>[0]['query']
+    variables: TVars
+  }
+
+  return async <TResult, TVars>(opts: MutationOpts<TVars>) => {
+    const result = await server.executeOperation({ ...opts, query: opts.mutation })
+    return result as TypedGraphQLResponse<TResult>
+  }
+}
+
+export type QueryFn = ReturnType<typeof createQueryFn>
+export type MutationFn = ReturnType<typeof createMutationFn>

+ 80 - 0
tests/queries/featuredContent.ts

@@ -0,0 +1,80 @@
+import { gql } from 'apollo-server-express'
+import { FeaturedVideo, VideoHero } from '../../src/models/FeaturedContent'
+import { CategoryFeaturedVideos } from '../../src/entities/CategoryFeaturedVideos'
+
+export const GET_VIDEO_HERO = gql`
+  query GetVideoHero {
+    videoHero {
+      heroTitle
+      heroVideoCutUrl
+      videoId
+    }
+  }
+`
+export type GetVideoHero = {
+  videoHero: VideoHero | null
+}
+
+export const GET_CATEGORY_FEATURED_VIDEOS = gql`
+  query GetCategoryFeaturedVideos($categoryId: ID!) {
+    categoryFeaturedVideos(categoryId: $categoryId) {
+      videoId
+      videoCutUrl
+    }
+  }
+`
+export type GetCategoryFeaturedVideos = {
+  categoryFeaturedVideos: FeaturedVideo[] | null
+}
+export type GetCategoryFeaturedVideosArgs = {
+  categoryId: string
+}
+
+export const GET_ALL_CATEGORIES_FEATURED_VIDEOS = gql`
+  query GetAllCategoriesFeaturedVideos {
+    allCategoriesFeaturedVideos {
+      categoryId
+      videos {
+        videoId
+        videoCutUrl
+      }
+    }
+  }
+`
+export type GetAllCategoriesFeaturedVideos = {
+  allCategoriesFeaturedVideos: CategoryFeaturedVideos[] | null
+}
+
+export const SET_VIDEO_HERO = gql`
+  mutation SetVideoHero($videoId: ID!, $heroTitle: String!, $heroVideoCutUrl: String!) {
+    setVideoHero(videoId: $videoId, heroTitle: $heroTitle, heroVideoCutUrl: $heroVideoCutUrl) {
+      videoId
+      heroTitle
+      heroVideoCutUrl
+    }
+  }
+`
+export type SetVideoHero = {
+  setVideoHero: VideoHero
+}
+export type SetVideoHeroArgs = {
+  videoId: string
+  heroTitle: string
+  heroVideoCutUrl: string
+}
+
+export const SET_CATEGORY_FEATURED_VIDEOS = gql`
+  mutation SetCategoryFeaturedVideos($categoryId: ID!, $videos: [FeaturedVideoInput!]!) {
+    setCategoryFeaturedVideos(categoryId: $categoryId, videos: $videos) {
+      videoId
+      videoCutUrl
+    }
+  }
+`
+export type SetCategoryFeaturedVideos = {
+  setCategoryFeaturedVideos: FeaturedVideo[]
+}
+export type SetCategoryFeaturedVideosArgs = {
+  categoryId: string
+  videos: FeaturedVideo[]
+}

+ 1 - 0
tests/server.test.ts

@@ -12,6 +12,7 @@ describe('The server', () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
     server = await createServer(mongoose, aggregates)
+    await server.start()
   })
 
   afterEach(async () => {

+ 8 - 10
tests/views.test.ts

@@ -1,9 +1,7 @@
 import { ApolloServer } from 'apollo-server-express'
 import { Mongoose } from 'mongoose'
 import { Aggregates } from '../src/types'
-import { ApolloServerTestClient } from 'apollo-server-testing/dist/createTestClient'
 import { buildAggregates, connectMongoose, createServer } from '../src/server'
-import { createTestClient } from 'apollo-server-testing'
 import {
   ADD_VIDEO_VIEW,
   AddVideoView,
@@ -36,6 +34,7 @@ import {
 import { EntityViewsInfo } from '../src/entities/EntityViewsInfo'
 import { VideoEventsBucketModel } from '../src/models/VideoEvent'
 import { TEST_BUCKET_SIZE } from './setup'
+import { createMutationFn, createQueryFn, MutationFn, QueryFn } from './helpers'
 
 const FIRST_VIDEO_ID = '12'
 const SECOND_VIDEO_ID = '13'
@@ -47,16 +46,16 @@ describe('Video and channel views resolver', () => {
   let server: ApolloServer
   let mongoose: Mongoose
   let aggregates: Aggregates
-  let query: ApolloServerTestClient['query']
-  let mutate: ApolloServerTestClient['mutate']
+  let query: QueryFn
+  let mutate: MutationFn
 
   beforeEach(async () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
     server = await createServer(mongoose, aggregates)
-    const testClient = createTestClient(server)
-    query = testClient.query
-    mutate = testClient.mutate
+    await server.start()
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
   })
 
   afterEach(async () => {
@@ -363,9 +362,8 @@ describe('Video and channel views resolver', () => {
     await server.stop()
     aggregates = await buildAggregates()
     server = await createServer(mongoose, aggregates)
-    const testClient = createTestClient(server)
-    query = testClient.query
-    mutate = testClient.mutate
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
 
     await checkViews()
   })

Різницю між файлами не показано, бо вона завелика
+ 473 - 235
yarn.lock


Деякі файли не було показано, через те що забагато файлів було змінено