Просмотр исходного кода

🚀 Introduce data stiching, make Orion into a proper gateway (#29)

* add data stiching

* remove operation names

* remove unnecessary packages, organize helpers, refactor

* more cleaning

* add types

* move queryNodeStichingResolvers to resolvers

* add createSelectionSetAppendingTransform

* add resolvers for mostViewed and mostFollowed

* remove console.logs, refactor

* fix tests

* use loaders, remove executor

* refactor createSelectionSetAppendingTransform

* working example with directives

* remove directives

* simplify videohero resolver

* create resolvers for category featured content

* remove cross-undici-fetch

* add category to CategoryFeaturedVideos

* fix categoryFeaturedVideos query

* fix featuredContent tests

* remove queryNodeFollows and queryNodeViews

* add query node url to docker compose

* update docker

* use env variable in tests

* improve folder structure

* remove views and follows transforms

* remove transforms

* remove redundant queries

* remove old tests

* remove wrong resolvers

* remove redundant resolvers, add sorting by follows, use selectionSet in resolvers

* add sorting by views functionality

* remove channels and channelsConnectionResolver, add promisingNewChannels and discoverNewChannels query

* bring back VideoHero in orion schema

* move featured queries back to orion

* fix featuredContent tests

* remove console.log, fix tests

* add args to discoverNewChannels and promisingNewChannels

* update comment

* filter videos and channels without no views/follows

* remove redundant test

* return 0 if views/follows are null

* update config

* update tests (#2)

* get videosConnection from mostViewedVideos

* update mostViewedVideos and mostViewedVideosAllTime

* bring back limit

* use channelsConnection in mostFollowedChannels and mostViewedChannels

* remove async from views/follows resolvers

* add field resolution tracing

* fix TS error, update node

* fix debugging

* bring back executor with logger

* remove promisingNewChannels and discoverNewChannels query

* bring back loader for remote schema, remove redundant packages

* fix follows tests

* fix views tests

* increase node version

* update queries, clean up resolver logic

* update tests to use new queries

* add resolver for promising channels

* fix issue with not getting views or follows

* add discover/promising channels resolvers

* add popularChannels query, update schema types

* naming

Co-authored-by: drillprop <drylbartosz@gmail.com>
Klaudiusz Dembler 2 лет назад
Родитель
Сommit
1e98837edc

+ 1 - 1
.github/workflows/checks.yml

@@ -8,7 +8,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node-version: [12.x]
+        node-version: [16.x]
       fail-fast: true
     steps:
       - uses: actions/checkout@v2

+ 1 - 1
.github/workflows/tests.yml

@@ -8,7 +8,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node-version: [12.x]
+        node-version: [16.x]
       fail-fast: true
     steps:
       - uses: actions/checkout@v2

+ 1 - 0
.gitignore

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

+ 1 - 12
Dockerfile

@@ -1,19 +1,8 @@
-FROM node:14.18 AS build
-
+FROM node:16.13
 WORKDIR /usr/src/orion
 
 COPY . .
 RUN yarn install --frozen-lockfile
 RUN yarn run build
 
-
-FROM node:14.18
-
-WORKDIR /usr/src/orion
-
-COPY package.json .
-COPY yarn.lock .
-COPY --from=build /usr/src/orion/dist dist/
-RUN yarn install --frozen-lockfile --production
-
 CMD ["yarn", "start"]

+ 1 - 1
docker-compose.yml

@@ -6,11 +6,11 @@ services:
       - ORION_PORT=6116
       - ORION_MONGO_HOSTNAME=mongo
       - ORION_FEATURED_CONTENT_SECRET=change_me_please
+      - ORION_QUERY_NODE_URL=change_me_please
     ports:
       - '127.0.0.1:6116:6116'
     restart: always
   mongo:
-    container_name: mongo
     restart: always
     image: library/mongo:4.4
     volumes:

+ 8 - 1
package.json

@@ -30,6 +30,11 @@
     ]
   },
   "dependencies": {
+    "@graphql-tools/graphql-file-loader": "^7.3.3",
+    "@graphql-tools/load": "^7.4.1",
+    "@graphql-tools/schema": "^8.3.1",
+    "@graphql-tools/stitch": "^8.4.1",
+    "@graphql-tools/url-loader": "^7.5.2",
     "@typegoose/auto-increment": "^1.0.0",
     "@typegoose/typegoose": "^9.1.0",
     "apollo-server-core": "^3.4.0",
@@ -39,6 +44,7 @@
     "dotenv": "^10.0.0",
     "express": "^4.17.1",
     "graphql": "15",
+    "lodash": "^4.17.21",
     "mongodb": "^4.1.3",
     "mongoose": "^6.0.10",
     "reflect-metadata": "^0.1.13",
@@ -50,6 +56,7 @@
     "@shelf/jest-mongodb": "^2.1.0",
     "@types/express": "^4.17.13",
     "@types/jest": "^27.0.2",
+    "@types/lodash": "^4.14.178",
     "@types/node": "^14.11.2",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
@@ -70,6 +77,6 @@
     "@types/serve-static": "1.13.10"
   },
   "engines": {
-    "node": ">=12"
+    "node": "^16.13.1"
   }
 }

+ 125 - 0
queryNodeSchemaExtension.graphql

@@ -0,0 +1,125 @@
+extend type VideoHero {
+  video: Video!
+}
+
+extend type FeaturedVideo {
+  video: Video!
+}
+
+extend type CategoryFeaturedVideos {
+  category: VideoCategory!
+}
+
+extend type Video {
+  views: Int!
+}
+
+extend type Channel {
+  views: Int!
+  follows: Int!
+}
+
+type Query {
+  # ===== Videos =====
+
+  """
+  Get list of 10 most watched videos in last week
+  """
+  top10VideosThisWeek(where: VideoWhereInput): [Video!]!
+
+  """
+  Get list of 10 most watched videos in last month
+  """
+  top10VideosThisMonth(where: VideoWhereInput): [Video!]!
+
+  """
+  Get connection of most viewed videos in a given period or of all time
+  """
+  mostViewedVideosConnection(
+    """
+    `periodDays` indicates from which time period the views should be taken from. Can be 7 or 30.
+    If not provided, views from all time will be used.
+    """
+    periodDays: Int
+
+    """
+    `limit` indicates on how many videos the connection should be capped.
+    """
+    limit: Int!
+
+    first: Int
+    after: String
+    last: Int
+    before: String
+    where: VideoWhereInput
+    orderBy: [VideoOrderByInput!]
+  ): VideoConnection!
+
+  # ===== Channels =====
+
+  """
+  Get list of 15 most followed channels out of 100 newest channels in random order
+  """
+  discoverChannels(where: ChannelWhereInput): [Channel!]!
+
+  """
+  Get list of 15 most watched channels out of 100 newest channels in random order
+  """
+  promisingChannels(where: ChannelWhereInput): [Channel!]!
+
+  """
+  Get list of 15 most watched channels in random order
+  """
+  popularChannels(where: ChannelWhereInput): [Channel!]!
+
+  """
+  Get list of 10 most followed channels of all time
+  """
+  top10Channels(where: ChannelWhereInput): [Channel!]!
+
+  """
+  Get connection of most followed channels in a given period or of all time
+  """
+  mostFollowedChannelsConnection(
+    """
+    `periodDays` indicates from which time period the follows should be taken from. Can be 7 or 30.
+    If not provided, follows from all time will be used.
+    """
+    periodDays: Int
+
+    """
+    `limit` indicates on how many channels the connection should be capped.
+    """
+    limit: Int!
+
+    first: Int
+    after: String
+    last: Int
+    before: String
+    where: ChannelWhereInput
+    orderBy: [ChannelOrderByInput!]
+  ): ChannelConnection!
+
+  """
+  Get connection of most viewed channels in a given period or of all time
+  """
+  mostViewedChannelsConnection(
+    """
+    `periodDays` indicates from which time period the views should be taken from. Can be 7 or 30.
+    If not provided, views from all time will be used.
+    """
+    periodDays: Int
+
+    """
+    `limit` indicates on how many channels the connection should be capped.
+    """
+    limit: Int!
+
+    first: Int
+    after: String
+    last: Int
+    before: String
+    where: ChannelWhereInput
+    orderBy: [ChannelOrderByInput!]
+  ): ChannelConnection!
+}

+ 0 - 51
schema.graphql

@@ -45,35 +45,9 @@ type Query {
   """Get featured videos for all categories"""
   allCategoriesFeaturedVideos: [CategoryFeaturedVideos!]!
 
-  """Get follows counts for a list of channels"""
-  batchedChannelFollows(channelIdList: [ID!]!): [ChannelFollowsInfo]!
-
-  """Get views counts for a list of channels"""
-  batchedChannelsViews(channelIdList: [ID!]!): [EntityViewsInfo]!
-
-  """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
-
-  """Get views count for a single channel"""
-  channelViews(channelId: ID!): EntityViewsInfo
-
-  """Get list of most followed channels"""
-  mostFollowedChannels(
-    limit: Int
-
-    """timePeriodDays must take one of the following values: 7, 30"""
-    timePeriodDays: Int!
-  ): [ChannelFollowsInfo!]!
-
-  """Get list of most followed channels of all time"""
-  mostFollowedChannelsAllTime(limit: Int!): [ChannelFollowsInfo!]
-
   """Get list of most viewed categories in a given time period"""
   mostViewedCategories(
     limit: Int
@@ -85,33 +59,8 @@ type Query {
   """Get list of most viewed categories of all time"""
   mostViewedCategoriesAllTime(limit: Int!): [EntityViewsInfo!]
 
-  """Get list of most viewed channels in a given time period"""
-  mostViewedChannels(
-    limit: Int
-
-    """timePeriodDays must take one of the following values: 7, 30"""
-    timePeriodDays: Int!
-  ): [EntityViewsInfo!]
-
-  """Get list of most viewed channels of all time"""
-  mostViewedChannelsAllTime(limit: Int!): [EntityViewsInfo!]
-
-  """Get list of most viewed videos in a given time period"""
-  mostViewedVideos(
-    limit: Int
-
-    """timePeriodDays must take one of the following values: 7, 30"""
-    timePeriodDays: Int!
-  ): [EntityViewsInfo!]
-
-  """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 {

+ 13 - 0
src/config.ts

@@ -28,6 +28,8 @@ export class Config {
   private _bucketSize: number
   private _mongoDBUri: string
   private _featuredContentSecret: string
+  private _queryNodeUrl: string
+  private _isDebugging: boolean
 
   get port(): number {
     return this._port
@@ -45,6 +47,14 @@ export class Config {
     return this._featuredContentSecret
   }
 
+  get queryNodeUrl(): string {
+    return this._queryNodeUrl
+  }
+
+  get isDebugging(): boolean {
+    return this._isDebugging
+  }
+
   loadConfig() {
     dotenv.config()
 
@@ -61,6 +71,9 @@ export class Config {
     this._mongoDBUri = `mongodb://${mongoHostname}:${rawMongoPort}/${mongoDatabase}`
 
     this._featuredContentSecret = loadEnvVar('ORION_FEATURED_CONTENT_SECRET')
+    this._queryNodeUrl = loadEnvVar('ORION_QUERY_NODE_URL')
+
+    this._isDebugging = loadEnvVar('ORION_DEBUGGING', { defaultValue: 'false' }) === 'true'
   }
 }
 

+ 12 - 0
src/helpers/data.ts

@@ -0,0 +1,12 @@
+type HasId = {
+  id: string
+}
+
+export const createLookup = <T extends HasId>(data: T[]): Record<string, T> => {
+  return data.reduce((acc, item) => {
+    if (item) {
+      acc[item.id] = item
+    }
+    return acc
+  }, {} as Record<string, T>)
+}

+ 3 - 1
src/helpers/index.ts

@@ -1 +1,3 @@
-export { mapPeriods } from './period'
+export * from './period'
+export * from './data'
+export * from './auth'

+ 7 - 2
src/main.ts

@@ -11,14 +11,19 @@ const main = async () => {
 
   const aggregates = await wrapTask('Rebuilding aggregates', buildAggregates)
 
-  const server = await createServer(mongoose, aggregates)
+  const server = await createServer(mongoose, aggregates, config.queryNodeUrl)
   await server.start()
   const app = Express()
   server.applyMiddleware({ app })
 
   app.enable('trust proxy')
   app.listen({ port: config.port }, () =>
-    console.log(`🚀 Server listening at ==> http://localhost:${config.port}${server.graphqlPath}`)
+    console.log(`
+        🚀 Orion online
+        Mongo => ${config.mongoDBUri}
+        Query node => ${config.queryNodeUrl}
+        Playground => http://localhost:${config.port}${server.graphqlPath}
+      `)
   )
 }
 

+ 2 - 2
src/models/ChannelEvent.ts

@@ -11,14 +11,14 @@ export class ChannelEvent extends GenericEvent {
   channelId: string
 
   @prop({ required: true, index: true, enum: ChannelEventType })
-  type: ChannelEventType
+  declare type: ChannelEventType
 }
 
 export type UnsequencedChannelEvent = Omit<ChannelEvent, '_id'>
 
 class ChannelEventsBucket extends GenericBucket {
   @prop({ required: true, type: () => [ChannelEvent] })
-  events: ChannelEvent[]
+  declare events: ChannelEvent[]
 }
 
 export const ChannelEventsBucketModel = getModelForClass(ChannelEventsBucket, {

+ 2 - 2
src/models/VideoEvent.ts

@@ -16,14 +16,14 @@ export class VideoEvent extends GenericEvent {
   categoryId?: string
 
   @prop({ required: true, index: true, enum: VideoEventType })
-  type: VideoEventType
+  declare type: VideoEventType
 }
 
 export type UnsequencedVideoEvent = Omit<VideoEvent, '_id'>
 
 class VideoEventsBucket extends GenericBucket {
   @prop({ required: true, type: () => [VideoEvent] })
-  events: VideoEvent[]
+  declare events: VideoEvent[]
 }
 
 export const VideoEventsBucketModel = getModelForClass(VideoEventsBucket, {

+ 6 - 6
src/resolvers/featuredContent.ts

@@ -1,6 +1,6 @@
 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'
+import { FeaturedVideo, getFeaturedContentDoc, VideoHero } from '../models/FeaturedContent'
 
 @InputType()
 class FeaturedVideoInput implements FeaturedVideo {
@@ -37,11 +37,6 @@ class SetCategoryFeaturedVideoArgs {
 
 @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()
@@ -63,6 +58,11 @@ export class FeaturedContentResolver {
     return categoriesList
   }
 
+  @Query(() => VideoHero, { nullable: false, description: 'Get current video hero' })
+  async videoHero() {
+    return (await getFeaturedContentDoc()).videoHero
+  }
+
   @Mutation(() => VideoHero, { nullable: false })
   @Authorized()
   async setVideoHero(@Arg('newVideoHero', () => VideoHeroInput) newVideoHero: VideoHeroInput) {

+ 4 - 69
src/resolvers/followsInfo.ts

@@ -1,9 +1,7 @@
-import { Args, ArgsType, Ctx, Field, ID, Int, Mutation, Query, Resolver } from 'type-graphql'
-import { Min, Max, IsIn } from 'class-validator'
+import { Args, ArgsType, Ctx, Field, ID, Mutation, Resolver } from 'type-graphql'
 import { ChannelFollowsInfo } from '../entities/ChannelFollowsInfo'
-import { ChannelEventType, saveChannelEvent, UnsequencedChannelEvent } from '../models/ChannelEvent'
+import { UnsequencedChannelEvent, ChannelEventType, saveChannelEvent } from '../models/ChannelEvent'
 import { OrionContext } from '../types'
-import { mapPeriods } from '../helpers'
 
 @ArgsType()
 class ChannelFollowsArgs {
@@ -11,32 +9,6 @@ class ChannelFollowsArgs {
   channelId: string
 }
 
-@ArgsType()
-class MostFollowedArgs {
-  @Field(() => Int, {
-    description: 'timePeriodDays must take one of the following values: 7, 30',
-  })
-  @IsIn([7, 30])
-  timePeriodDays: 7 | 30
-
-  @Field(() => Int, { nullable: true })
-  limit?: number
-}
-
-@ArgsType()
-class MostFollowedChannelsAllTimeArgs {
-  @Field(() => Int)
-  @Min(1)
-  @Max(200)
-  limit: number
-}
-
-@ArgsType()
-class BatchedChannelFollowsArgs {
-  @Field(() => [ID])
-  channelIdList: string[]
-}
-
 @ArgsType()
 class FollowChannelArgs extends ChannelFollowsArgs {}
 
@@ -45,39 +17,6 @@ class UnfollowChannelArgs extends ChannelFollowsArgs {}
 
 @Resolver()
 export class ChannelFollowsInfosResolver {
-  @Query(() => ChannelFollowsInfo, { nullable: true, description: 'Get follows count for a single channel' })
-  async channelFollows(
-    @Args() { channelId }: ChannelFollowsArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<ChannelFollowsInfo | null> {
-    return getFollowsInfo(channelId, ctx)
-  }
-
-  @Query(() => [ChannelFollowsInfo], { description: 'Get list of most followed channels' })
-  async mostFollowedChannels(
-    @Args() { timePeriodDays, limit }: MostFollowedArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<ChannelFollowsInfo[]> {
-    ctx.followsAggregate.filterEventsByPeriod(timePeriodDays)
-    return limitFollows(ctx.followsAggregate.getTimePeriodChannelFollows()[mapPeriods(timePeriodDays)], limit)
-  }
-
-  @Query(() => [ChannelFollowsInfo], { nullable: true, description: 'Get list of most followed channels of all time' })
-  async mostFollowedChannelsAllTime(
-    @Args() { limit }: MostFollowedChannelsAllTimeArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<ChannelFollowsInfo[]> {
-    return limitFollows(ctx.followsAggregate.getAllChannelFollows(), limit)
-  }
-
-  @Query(() => [ChannelFollowsInfo], { description: 'Get follows counts for a list of channels', nullable: 'items' })
-  async batchedChannelFollows(
-    @Args() { channelIdList }: BatchedChannelFollowsArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<(ChannelFollowsInfo | null)[]> {
-    return channelIdList.map((channelId) => getFollowsInfo(channelId, ctx))
-  }
-
   @Mutation(() => ChannelFollowsInfo, { description: 'Add a single follow to the target channel' })
   async followChannel(@Args() { channelId }: FollowChannelArgs, @Ctx() ctx: OrionContext): Promise<ChannelFollowsInfo> {
     const event: UnsequencedChannelEvent = {
@@ -112,12 +51,8 @@ export class ChannelFollowsInfosResolver {
   }
 }
 
-const limitFollows = (follows: ChannelFollowsInfo[], limit?: number) => {
-  return follows.sort((a, b) => (a.follows > b.follows ? -1 : 1)).slice(0, limit)
-}
-
-const getFollowsInfo = (channelId: string, ctx: OrionContext): ChannelFollowsInfo | null => {
-  const follows = ctx.followsAggregate.channelFollows(channelId)
+export const getFollowsInfo = (channelId: string, ctx: OrionContext): ChannelFollowsInfo | null => {
+  const follows = ctx.followsAggregate.channelFollows(channelId) || 0
   if (follows != null) {
     return {
       id: channelId,

+ 48 - 0
src/resolvers/helpers.ts

@@ -0,0 +1,48 @@
+import { OrionContext } from '../types'
+import { mapPeriods } from '../helpers'
+
+export type IdsInPeriodOpts = {
+  period: 7 | 30 | null
+  limit: number
+}
+export const getMostViewedVideosIds = (context: OrionContext, { period, limit }: IdsInPeriodOpts) => {
+  // prepare aggregate
+  if (period) {
+    context.viewsAggregate.filterEventsByPeriod(period)
+  }
+
+  const views = period
+    ? context.viewsAggregate.getTimePeriodVideoViews()[mapPeriods(period)]
+    : context.viewsAggregate.getAllVideoViews()
+  const limitedViews = views.slice(0, limit)
+
+  return limitedViews.filter((entity) => entity.views).map((entity) => entity.id)
+}
+
+export const getMostViewedChannelsIds = (context: OrionContext, { period, limit }: IdsInPeriodOpts) => {
+  // prepare aggregate
+  if (period) {
+    context.viewsAggregate.filterEventsByPeriod(period)
+  }
+
+  const views = period
+    ? context.viewsAggregate.getTimePeriodChannelViews()[mapPeriods(period)]
+    : context.viewsAggregate.getAllChannelViews()
+  const limitedViews = views.slice(0, limit)
+
+  return limitedViews.filter((entity) => entity.views).map((entity) => entity.id)
+}
+
+export const getMostFollowedChannelsIds = (context: OrionContext, { period, limit }: IdsInPeriodOpts) => {
+  // prepare aggregate
+  if (period) {
+    context.followsAggregate.filterEventsByPeriod(period)
+  }
+
+  const follows = period
+    ? context.followsAggregate.getTimePeriodChannelFollows()[mapPeriods(period)]
+    : context.followsAggregate.getAllChannelFollows()
+  const limitedFollows = follows.slice(0, limit)
+
+  return limitedFollows.filter((entity) => entity.follows).map((entity) => entity.id)
+}

+ 112 - 0
src/resolvers/queryNodeStitchingResolvers/channelResolvers.ts

@@ -0,0 +1,112 @@
+import type { IResolvers } from '@graphql-tools/utils'
+import { GraphQLSchema } from 'graphql'
+import { Channel, OrionContext } from '../../types'
+import { getFollowsInfo } from '../followsInfo'
+import { getMostFollowedChannelsIds, getMostViewedChannelsIds } from '../helpers'
+import { getChannelViewsInfo } from '../viewsInfo'
+import { createResolver, getDataWithIds, sortEntities } from './helpers'
+import { shuffle } from 'lodash'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const channelResolvers = (queryNodeSchema: GraphQLSchema): IResolvers<any, OrionContext> => ({
+  Query: {
+    discoverChannels: {
+      selectionSet: '{ id }',
+      resolve: async (parent, args, context, info) => {
+        const resolver = createResolver(queryNodeSchema, 'channels')
+        const newestChannels: Channel[] = await resolver(
+          parent,
+          {
+            limit: 100,
+            where: {
+              ...(args.where || {}),
+            },
+            orderBy: ['createdAt_DESC'],
+          },
+          context,
+          info
+        )
+
+        const sortedChannels = newestChannels
+          .map((channel) => ({
+            ...channel,
+            follows: context.followsAggregate.channelFollows(channel.id),
+          }))
+          .filter((channel) => channel.follows)
+          .sort((a, b) => (b.follows || 0) - (a.follows || 0))
+
+        const slicedChannels = sortedChannels.slice(0, 15)
+        return shuffle(slicedChannels)
+      },
+    },
+    promisingChannels: {
+      selectionSet: '{ id }',
+      resolve: async (parent, args, context, info) => {
+        const resolver = createResolver(queryNodeSchema, 'channels')
+        const newestChannels: Channel[] = await resolver(
+          parent,
+          {
+            limit: 100,
+            where: {
+              ...(args.where || {}),
+            },
+            orderBy: ['createdAt_DESC'],
+          },
+          context,
+          info
+        )
+
+        const sortedChannels = newestChannels
+          .map((channel) => ({
+            ...channel,
+            views: context.viewsAggregate.channelViews(channel.id),
+          }))
+          .filter((channel) => channel.views)
+          .sort((a, b) => (b.views || 0) - (a.views || 0))
+
+        const slicedChannels = sortedChannels.slice(0, 15)
+        return shuffle(slicedChannels)
+      },
+    },
+    popularChannels: {
+      resolve: async (parent, args, context, info) => {
+        const mostViewedChannelsIds = getMostViewedChannelsIds(context, { limit: 15, period: null })
+        const resolver = createResolver(queryNodeSchema, 'channels')
+        const channels = await getDataWithIds(resolver, mostViewedChannelsIds, parent, args, context, info)
+        return shuffle(channels)
+      },
+    },
+    top10Channels: async (parent, args, context, info) => {
+      const mostFollowedChannelsIds = getMostFollowedChannelsIds(context, { limit: 10, period: null })
+      const resolver = createResolver(queryNodeSchema, 'channels')
+      const channels = await getDataWithIds(resolver, mostFollowedChannelsIds, parent, args, context, info)
+      return sortEntities(channels, mostFollowedChannelsIds)
+    },
+    mostViewedChannelsConnection: async (parent, args, context, info) => {
+      const mostViewedChannelsIds = getMostViewedChannelsIds(context, {
+        limit: args.limit,
+        period: args.periodDays || null,
+      })
+      const resolver = createResolver(queryNodeSchema, 'channelsConnection')
+      return getDataWithIds(resolver, mostViewedChannelsIds, parent, args, context, info)
+    },
+    mostFollowedChannelsConnection: async (parent, args, context, info) => {
+      const mostFollowedChannelsIds = getMostFollowedChannelsIds(context, {
+        limit: args.limit,
+        period: args.periodDays || null,
+      })
+      const resolver = createResolver(queryNodeSchema, 'channelsConnection')
+      return getDataWithIds(resolver, mostFollowedChannelsIds, parent, args, context, info)
+    },
+  },
+  Channel: {
+    views: {
+      selectionSet: '{ id }',
+      resolve: (parent, args, context) => getChannelViewsInfo(parent.id, context)?.views,
+    },
+    follows: {
+      selectionSet: '{ id }',
+      resolve: (parent, args, context) => getFollowsInfo(parent.id, context)?.follows,
+    },
+  },
+})

+ 57 - 0
src/resolvers/queryNodeStitchingResolvers/featuredContentResolvers.ts

@@ -0,0 +1,57 @@
+import type { IResolvers } from '@graphql-tools/utils'
+import { GraphQLSchema } from 'graphql'
+import { createResolver } from './helpers'
+
+export const featuredContentResolvers = (queryNodeSchema: GraphQLSchema): IResolvers => ({
+  VideoHero: {
+    video: async (parent, args, context, info) => {
+      const videoResolver = createResolver(queryNodeSchema, 'videoByUniqueInput')
+      return videoResolver(
+        parent,
+        {
+          where: {
+            id: parent.videoId,
+          },
+        },
+        context,
+        info
+      )
+    },
+  },
+  FeaturedVideo: {
+    video: {
+      selectionSet: '{ videoId }',
+      resolve: async (parent, args, context, info) => {
+        const videoResolver = createResolver(queryNodeSchema, 'videoByUniqueInput')
+        return videoResolver(
+          parent,
+          {
+            where: {
+              id: parent.videoId,
+            },
+          },
+          context,
+          info
+        )
+      },
+    },
+  },
+  CategoryFeaturedVideos: {
+    category: {
+      selectionSet: '{ categoryId }',
+      resolve: async (parent, args, context, info) => {
+        const categoryResolver = createResolver(queryNodeSchema, 'videoCategoryByUniqueInput')
+        return categoryResolver(
+          parent,
+          {
+            where: {
+              id: parent.categoryId,
+            },
+          },
+          context,
+          info
+        )
+      },
+    },
+  },
+})

+ 52 - 0
src/resolvers/queryNodeStitchingResolvers/helpers.ts

@@ -0,0 +1,52 @@
+import { delegateToSchema } from '@graphql-tools/delegate'
+import type { ISchemaLevelResolver } from '@graphql-tools/utils'
+import { GraphQLResolveInfo, GraphQLSchema } from 'graphql'
+import { OrionContext } from '../../types'
+
+export const createResolver = (
+  schema: GraphQLSchema,
+  fieldName: string
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): ISchemaLevelResolver<any, any> => {
+  return async (parent, args, context, info) =>
+    delegateToSchema({
+      schema,
+      operation: 'query',
+      fieldName,
+      args,
+      context,
+      info,
+    })
+}
+
+export const getDataWithIds = (
+  resolver: ReturnType<typeof createResolver>,
+  ids: string[],
+  parent: unknown,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  args: any,
+  context: OrionContext,
+  info: GraphQLResolveInfo
+) => {
+  return resolver(
+    parent,
+    {
+      ...args,
+      where: {
+        ...args.where,
+        id_in: ids,
+      },
+    },
+    context,
+    info
+  )
+}
+
+type Entity = {
+  id: string
+}
+export const sortEntities = (entities: Entity[], ids: string[]) => {
+  return [...entities].sort((a: Entity, b: Entity) => {
+    return ids.indexOf(a.id) - ids.indexOf(b.id)
+  })
+}

+ 14 - 0
src/resolvers/queryNodeStitchingResolvers/index.ts

@@ -0,0 +1,14 @@
+import { GraphQLSchema } from 'graphql'
+import type { IResolvers } from '@graphql-tools/utils'
+import { mergeResolvers } from '@graphql-tools/merge'
+import { channelResolvers } from './channelResolvers'
+import { featuredContentResolvers } from './featuredContentResolvers'
+import { videoResolvers } from './videoResolvers'
+
+export const queryNodeStitchingResolvers = (queryNodeSchema: GraphQLSchema): IResolvers => {
+  return mergeResolvers([
+    channelResolvers(queryNodeSchema),
+    featuredContentResolvers(queryNodeSchema),
+    videoResolvers(queryNodeSchema),
+  ])
+}

+ 38 - 0
src/resolvers/queryNodeStitchingResolvers/videoResolvers.ts

@@ -0,0 +1,38 @@
+import type { IResolvers } from '@graphql-tools/utils'
+import { GraphQLSchema } from 'graphql'
+import { getVideoViewsInfo } from '../viewsInfo'
+import { createResolver, getDataWithIds, sortEntities } from './helpers'
+import { getMostViewedVideosIds } from '../helpers'
+import { OrionContext } from '../../types'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const videoResolvers = (queryNodeSchema: GraphQLSchema): IResolvers<any, OrionContext> => ({
+  Query: {
+    top10VideosThisWeek: async (parent, args, context, info) => {
+      const mostViewedVideosIds = getMostViewedVideosIds(context, { limit: 10, period: 7 })
+      const resolver = createResolver(queryNodeSchema, 'videos')
+      const videos = await getDataWithIds(resolver, mostViewedVideosIds, parent, args, context, info)
+      return sortEntities(videos, mostViewedVideosIds)
+    },
+    top10VideosThisMonth: async (parent, args, context, info) => {
+      const mostViewedVideosIds = getMostViewedVideosIds(context, { limit: 10, period: 30 })
+      const resolver = createResolver(queryNodeSchema, 'videos')
+      const videos = await getDataWithIds(resolver, mostViewedVideosIds, parent, args, context, info)
+      return sortEntities(videos, mostViewedVideosIds)
+    },
+    mostViewedVideosConnection: async (parent, args, context, info) => {
+      const mostViewedVideosIds = getMostViewedVideosIds(context, {
+        limit: args.limit,
+        period: args.periodDays || null,
+      })
+      const resolver = createResolver(queryNodeSchema, 'videosConnection')
+      return getDataWithIds(resolver, mostViewedVideosIds, parent, args, context, info)
+    },
+  },
+  Video: {
+    views: {
+      selectionSet: `{ id }`,
+      resolve: (parent, args, context) => getVideoViewsInfo(parent.id, context)?.views,
+    },
+  },
+})

+ 10 - 105
src/resolvers/viewsInfo.ts

@@ -1,21 +1,9 @@
 import { Args, ArgsType, Ctx, Field, ID, Int, Mutation, Query, Resolver } from 'type-graphql'
 import { Min, Max, IsIn } from 'class-validator'
 import { EntityViewsInfo } from '../entities/EntityViewsInfo'
-import { saveVideoEvent, VideoEventType, UnsequencedVideoEvent } from '../models/VideoEvent'
-import { OrionContext } from '../types'
 import { mapPeriods } from '../helpers'
-
-@ArgsType()
-class VideoViewsArgs {
-  @Field(() => ID)
-  videoId: string
-}
-
-@ArgsType()
-class BatchedVideoViewsArgs {
-  @Field(() => [ID])
-  videoIdList: string[]
-}
+import { UnsequencedVideoEvent, VideoEventType, saveVideoEvent } from '../models/VideoEvent'
+import { OrionContext } from '../types'
 
 @ArgsType()
 class MostViewedArgs {
@@ -29,18 +17,6 @@ class MostViewedArgs {
   limit?: number
 }
 
-@ArgsType()
-class ChannelViewsArgs {
-  @Field(() => ID)
-  channelId: string
-}
-
-@ArgsType()
-class BatchedChannelViewsArgs {
-  @Field(() => [ID])
-  channelIdList: string[]
-}
-
 @ArgsType()
 class AddVideoViewArgs {
   @Field(() => ID)
@@ -63,59 +39,6 @@ class MostViewedAllTimeArgs {
 
 @Resolver()
 export class VideoViewsInfosResolver {
-  @Query(() => EntityViewsInfo, { nullable: true, description: 'Get views count for a single video' })
-  async videoViews(@Args() { videoId }: VideoViewsArgs, @Ctx() ctx: OrionContext): Promise<EntityViewsInfo | null> {
-    return getVideoViewsInfo(videoId, ctx)
-  }
-
-  @Query(() => [EntityViewsInfo], { description: 'Get views counts for a list of videos', nullable: 'items' })
-  async batchedVideoViews(
-    @Args() { videoIdList }: BatchedVideoViewsArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<(EntityViewsInfo | null)[]> {
-    return videoIdList.map((videoId) => getVideoViewsInfo(videoId, ctx))
-  }
-
-  @Query(() => [EntityViewsInfo], {
-    nullable: true,
-    description: 'Get list of most viewed videos in a given time period',
-  })
-  async mostViewedVideos(
-    @Args() { timePeriodDays, limit }: MostViewedArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<EntityViewsInfo[]> {
-    ctx.viewsAggregate.filterEventsByPeriod(timePeriodDays)
-    return limitViews(ctx.viewsAggregate.getTimePeriodVideoViews()[mapPeriods(timePeriodDays)], limit)
-  }
-
-  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get list of most viewed videos of all time' })
-  async mostViewedVideosAllTime(
-    @Args() { limit }: MostViewedAllTimeArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<EntityViewsInfo[]> {
-    return limitViews(ctx.viewsAggregate.getAllVideoViews(), limit)
-  }
-
-  @Query(() => [EntityViewsInfo], {
-    nullable: true,
-    description: 'Get list of most viewed channels in a given time period',
-  })
-  async mostViewedChannels(
-    @Args() { timePeriodDays, limit }: MostViewedArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<EntityViewsInfo[]> {
-    ctx.viewsAggregate.filterEventsByPeriod(timePeriodDays)
-    return limitViews(ctx.viewsAggregate.getTimePeriodChannelViews()[mapPeriods(timePeriodDays)], limit)
-  }
-
-  @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get list of most viewed channels of all time' })
-  async mostViewedChannelsAllTime(
-    @Args() { limit }: MostViewedAllTimeArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<EntityViewsInfo[]> {
-    return limitViews(ctx.viewsAggregate.getAllChannelViews(), limit)
-  }
-
   @Query(() => [EntityViewsInfo], {
     nullable: true,
     description: 'Get list of most viewed categories in a given time period',
@@ -125,7 +48,8 @@ export class VideoViewsInfosResolver {
     @Ctx() ctx: OrionContext
   ): Promise<EntityViewsInfo[]> {
     ctx.viewsAggregate.filterEventsByPeriod(timePeriodDays)
-    return limitViews(ctx.viewsAggregate.getTimePeriodCategoryViews()[mapPeriods(timePeriodDays)], limit)
+    const views = ctx.viewsAggregate.getTimePeriodCategoryViews()[mapPeriods(timePeriodDays)]
+    return views.slice(0, limit)
   }
 
   @Query(() => [EntityViewsInfo], { nullable: true, description: 'Get list of most viewed categories of all time' })
@@ -133,23 +57,8 @@ export class VideoViewsInfosResolver {
     @Args() { limit }: MostViewedAllTimeArgs,
     @Ctx() ctx: OrionContext
   ): Promise<EntityViewsInfo[]> {
-    return limitViews(ctx.viewsAggregate.getAllCategoryViews(), limit)
-  }
-
-  @Query(() => EntityViewsInfo, { nullable: true, description: 'Get views count for a single channel' })
-  async channelViews(
-    @Args() { channelId }: ChannelViewsArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<EntityViewsInfo | null> {
-    return getChannelViewsInfo(channelId, ctx)
-  }
-
-  @Query(() => [EntityViewsInfo], { description: 'Get views counts for a list of channels', nullable: 'items' })
-  async batchedChannelsViews(
-    @Args() { channelIdList }: BatchedChannelViewsArgs,
-    @Ctx() ctx: OrionContext
-  ): Promise<(EntityViewsInfo | null)[]> {
-    return channelIdList.map((channelId) => getChannelViewsInfo(channelId, ctx))
+    const views = ctx.viewsAggregate.getAllCategoryViews()
+    return views.slice(0, limit)
   }
 
   @Mutation(() => EntityViewsInfo, { description: "Add a single view to the target video's count" })
@@ -173,10 +82,6 @@ export class VideoViewsInfosResolver {
   }
 }
 
-const limitViews = (views: EntityViewsInfo[], limit?: number) => {
-  return views.slice(0, limit)
-}
-
 const buildViewsObject = (id: string, views: number | null): EntityViewsInfo | null => {
   if (views != null) {
     return {
@@ -187,12 +92,12 @@ const buildViewsObject = (id: string, views: number | null): EntityViewsInfo | n
   return null
 }
 
-const getVideoViewsInfo = (videoId: string, ctx: OrionContext): EntityViewsInfo | null => {
-  const views = ctx.viewsAggregate.videoViews(videoId)
+export const getVideoViewsInfo = (videoId: string, ctx: OrionContext): EntityViewsInfo | null => {
+  const views = ctx.viewsAggregate.videoViews(videoId) || 0
   return buildViewsObject(videoId, views)
 }
 
-const getChannelViewsInfo = (channelId: string, ctx: OrionContext): EntityViewsInfo | null => {
-  const views = ctx.viewsAggregate.channelViews(channelId)
+export const getChannelViewsInfo = (channelId: string, ctx: OrionContext): EntityViewsInfo | null => {
+  const views = ctx.viewsAggregate.channelViews(channelId) || 0
   return buildViewsObject(channelId, views)
 }

+ 55 - 10
src/server.ts

@@ -1,26 +1,48 @@
-import 'reflect-metadata'
-import { ApolloServer } from 'apollo-server-express'
-import { ExpressContext } from 'apollo-server-express/dist/ApolloServer'
-import { ContextFunction, ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
+import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
+import { loadSchema } from '@graphql-tools/load'
+import { stitchSchemas } from '@graphql-tools/stitch'
+import { ApolloServerPluginLandingPageGraphQLPlayground, ContextFunction, PluginDefinition } from 'apollo-server-core'
+import { ApolloServer, ExpressContext } from 'apollo-server-express'
+import type { GraphQLRequestContext } from 'apollo-server-types'
+import type { GraphQLRequestListener } from 'apollo-server-plugin-base'
+
 import { connect, Mongoose } from 'mongoose'
+import 'reflect-metadata'
 import { buildSchema } from 'type-graphql'
-
 import { FollowsAggregate, ViewsAggregate } from './aggregates'
+import { customAuthChecker } from './helpers'
 import { ChannelFollowsInfosResolver, VideoViewsInfosResolver } from './resolvers'
-import { Aggregates, OrionContext } from './types'
 import { FeaturedContentResolver } from './resolvers/featuredContent'
-import { customAuthChecker } from './helpers/auth'
+import { queryNodeStitchingResolvers } from './resolvers/queryNodeStitchingResolvers'
+import { Aggregates, OrionContext } from './types'
+import config from './config'
+import { UrlLoader } from '@graphql-tools/url-loader'
 
-export const createServer = async (mongoose: Mongoose, aggregates: Aggregates) => {
+export const createServer = async (mongoose: Mongoose, aggregates: Aggregates, queryNodeUrl: string) => {
   await mongoose.connection
 
-  const schema = await buildSchema({
+  const remoteQueryNodeSchema = await loadSchema(queryNodeUrl, {
+    loaders: [new UrlLoader()],
+  })
+
+  const orionSchema = await buildSchema({
     resolvers: [VideoViewsInfosResolver, ChannelFollowsInfosResolver, FeaturedContentResolver],
     authChecker: customAuthChecker,
     emitSchemaFile: 'schema.graphql',
     validate: true,
   })
 
+  const queryNodeSchemaExtension = await loadSchema('./queryNodeSchemaExtension.graphql', {
+    loaders: [new GraphQLFileLoader()],
+    schemas: [remoteQueryNodeSchema, orionSchema],
+    resolvers: queryNodeStitchingResolvers(remoteQueryNodeSchema),
+  })
+
+  const schema = stitchSchemas({
+    subschemas: [orionSchema, remoteQueryNodeSchema, queryNodeSchemaExtension],
+    resolvers: queryNodeStitchingResolvers(remoteQueryNodeSchema),
+  })
+
   const contextFn: ContextFunction<ExpressContext, OrionContext> = ({ req }) => ({
     ...aggregates,
     remoteHost: req?.ip,
@@ -30,7 +52,7 @@ export const createServer = async (mongoose: Mongoose, aggregates: Aggregates) =
   return new ApolloServer({
     schema,
     context: contextFn,
-    plugins: [ApolloServerPluginLandingPageGraphQLPlayground],
+    plugins: [ApolloServerPluginLandingPageGraphQLPlayground, config.isDebugging ? graphQLLoggingPlugin : {}],
   })
 }
 
@@ -46,3 +68,26 @@ export const buildAggregates = async (): Promise<Aggregates> => {
 
   return { viewsAggregate, followsAggregate }
 }
+
+const graphQLLoggingPlugin: PluginDefinition = {
+  async requestDidStart(requestContext: GraphQLRequestContext): Promise<GraphQLRequestListener | void> {
+    console.log('Request started: ' + requestContext.request.operationName)
+    return {
+      async executionDidStart() {
+        return {
+          willResolveField({ info }) {
+            const start = process.hrtime.bigint()
+            return () => {
+              const end = process.hrtime.bigint()
+              const time = end - start
+              // log only fields that took longer than 1ms to resolve
+              if (time > 1000 * 1000) {
+                console.log(`Field ${info.parentType.name}.${info.fieldName} took ${time / 1000n / 1000n}ms`)
+              }
+            }
+          },
+        }
+      },
+    }
+  },
+}

+ 59 - 0
src/types.ts

@@ -9,3 +9,62 @@ export type OrionContext = {
   remoteHost?: string
   authorization?: string
 } & Aggregates
+
+export type Scalars = {
+  ID: string
+  String: string
+  Boolean: boolean
+  Int: number
+  Float: number
+  DateTime: Date
+}
+
+export type Maybe<T> = T | null
+
+export type Video = {
+  __typename?: 'Video'
+  id: Scalars['ID']
+  views?: Maybe<Scalars['Int']>
+}
+
+export type VideoEdge = {
+  __typename?: 'VideoEdge'
+  node: Video
+  cursor: Scalars['String']
+}
+
+export type VideoConnection = {
+  __typename?: 'VideoConnection'
+  totalCount: number
+  edges: VideoEdge[]
+}
+
+export type Channel = {
+  __typename?: 'Channel'
+  id: Scalars['ID']
+  videos: Array<Video>
+  follows?: Maybe<Scalars['Int']>
+  views?: Maybe<Scalars['Int']>
+}
+
+export type ChannelEdge = {
+  __typename?: 'ChannelEdge'
+  node: Channel
+  cursor: Scalars['String']
+}
+
+export type ChannelConnection = {
+  __typename?: 'ChannelConnection'
+  totalCount: number
+  edges: ChannelEdge[]
+}
+
+export type SearchResult = Video | Channel
+
+export type SearchFtsOutput = {
+  __typename?: 'SearchFTSOutput'
+  item: SearchResult
+  rank: Scalars['Float']
+  isTypeOf: Scalars['String']
+  highlight: Scalars['String']
+}

+ 1 - 1
tests/featuredContent.test.ts

@@ -35,7 +35,7 @@ describe('Featured content resolver', () => {
   beforeEach(async () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
-    server = await createServer(mongoose, aggregates)
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
     await server.start()
     query = createQueryFn(server)
     mutate = createMutationFn(server)

+ 53 - 102
tests/follows.test.ts

@@ -6,18 +6,12 @@ import {
   FOLLOW_CHANNEL,
   FollowChannel,
   FollowChannelArgs,
-  GET_CHANNEL_FOLLOWS,
-  GetChannelFollows,
-  GetChannelFollowsArgs,
   UNFOLLOW_CHANNEL,
   UnfollowChannel,
   UnfollowChannelArgs,
-  GET_MOST_FOLLOWED_CHANNELS,
-  GetMostFollowedChannels,
-  GetMostFollowedChannelsArgs,
-  GET_MOST_FOLLOWED_CHANNELS_ALL_TIME,
-  GetMostFollowedChannelsAllTime,
-  GetMostFollowedChannelsAllTimeArgs,
+  GET_MOST_FOLLOWED_CHANNELS_CONNECTION,
+  GetMostFollowedChannelsConnection,
+  GetMostFollowedChannelsConnectionArgs,
 } from './queries/follows'
 import { ChannelFollowsInfo } from '../src/entities/ChannelFollowsInfo'
 import { ChannelEventsBucketModel } from '../src/models/ChannelEvent'
@@ -37,7 +31,7 @@ describe('Channel follows resolver', () => {
   beforeEach(async () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
-    server = await createServer(mongoose, aggregates)
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
     await server.start()
     query = createQueryFn(server)
     mutate = createMutationFn(server)
@@ -67,44 +61,24 @@ describe('Channel follows resolver', () => {
     return unfollowChannelResponse.data?.unfollowChannel
   }
 
-  const getChannelFollows = async (channelId: string) => {
-    const channelFollowsResponse = await query<GetChannelFollows, GetChannelFollowsArgs>({
-      query: GET_CHANNEL_FOLLOWS,
-      variables: { channelId },
-    })
-    expect(channelFollowsResponse.errors).toBeUndefined()
-    return channelFollowsResponse.data?.channelFollows
-  }
-
-  const getMostFollowedChannels = async (timePeriodDays: number) => {
-    const mostFollowedChannelsResponse = await query<GetMostFollowedChannels, GetMostFollowedChannelsArgs>({
-      query: GET_MOST_FOLLOWED_CHANNELS,
-      variables: { timePeriodDays },
-    })
-    expect(mostFollowedChannelsResponse.errors).toBeUndefined()
-    return mostFollowedChannelsResponse.data?.mostFollowedChannels
-  }
-
-  const getMostFollowedChannelsAllTime = async (limit: number) => {
-    const mostFollowedChannelsAllTimeResponse = await query<
-      GetMostFollowedChannelsAllTime,
-      GetMostFollowedChannelsAllTimeArgs
+  const getMostFollowedChannels = async (periodDays: 7 | 30 | null) => {
+    const mostFollowedChannelsResponse = await query<
+      GetMostFollowedChannelsConnection,
+      GetMostFollowedChannelsConnectionArgs
     >({
-      query: GET_MOST_FOLLOWED_CHANNELS_ALL_TIME,
-      variables: { limit },
+      query: GET_MOST_FOLLOWED_CHANNELS_CONNECTION,
+      variables: { periodDays, limit: 10 },
     })
-    expect(mostFollowedChannelsAllTimeResponse.errors).toBeUndefined()
-    return mostFollowedChannelsAllTimeResponse.data?.mostFollowedChannelsAllTime
+    expect(mostFollowedChannelsResponse.errors).toBeUndefined()
+    return mostFollowedChannelsResponse.data?.mostFollowedChannelsConnection
   }
 
   it('should return null for unknown channel follows', async () => {
-    const channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     const mostFollowedChannels = await getMostFollowedChannels(30)
-    const mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
+    const mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
 
-    expect(channelFollows).toBeNull()
-    expect(mostFollowedChannels).toHaveLength(0)
-    expect(mostFollowedChannelsAllTime).toHaveLength(0)
+    expect(mostFollowedChannels?.edges).toHaveLength(0)
+    expect(mostFollowedChannelsAllTime?.edges).toHaveLength(0)
   })
 
   it('should properly handle channel follow', async () => {
@@ -112,28 +86,27 @@ describe('Channel follows resolver', () => {
       id: FIRST_CHANNEL_ID,
       follows: 1,
     }
+    const expectedMostFollowedChannels = {
+      edges: [expectedChannelFollows].map((follow) => ({ node: follow })),
+    }
 
     let addChannelFollowData = await followChannel(FIRST_CHANNEL_ID)
     expect(addChannelFollowData).toEqual(expectedChannelFollows)
 
-    let channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     let mostFollowedChannels = await getMostFollowedChannels(30)
-    let mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
+    let mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
 
     expectedChannelFollows.follows++
 
     addChannelFollowData = await followChannel(FIRST_CHANNEL_ID)
     expect(addChannelFollowData).toEqual(expectedChannelFollows)
 
-    channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     mostFollowedChannels = await getMostFollowedChannels(30)
-    mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
+    mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
   })
 
   it('should properly handle channel unfollow', async () => {
@@ -142,51 +115,30 @@ describe('Channel follows resolver', () => {
       follows: 5,
     }
 
+    const expectedMostFollowedChannels = {
+      edges: [expectedChannelFollows].map((follow) => ({ node: follow })),
+    }
+
     await followChannel(FIRST_CHANNEL_ID)
     await followChannel(FIRST_CHANNEL_ID)
     await followChannel(FIRST_CHANNEL_ID)
     await followChannel(FIRST_CHANNEL_ID)
     await followChannel(FIRST_CHANNEL_ID)
 
-    let channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     let mostFollowedChannels = await getMostFollowedChannels(30)
-    let mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
+    let mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
 
     expectedChannelFollows.follows--
 
     const unfollowChannelData = await unfollowChannel(FIRST_CHANNEL_ID)
     expect(unfollowChannelData).toEqual(expectedChannelFollows)
 
-    channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     mostFollowedChannels = await getMostFollowedChannels(30)
-    mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
-  })
-
-  it('should keep the channel follows non-negative', async () => {
-    const expectedChannelFollows: ChannelFollowsInfo = {
-      id: FIRST_CHANNEL_ID,
-      follows: 0,
-    }
-
-    await followChannel(FIRST_CHANNEL_ID)
-    await followChannel(FIRST_CHANNEL_ID)
-
-    await unfollowChannel(FIRST_CHANNEL_ID)
-    await unfollowChannel(FIRST_CHANNEL_ID)
-    await unfollowChannel(FIRST_CHANNEL_ID)
-
-    const channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
-    const mostFollowedChannels = await getMostFollowedChannels(30)
-    const mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
+    mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
   })
 
   it('should distinct follows of separate channels', async () => {
@@ -198,6 +150,9 @@ describe('Channel follows resolver', () => {
       id: SECOND_CHANNEL_ID,
       follows: 1,
     }
+    const expectedMostFollowedChannels = {
+      edges: [expectedFirstChannelFollows, expectedSecondChannelFollows].map((follow) => ({ node: follow })),
+    }
 
     const firstChannelFollowData = await followChannel(FIRST_CHANNEL_ID)
     const secondChannelFollowData = await followChannel(SECOND_CHANNEL_ID)
@@ -209,15 +164,11 @@ describe('Channel follows resolver', () => {
 
     await followChannel(FIRST_CHANNEL_ID)
 
-    const firstChannelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
-    const secondChannelFollows = await getChannelFollows(SECOND_CHANNEL_ID)
     const mostFollowedChannels = await getMostFollowedChannels(30)
-    const mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
+    const mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
 
-    expect(firstChannelFollows).toEqual(expectedFirstChannelFollows)
-    expect(secondChannelFollows).toEqual(expectedSecondChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedFirstChannelFollows, expectedSecondChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedFirstChannelFollows, expectedSecondChannelFollows])
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
   })
 
   it('should properly rebuild the aggregate', async () => {
@@ -229,17 +180,16 @@ describe('Channel follows resolver', () => {
       id: SECOND_CHANNEL_ID,
       follows: 4,
     }
+    const expectedMostFollowedChannels = {
+      edges: [expectedFirstChannelFollows, expectedSecondChannelFollows].map((follow) => ({ node: follow })),
+    }
 
     const checkFollows = async () => {
-      const firstChannelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
-      const secondChannelFollows = await getChannelFollows(SECOND_CHANNEL_ID)
       const mostFollowedChannels = await getMostFollowedChannels(30)
-      const mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
+      const mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
 
-      expect(firstChannelFollows).toEqual(expectedFirstChannelFollows)
-      expect(secondChannelFollows).toEqual(expectedSecondChannelFollows)
-      expect(mostFollowedChannels).toEqual([expectedSecondChannelFollows, expectedFirstChannelFollows])
-      expect(mostFollowedChannelsAllTime).toEqual([expectedSecondChannelFollows, expectedFirstChannelFollows])
+      expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+      expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
     }
 
     await followChannel(FIRST_CHANNEL_ID)
@@ -257,7 +207,7 @@ describe('Channel follows resolver', () => {
 
     await server.stop()
     aggregates = await buildAggregates()
-    server = await createServer(mongoose, aggregates)
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
     query = createQueryFn(server)
     mutate = createMutationFn(server)
 
@@ -275,12 +225,13 @@ describe('Channel follows resolver', () => {
     for (let i = 0; i < eventsCount; i++) {
       await followChannel(FIRST_CHANNEL_ID)
     }
+    const expectedMostFollowedChannels = {
+      edges: [expectedChannelFollows].map((follow) => ({ node: follow })),
+    }
 
-    const channelFollows = await getChannelFollows(FIRST_CHANNEL_ID)
     const mostFollowedChannels = await getMostFollowedChannels(30)
-    const mostFollowedChannelsAllTime = await getMostFollowedChannelsAllTime(10)
-    expect(channelFollows).toEqual(expectedChannelFollows)
-    expect(mostFollowedChannels).toEqual([expectedChannelFollows])
-    expect(mostFollowedChannelsAllTime).toEqual([expectedChannelFollows])
+    const mostFollowedChannelsAllTime = await getMostFollowedChannels(null)
+    expect(mostFollowedChannels).toEqual(expectedMostFollowedChannels)
+    expect(mostFollowedChannelsAllTime).toEqual(expectedMostFollowedChannels)
   })
 })

+ 5 - 2
tests/queries/featuredContent.ts

@@ -19,13 +19,16 @@ export type GetVideoHero = {
 export const GET_CATEGORY_FEATURED_VIDEOS = gql`
   query GetCategoryFeaturedVideos($categoryId: ID!) {
     categoryFeaturedVideos(categoryId: $categoryId) {
-      videoId
       videoCutUrl
+      videoId
     }
   }
 `
 export type GetCategoryFeaturedVideos = {
-  categoryFeaturedVideos: FeaturedVideo[] | null
+  categoryFeaturedVideos: {
+    videoId: string
+    videoCutUrl: string
+  }
 }
 export type GetCategoryFeaturedVideosArgs = {
   categoryId: string

+ 14 - 37
tests/queries/follows.ts

@@ -1,47 +1,24 @@
 import { gql } from 'apollo-server-express'
 import { ChannelFollowsInfo } from '../../src/entities/ChannelFollowsInfo'
+import { ChannelConnection } from '../../src/types'
 
-export const GET_CHANNEL_FOLLOWS = gql`
-  query GetChannelFollows($channelId: ID!) {
-    channelFollows(channelId: $channelId) {
-      id
-      follows
-    }
-  }
-`
-export const GET_MOST_FOLLOWED_CHANNELS = gql`
-  query GetMostFollowedChannels($timePeriodDays: Int!) {
-    mostFollowedChannels(timePeriodDays: $timePeriodDays) {
-      id
-      follows
-    }
-  }
-`
-
-export const GET_MOST_FOLLOWED_CHANNELS_ALL_TIME = gql`
-  query GetMostFollowedChannelsAllTime($limit: Int!) {
-    mostFollowedChannelsAllTime(limit: $limit) {
-      id
-      follows
+export const GET_MOST_FOLLOWED_CHANNELS_CONNECTION = gql`
+  query GetMostFollowedChannelsConnection($periodDays: Int, $limit: Int!) {
+    mostFollowedChannelsConnection(periodDays: $periodDays, limit: $limit) {
+      edges {
+        node {
+          id
+          follows
+        }
+      }
     }
   }
 `
-export type GetChannelFollows = {
-  channelFollows: ChannelFollowsInfo | null
-}
-export type GetChannelFollowsArgs = {
-  channelId: string
-}
-export type GetMostFollowedChannelsArgs = {
-  timePeriodDays: number
-}
-export type GetMostFollowedChannels = {
-  mostFollowedChannels: ChannelFollowsInfo[]
-}
-export type GetMostFollowedChannelsAllTime = {
-  mostFollowedChannelsAllTime: ChannelFollowsInfo[]
+export type GetMostFollowedChannelsConnection = {
+  mostFollowedChannelsConnection: ChannelConnection
 }
-export type GetMostFollowedChannelsAllTimeArgs = {
+export type GetMostFollowedChannelsConnectionArgs = {
+  periodDays: number | null
   limit: number
 }
 

+ 28 - 74
tests/queries/views.ts

@@ -1,95 +1,49 @@
 import { gql } from 'apollo-server-express'
 import { EntityViewsInfo } from '../../src/entities/EntityViewsInfo'
+import { ChannelConnection, VideoConnection } from '../../src/types'
 
-export const GET_VIDEO_VIEWS = gql`
-  query GetVideoViews($videoId: ID!) {
-    videoViews(videoId: $videoId) {
-      id
-      views
+export const GET_MOST_VIEWED_VIDEOS_CONNECTION = gql`
+  query GetMostViewedVideosConnection($periodDays: Int, $limit: Int!) {
+    mostViewedVideosConnection(periodDays: $periodDays, limit: $limit) {
+      edges {
+        node {
+          id
+          views
+        }
+      }
     }
   }
 `
 
-export const GET_MOST_VIEWED_VIDEOS = gql`
-  query GetMostViewedVideos($timePeriodDays: Int!) {
-    mostViewedVideos(timePeriodDays: $timePeriodDays) {
-      id
-      views
-    }
-  }
-`
-
-export const GET_MOST_VIEWED_VIDEOS_ALL_TIME = gql`
-  query GetMostViewedVideosAllTime($limit: Int!) {
-    mostViewedVideosAllTime(limit: $limit) {
-      id
-      views
-    }
-  }
-`
-export type GetVideoViews = {
-  videoViews: EntityViewsInfo | null
-}
-export type GetMostViewedVideos = {
-  mostViewedVideos: EntityViewsInfo[]
-}
-export type GetVideoViewsArgs = {
-  videoId: string
-}
-export type GetMostViewedVideosArgs = {
-  timePeriodDays: number
+export type GetMostViewedVideosConnection = {
+  mostViewedVideosConnection: VideoConnection
 }
-export type GetMostViewedVideosAllTimeArgs = {
+export type GetMostViewedVideosConnectionArgs = {
+  periodDays: number | null
   limit: number
 }
-export type GetMostViewedVideosAllTime = {
-  mostViewedVideosAllTime: EntityViewsInfo[]
-}
 
-export const GET_CHANNEL_VIEWS = gql`
-  query GetChannelViews($channelId: ID!) {
-    channelViews(channelId: $channelId) {
-      id
-      views
-    }
-  }
-`
-
-export const GET_MOST_VIEWED_CHANNELS = gql`
-  query GetMostViewedChannels($timePeriodDays: Int!) {
-    mostViewedChannels(timePeriodDays: $timePeriodDays) {
-      id
-      views
+export const GET_MOST_VIEWED_CHANNELS_CONNECTION = gql`
+  query GetMostViewedChannelsConnection($periodDays: Int, $limit: Int!) {
+    mostViewedChannelsConnection(periodDays: $periodDays, limit: $limit) {
+      edges {
+        node {
+          id
+          views
+        }
+      }
     }
   }
 `
 
-export const GET_MOST_VIEWED_CHANNELS_ALL_TIME = gql`
-  query GetMostViewedVideosAllTime($limit: Int!) {
-    mostViewedChannelsAllTime(limit: $limit) {
-      id
-      views
-    }
-  }
-`
-export type GetChannelViews = {
-  channelViews: EntityViewsInfo | null
-}
-export type GetMostViewedChannels = {
-  mostViewedChannels: EntityViewsInfo[]
-}
-export type GetChannelViewsArgs = {
-  channelId: string
-}
-export type GetMostViewedChannelsArgs = {
-  timePeriodDays: number
+export type GetMostViewedChannelsConnection = {
+  mostViewedChannelsConnection: ChannelConnection
 }
-export type GetMostViewedChannelsAllTimeArgs = {
+
+export type GetMostViewedChannelsConnectionArgs = {
+  periodDays: number | null
   limit: number
 }
-export type GetMostViewedChannelsAllTime = {
-  mostViewedChannelsAllTime: EntityViewsInfo[]
-}
 
 export const GET_MOST_VIEWED_CATEGORIES = gql`
   query GetMostViewedCategories($timePeriodDays: Int!) {

+ 1 - 1
tests/server.test.ts

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

+ 2 - 0
tests/setup.ts

@@ -1,3 +1,5 @@
 export const TEST_BUCKET_SIZE = 20
 
 jest.mock('../src/config', () => ({ bucketSize: TEST_BUCKET_SIZE }))
+
+process.env.ORION_QUERY_NODE_URL = 'https://sumer-dev-2.joystream.app/query/server/graphql'

+ 84 - 125
tests/views.test.ts

@@ -6,26 +6,14 @@ import {
   ADD_VIDEO_VIEW,
   AddVideoView,
   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_CHANNELS_CONNECTION,
+  GET_MOST_VIEWED_VIDEOS_CONNECTION,
   GET_MOST_VIEWED_CATEGORIES,
   GET_MOST_VIEWED_CATEGORIES_ALL_TIME,
-  GetChannelViews,
-  GetChannelViewsArgs,
-  GetVideoViews,
-  GetVideoViewsArgs,
-  GetMostViewedVideosArgs,
-  GetMostViewedVideosAllTimeArgs,
-  GetMostViewedChannelsArgs,
-  GetMostViewedChannelsAllTimeArgs,
-  GetMostViewedVideos,
-  GetMostViewedVideosAllTime,
-  GetMostViewedChannels,
-  GetMostViewedChannelsAllTime,
+  GetMostViewedVideosConnectionArgs,
+  GetMostViewedChannelsConnectionArgs,
+  GetMostViewedVideosConnection,
+  GetMostViewedChannelsConnection,
   GetMostViewedCategoriesArgs,
   GetMostViewedCategoriesAllTimeArgs,
   GetMostViewedCategories,
@@ -52,7 +40,7 @@ describe('Video and channel views resolver', () => {
   beforeEach(async () => {
     mongoose = await connectMongoose(process.env.MONGO_URL!)
     aggregates = await buildAggregates()
-    server = await createServer(mongoose, aggregates)
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
     await server.start()
     query = createQueryFn(server)
     mutate = createMutationFn(server)
@@ -73,61 +61,25 @@ describe('Video and channel views resolver', () => {
     return addVideoViewResponse.data?.addVideoView
   }
 
-  const getVideoViews = async (videoId: string) => {
-    const videoViewsResponse = await query<GetVideoViews, GetVideoViewsArgs>({
-      query: GET_VIDEO_VIEWS,
-      variables: { videoId },
-    })
-    expect(videoViewsResponse.errors).toBeUndefined()
-    return videoViewsResponse.data?.videoViews
-  }
-
-  const getMostViewedVideos = async (timePeriodDays: number) => {
-    const mostViewedVideosResponse = await query<GetMostViewedVideos, GetMostViewedVideosArgs>({
-      query: GET_MOST_VIEWED_VIDEOS,
-      variables: { timePeriodDays },
+  const getMostViewedVideos = async (periodDays: 7 | 30 | null) => {
+    const mostViewedVideosResponse = await query<GetMostViewedVideosConnection, GetMostViewedVideosConnectionArgs>({
+      query: GET_MOST_VIEWED_VIDEOS_CONNECTION,
+      variables: { periodDays, limit: 10 },
     })
     expect(mostViewedVideosResponse.errors).toBeUndefined()
-    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,
-      variables: { channelId },
-    })
-    expect(channelViewsResponse.errors).toBeUndefined()
-    return channelViewsResponse.data?.channelViews
+    return mostViewedVideosResponse.data?.mostViewedVideosConnection
   }
 
-  const getMostViewedChannels = async (timePeriodDays: number) => {
-    const mostViewedChannelsResponse = await query<GetMostViewedChannels, GetMostViewedChannelsArgs>({
-      query: GET_MOST_VIEWED_CHANNELS,
-      variables: { timePeriodDays },
-    })
-    expect(mostViewedChannelsResponse.errors).toBeUndefined()
-    return mostViewedChannelsResponse.data?.mostViewedChannels
-  }
-
-  const getMostViewedChannelsAllTime = async (limit: number) => {
-    const mostViewedChannelsAllTimeResponse = await query<
-      GetMostViewedChannelsAllTime,
-      GetMostViewedChannelsAllTimeArgs
+  const getMostViewedChannels = async (periodDays: 7 | 30 | null) => {
+    const mostViewedChannelsResponse = await query<
+      GetMostViewedChannelsConnection,
+      GetMostViewedChannelsConnectionArgs
     >({
-      query: GET_MOST_VIEWED_CHANNELS_ALL_TIME,
-      variables: { limit },
+      query: GET_MOST_VIEWED_CHANNELS_CONNECTION,
+      variables: { periodDays, limit: 10 },
     })
-    expect(mostViewedChannelsAllTimeResponse.errors).toBeUndefined()
-    return mostViewedChannelsAllTimeResponse.data?.mostViewedChannelsAllTime
+    expect(mostViewedChannelsResponse.errors).toBeUndefined()
+    return mostViewedChannelsResponse.data?.mostViewedChannelsConnection
   }
 
   const getMostViewedCategories = async (timePeriodDays: number) => {
@@ -152,21 +104,17 @@ describe('Video and channel views resolver', () => {
   }
 
   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 mostViewedVideosAllTime = await getMostViewedVideos(null)
     const mostViewedChannels = await getMostViewedChannels(30)
-    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
+    const mostViewedChannelsAllTime = await getMostViewedChannels(null)
     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(mostViewedVideos?.edges).toHaveLength(0)
+    expect(mostViewedVideosAllTime?.edges).toHaveLength(0)
+    expect(mostViewedChannels?.edges).toHaveLength(0)
+    expect(mostViewedChannelsAllTime?.edges).toHaveLength(0)
     expect(mostViewedCategories).toHaveLength(0)
     expect(mostViewedCategoriesAllTime).toHaveLength(0)
   })
@@ -184,22 +132,27 @@ describe('Video and channel views resolver', () => {
       id: FIRST_CATEGORY_ID,
       views: 1,
     }
+
+    const expectedMostViewedVideos = {
+      edges: [expectedVideoViews].map((view) => ({ node: view })),
+    }
+
+    const expectedMostViewedChannels = {
+      edges: [expectedChannelViews].map((view) => ({ node: view })),
+    }
+
     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 mostViewedVideosAllTime = await getMostViewedVideos(null)
       const mostViewedChannels = await getMostViewedChannels(30)
-      const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
+      const mostViewedChannelsAllTime = await getMostViewedChannels(null)
       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(mostViewedVideos).toEqual(expectedMostViewedVideos)
+      expect(mostViewedVideosAllTime).toEqual(expectedMostViewedVideos)
+      expect(mostViewedChannels).toEqual(expectedMostViewedChannels)
+      expect(mostViewedChannelsAllTime).toEqual(expectedMostViewedChannels)
       expect(mostViewedCategories).toEqual([expectedCategoryViews])
       expect(mostViewedCategoriesAllTime).toEqual([expectedCategoryViews])
     }
@@ -229,6 +182,10 @@ describe('Video and channel views resolver', () => {
       views: 1,
     }
 
+    const expectedMostViewedVideos = {
+      edges: [expectedFirstVideoViews, expectedSecondVideoViews].map((view) => ({ node: view })),
+    }
+
     const addFirstVideoViewData = await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
     const addSecondVideoViewData = await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
 
@@ -239,15 +196,11 @@ describe('Video and channel views resolver', () => {
 
     await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
 
-    const firstVideoViews = await getVideoViews(FIRST_VIDEO_ID)
-    const secondVideoViews = await getVideoViews(SECOND_VIDEO_ID)
     const mostViewedVideos = await getMostViewedVideos(30)
-    const mostViewedVideosAllTime = await getMostViewedVideosAllTime(10)
+    const mostViewedVideosAllTime = await getMostViewedVideos(null)
 
-    expect(firstVideoViews).toEqual(expectedFirstVideoViews)
-    expect(secondVideoViews).toEqual(expectedSecondVideoViews)
-    expect(mostViewedVideos).toEqual([expectedFirstVideoViews, expectedSecondVideoViews])
-    expect(mostViewedVideosAllTime).toEqual([expectedFirstVideoViews, expectedSecondVideoViews])
+    expect(mostViewedVideos).toEqual(expectedMostViewedVideos)
+    expect(mostViewedVideosAllTime).toEqual(expectedMostViewedVideos)
   })
 
   it('should distinct views of separate channels', async () => {
@@ -260,18 +213,18 @@ describe('Video and channel views resolver', () => {
       views: 1,
     }
 
+    const expectedMostViewedChannels = {
+      edges: [expectedFirstChanelViews, expectedSecondChannelViews].map((view) => ({ node: view })),
+    }
+
     await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
     await addVideoView(SECOND_VIDEO_ID, SECOND_CHANNEL_ID)
 
-    const firstChannelViews = await getChannelViews(FIRST_CHANNEL_ID)
-    const secondChannelViews = await getChannelViews(SECOND_CHANNEL_ID)
     const mostViewedChannels = await getMostViewedChannels(30)
-    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
+    const mostViewedChannelsAllTime = await getMostViewedChannels(null)
 
-    expect(firstChannelViews).toEqual(expectedFirstChanelViews)
-    expect(secondChannelViews).toEqual(expectedSecondChannelViews)
-    expect(mostViewedChannels).toEqual([expectedFirstChanelViews, expectedSecondChannelViews])
-    expect(mostViewedChannelsAllTime).toEqual([expectedFirstChanelViews, expectedSecondChannelViews])
+    expect(mostViewedChannels).toEqual(expectedMostViewedChannels)
+    expect(mostViewedChannelsAllTime).toEqual(expectedMostViewedChannels)
   })
 
   it('should properly aggregate views of a channel', async () => {
@@ -283,13 +236,15 @@ describe('Video and channel views resolver', () => {
     await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
     await addVideoView(SECOND_VIDEO_ID, FIRST_CHANNEL_ID)
 
-    const channelViews = await getChannelViews(FIRST_CHANNEL_ID)
+    const expectedMostViewedChannels = {
+      edges: [expectedChannelViews].map((view) => ({ node: view })),
+    }
+
     const mostViewedChannels = await getMostViewedChannels(30)
-    const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
+    const mostViewedChannelsAllTime = await getMostViewedChannels(null)
 
-    expect(channelViews).toEqual(expectedChannelViews)
-    expect(mostViewedChannels).toEqual([expectedChannelViews])
-    expect(mostViewedChannelsAllTime).toEqual([expectedChannelViews])
+    expect(mostViewedChannels).toEqual(expectedMostViewedChannels)
+    expect(mostViewedChannelsAllTime).toEqual(expectedMostViewedChannels)
   })
 
   it('should properly aggregate views of a category', async () => {
@@ -326,24 +281,26 @@ describe('Video and channel views resolver', () => {
       views: 7,
     }
 
+    const expectedMostViewedVideos = {
+      edges: [expectedFirstVideoViews, expectedSecondVideoViews].map((view) => ({ node: view })),
+    }
+
+    const expectedMostViewedChannels = {
+      edges: [expectedChannelViews].map((view) => ({ node: view })),
+    }
+
     const checkViews = async () => {
-      const firstVideoViews = await getVideoViews(FIRST_VIDEO_ID)
-      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 mostViewedVideosAllTime = await getMostViewedVideos(null)
       const mostViewedChannels = await getMostViewedChannels(30)
-      const mostViewedChannelsAllTime = await getMostViewedChannelsAllTime(10)
+      const mostViewedChannelsAllTime = await getMostViewedChannels(null)
       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(mostViewedVideos).toEqual(expectedMostViewedVideos)
+      expect(mostViewedVideosAllTime).toEqual(expectedMostViewedVideos)
+      expect(mostViewedChannels).toEqual(expectedMostViewedChannels)
+      expect(mostViewedChannelsAllTime).toEqual(expectedMostViewedChannels)
       expect(mostViewedCategories).toEqual([expectedCategoryViews])
       expect(mostViewedCategoriesAllTime).toEqual([expectedCategoryViews])
     }
@@ -361,7 +318,7 @@ describe('Video and channel views resolver', () => {
 
     await server.stop()
     aggregates = await buildAggregates()
-    server = await createServer(mongoose, aggregates)
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
     query = createQueryFn(server)
     mutate = createMutationFn(server)
 
@@ -375,15 +332,17 @@ describe('Video and channel views resolver', () => {
       views: eventsCount,
     }
 
+    const expectedMostViewedVideos = {
+      edges: [expectedVideoViews].map((view) => ({ node: view })),
+    }
+
     for (let i = 0; i < eventsCount; i++) {
       await addVideoView(FIRST_VIDEO_ID, FIRST_CHANNEL_ID)
     }
 
-    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])
+    const mostViewedVideosAllTime = await getMostViewedVideos(null)
+    expect(mostViewedVideos).toEqual(expectedMostViewedVideos)
+    expect(mostViewedVideosAllTime).toEqual(expectedMostViewedVideos)
   })
 })

+ 2 - 2
tsconfig.json

@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es2019",
-    "lib": ["dom", "es2019", "esnext.asynciterable"],
+    "target": "esnext",
+    "lib": ["dom", "esnext"],
     "esModuleInterop": true,
     "allowSyntheticDefaultImports": true,
     "forceConsistentCasingInFileNames": true,

+ 333 - 8
yarn.lock

@@ -551,6 +551,68 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
+"@graphql-tools/batch-delegate@8.2.1":
+  version "8.2.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/batch-delegate/-/batch-delegate-8.2.1.tgz#e6e4b5364ffa66ea9f7c28d5f5ad13cfadcb0f7e"
+  integrity sha512-gE4zLRzJnWQeg3GySjPgY9s4YmCXsR/b13lLIJKGupT3yhdSgGov687ZAaOixRZY/PrOlE3BGBoaFmi7/IowCw==
+  dependencies:
+    "@graphql-tools/delegate" "^8.4.1"
+    "@graphql-tools/utils" "^8.5.1"
+    dataloader "2.0.0"
+    tslib "~2.3.0"
+
+"@graphql-tools/batch-execute@^8.3.1":
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.3.1.tgz#0b74c54db5ac1c5b9a273baefc034c2343ebbb74"
+  integrity sha512-63kHY8ZdoO5FoeDXYHnAak1R3ysMViMPwWC2XUblFckuVLMUPmB2ONje8rjr2CvzWBHAW8c1Zsex+U3xhKtGIA==
+  dependencies:
+    "@graphql-tools/utils" "^8.5.1"
+    dataloader "2.0.0"
+    tslib "~2.3.0"
+    value-or-promise "1.0.11"
+
+"@graphql-tools/delegate@^8.4.1":
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-8.4.1.tgz#f113e6a4d39c90ad955fac09b4f0ad07fbd96551"
+  integrity sha512-Oz1DYXmAKPKgHNsFnflg+CXKRVoPJI3AGRkcaRR0kA/4VWlMiKonL2O0BZdoRd0IbtZimBeQCrDEb5TikjIv0g==
+  dependencies:
+    "@graphql-tools/batch-execute" "^8.3.1"
+    "@graphql-tools/schema" "^8.3.1"
+    "@graphql-tools/utils" "^8.5.1"
+    dataloader "2.0.0"
+    tslib "~2.3.0"
+    value-or-promise "1.0.11"
+
+"@graphql-tools/graphql-file-loader@^7.3.3":
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.3.3.tgz#7cee2f84f08dc13fa756820b510248b857583d36"
+  integrity sha512-6kUJZiNpYKVhum9E5wfl5PyLLupEDYdH7c8l6oMrk6c7EPEVs6iSUyB7yQoWrtJccJLULBW2CRQ5IHp5JYK0mA==
+  dependencies:
+    "@graphql-tools/import" "^6.5.7"
+    "@graphql-tools/utils" "^8.5.1"
+    globby "^11.0.3"
+    tslib "~2.3.0"
+    unixify "^1.0.0"
+
+"@graphql-tools/import@^6.5.7":
+  version "6.6.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-6.6.1.tgz#2a7e1ceda10103ffeb8652a48ddc47150b035485"
+  integrity sha512-i9WA6k+erJMci822o9w9DoX+uncVBK60LGGYW8mdbhX0l7wEubUpA000thJ1aarCusYh0u+ZT9qX0HyVPXu25Q==
+  dependencies:
+    "@graphql-tools/utils" "8.5.3"
+    resolve-from "5.0.0"
+    tslib "~2.3.0"
+
+"@graphql-tools/load@^7.4.1":
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-7.4.1.tgz#aa572fcef11d6028097b6ef39c13fa9d62e5a441"
+  integrity sha512-UvBodW5hRHpgBUBVz5K5VIhJDOTFIbRRAGD6sQ2l9J5FDKBEs3u/6JjZDzbdL96br94D5cEd2Tk6auaHpTn7mQ==
+  dependencies:
+    "@graphql-tools/schema" "8.3.1"
+    "@graphql-tools/utils" "^8.5.1"
+    p-limit "3.1.0"
+    tslib "~2.3.0"
+
 "@graphql-tools/merge@^8.1.0":
   version "8.1.2"
   resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.1.2.tgz#50f5763927c51de764d09c5bfd20261671976e24"
@@ -559,6 +621,14 @@
     "@graphql-tools/utils" "^8.2.2"
     tslib "~2.3.0"
 
+"@graphql-tools/merge@^8.2.1":
+  version "8.2.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.2.1.tgz#bf83aa06a0cfc6a839e52a58057a84498d0d51ff"
+  integrity sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA==
+  dependencies:
+    "@graphql-tools/utils" "^8.5.1"
+    tslib "~2.3.0"
+
 "@graphql-tools/mock@^8.1.2":
   version "8.4.0"
   resolved "https://registry.yarnpkg.com/@graphql-tools/mock/-/mock-8.4.0.tgz#c4a5d0c35cc5760b99b3e062145f36ac9cfe9ffb"
@@ -569,6 +639,16 @@
     fast-json-stable-stringify "^2.1.0"
     tslib "~2.3.0"
 
+"@graphql-tools/schema@8.3.1", "@graphql-tools/schema@^8.3.1":
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.3.1.tgz#1ee9da494d2da457643b3c93502b94c3c4b68c74"
+  integrity sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ==
+  dependencies:
+    "@graphql-tools/merge" "^8.2.1"
+    "@graphql-tools/utils" "^8.5.1"
+    tslib "~2.3.0"
+    value-or-promise "1.0.11"
+
 "@graphql-tools/schema@^8.0.0", "@graphql-tools/schema@^8.2.0":
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.2.0.tgz#ae75cbb2df6cee9ed6d89fce56be467ab23758dc"
@@ -579,6 +659,51 @@
     tslib "~2.3.0"
     value-or-promise "1.0.10"
 
+"@graphql-tools/stitch@^8.4.1":
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/stitch/-/stitch-8.4.1.tgz#4db431a3f5ec3b31a022deb3386ce344bceca2a6"
+  integrity sha512-WfbWyTo+6Jo9dLvfmIubnmpwUbma2WU1Ygnv/ePxgTDZfcnRbpYi/dAMOZ0Aqq18ecui8RwoyzR+VjmTrQjecg==
+  dependencies:
+    "@graphql-tools/batch-delegate" "8.2.1"
+    "@graphql-tools/delegate" "^8.4.1"
+    "@graphql-tools/merge" "^8.2.1"
+    "@graphql-tools/schema" "^8.3.1"
+    "@graphql-tools/utils" "^8.5.1"
+    "@graphql-tools/wrap" "^8.3.1"
+    tslib "~2.3.0"
+
+"@graphql-tools/url-loader@^7.5.2":
+  version "7.5.2"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-7.5.2.tgz#fb3737fd1269ab61b195b63052179b6049d90ce1"
+  integrity sha512-EilHqbhUY/qg55SSEdklDhPXgSz9+9a63SX3mcD8J2qwZHJD/wOLcyKs8m6BXfuGwUiuB0j3fmDSEVmva2onBg==
+  dependencies:
+    "@graphql-tools/delegate" "^8.4.1"
+    "@graphql-tools/utils" "^8.5.1"
+    "@graphql-tools/wrap" "^8.3.1"
+    "@n1ru4l/graphql-live-query" "0.9.0"
+    "@types/websocket" "1.0.4"
+    "@types/ws" "^8.0.0"
+    cross-undici-fetch "^0.0.20"
+    dset "^3.1.0"
+    extract-files "11.0.0"
+    graphql-sse "^1.0.1"
+    graphql-ws "^5.4.1"
+    isomorphic-ws "4.0.1"
+    meros "1.1.4"
+    subscriptions-transport-ws "^0.11.0"
+    sync-fetch "0.3.1"
+    tslib "~2.3.0"
+    valid-url "1.0.9"
+    value-or-promise "1.0.11"
+    ws "8.2.3"
+
+"@graphql-tools/utils@8.5.3":
+  version "8.5.3"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.5.3.tgz#404062e62cae9453501197039687749c4885356e"
+  integrity sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg==
+  dependencies:
+    tslib "~2.3.0"
+
 "@graphql-tools/utils@^8.0.0", "@graphql-tools/utils@^8.2.0", "@graphql-tools/utils@^8.2.2", "@graphql-tools/utils@^8.2.3":
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.3.0.tgz#382111bc4a93f248e3641740a5300f44286bffae"
@@ -586,6 +711,24 @@
   dependencies:
     tslib "~2.3.0"
 
+"@graphql-tools/utils@^8.5.1":
+  version "8.5.2"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.5.2.tgz#fa775d92c19237f648105f7d4aeeeb63ba3d257f"
+  integrity sha512-wxA51td/759nQziPYh+HxE0WbURRufrp1lwfOYMgfK4e8Aa6gCa1P1p6ERogUIm423NrIfOVau19Q/BBpHdolw==
+  dependencies:
+    tslib "~2.3.0"
+
+"@graphql-tools/wrap@^8.3.1":
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-8.3.1.tgz#53376227b2b6dc1b09199b10c0a93ee1800a91b2"
+  integrity sha512-vcbxawe4gFLuzvT9/CrZhauv9Rk9bMqZIwxgS/w3DWN+G7o6+fNxIv6LYt0acE5+rHJOF6+r3lX/SgFgPyoWww==
+  dependencies:
+    "@graphql-tools/delegate" "^8.4.1"
+    "@graphql-tools/schema" "^8.3.1"
+    "@graphql-tools/utils" "^8.5.1"
+    tslib "~2.3.0"
+    value-or-promise "1.0.11"
+
 "@humanwhocodes/config-array@^0.5.0":
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -812,6 +955,11 @@
   resolved "https://registry.yarnpkg.com/@joystream/prettier-config/-/prettier-config-1.0.0.tgz#d87c6370244f39281d9052c619977ca60f6e21cd"
   integrity sha512-ZY2H1PH05Yhn22B7G8ilLyIT9LxQ9b/PyErkPx8OOW+znary5QgExMhgBl1Gmv3k9m/QX1qIxKHZveO4R2yfeA==
 
+"@n1ru4l/graphql-live-query@0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc"
+  integrity sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg==
+
 "@nodelib/fs.scandir@2.1.3":
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@@ -1108,6 +1256,11 @@
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
+"@types/lodash@^4.14.178":
+  version "4.14.178"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
+  integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
+
 "@types/long@^4.0.0":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -1196,6 +1349,13 @@
   resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e"
   integrity sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q==
 
+"@types/websocket@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
+  integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/whatwg-url@^8.2.1":
   version "8.2.1"
   resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.1.tgz#f1aac222dab7c59e011663a0cb0a3117b2ef05d4"
@@ -1204,6 +1364,13 @@
     "@types/node" "*"
     "@types/webidl-conversions" "*"
 
+"@types/ws@^8.0.0":
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
+  integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/yargs-parser@*":
   version "20.2.0"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
@@ -1291,6 +1458,13 @@ abab@^2.0.3, abab@^2.0.5:
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
   integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
 
+abort-controller@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+  dependencies:
+    event-target-shim "^5.0.0"
+
 accepts@^1.3.5, accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -1661,6 +1835,11 @@ babel-preset-jest@^27.2.0:
     babel-plugin-jest-hoist "^27.2.0"
     babel-preset-current-node-syntax "^1.0.0"
 
+backo2@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+  integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -1776,7 +1955,7 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
-buffer@^5.5.0, buffer@^5.6.0:
+buffer@^5.5.0, buffer@^5.6.0, buffer@^5.7.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
   integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -2042,6 +2221,16 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+cross-undici-fetch@^0.0.20:
+  version "0.0.20"
+  resolved "https://registry.yarnpkg.com/cross-undici-fetch/-/cross-undici-fetch-0.0.20.tgz#6b7c5ac82a3601edd439f37275ac0319d77a120a"
+  integrity sha512-5d3WBC4VRHpFndECK9bx4TngXrw0OUXdhX561Ty1ZoqMASz9uf55BblhTC1CO6GhMWnvk9SOqYEXQliq6D2P4A==
+  dependencies:
+    abort-controller "^3.0.0"
+    form-data "^4.0.0"
+    node-fetch "^2.6.5"
+    undici "^4.9.3"
+
 cssfilter@0.0.10:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
@@ -2073,6 +2262,11 @@ data-urls@^2.0.0:
     whatwg-mimetype "^2.3.0"
     whatwg-url "^8.0.0"
 
+dataloader@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f"
+  integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==
+
 date-fns@^2.25.0:
   version "2.25.0"
   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
@@ -2214,6 +2408,11 @@ dotenv@^10.0.0:
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
   integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
 
+dset@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.1.tgz#07de5af7a8d03eab337ad1a8ba77fe17bba61a8c"
+  integrity sha512-hYf+jZNNqJBD2GiMYb+5mqOIX4R4RRHXU3qWMWYN+rqcR2/YpRL2bUHr8C8fU+5DNvqYjJ8YvMGSLuVPWU1cNg==
+
 dynamic-dedupe@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1"
@@ -2592,6 +2791,16 @@ etag@~1.8.1:
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
+event-target-shim@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+eventemitter3@^3.1.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
+  integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
+
 execa@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
@@ -2675,6 +2884,11 @@ express@^4.17.1:
     utils-merge "1.0.1"
     vary "~1.1.2"
 
+extract-files@11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a"
+  integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==
+
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -2815,6 +3029,15 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -2954,6 +3177,11 @@ graphql-query-complexity@^0.7.0:
   dependencies:
     lodash.get "^4.4.2"
 
+graphql-sse@^1.0.1:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/graphql-sse/-/graphql-sse-1.0.6.tgz#4f98e0a06f2020542ed054399116108491263224"
+  integrity sha512-y2mVBN2KwNrzxX2KBncQ6kzc6JWvecxuBernrl0j65hsr6MAS3+Yn8PTFSOgRmtolxugepxveyZVQEuaNEbw3w==
+
 graphql-subscriptions@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11"
@@ -2968,6 +3196,11 @@ graphql-tag@^2.11.0:
   dependencies:
     tslib "^2.1.0"
 
+graphql-ws@^5.4.1:
+  version "5.5.5"
+  resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.5.5.tgz#f375486d3f196e2a2527b503644693ae3a8670a9"
+  integrity sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw==
+
 graphql@15:
   version "15.4.0"
   resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347"
@@ -3279,6 +3512,11 @@ isexe@^2.0.0:
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
+isomorphic-ws@4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
+  integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+
 istanbul-lib-coverage@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
@@ -3955,7 +4193,7 @@ lodash.truncate@^4.4.2:
   resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
 
-lodash@4.x, lodash@^4.7.0:
+lodash@4.x, lodash@^4.17.21, lodash@^4.7.0:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -4060,6 +4298,11 @@ merge2@^1.3.0:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
+meros@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948"
+  integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -4263,6 +4506,13 @@ node-fetch@^2.6.1:
   dependencies:
     whatwg-url "^5.0.0"
 
+node-fetch@^2.6.5:
+  version "2.6.6"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
+  integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
+  dependencies:
+    whatwg-url "^5.0.0"
+
 node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -4288,6 +4538,13 @@ normalize-package-data@^2.3.2:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -4416,6 +4673,13 @@ optionator@^0.9.1:
     type-check "^0.4.0"
     word-wrap "^1.2.3"
 
+p-limit@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
 p-limit@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -4774,6 +5038,11 @@ regexpp@^3.0.0, regexpp@^3.1.0:
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
   integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
 
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
 require-at@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a"
@@ -4796,16 +5065,16 @@ resolve-cwd@^3.0.0:
   dependencies:
     resolve-from "^5.0.0"
 
+resolve-from@5.0.0, resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
-resolve-from@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
-  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-
 resolve@^1.0.0, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.13.1, resolve@^1.17.0:
   version "1.17.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
@@ -5237,6 +5506,17 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
+subscriptions-transport-ws@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883"
+  integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==
+  dependencies:
+    backo2 "^1.0.2"
+    eventemitter3 "^3.1.0"
+    iterall "^1.2.1"
+    symbol-observable "^1.0.4"
+    ws "^5.2.0 || ^6.0.0 || ^7.0.0"
+
 supports-color@^5.3.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -5266,11 +5546,24 @@ supports-hyperlinks@^2.0.0:
     has-flag "^4.0.0"
     supports-color "^7.0.0"
 
+symbol-observable@^1.0.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+  integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+
 symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
   integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
 
+sync-fetch@0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/sync-fetch/-/sync-fetch-0.3.1.tgz#62aa82c4b4d43afd6906bfd7b5f92056458509f0"
+  integrity sha512-xj5qiCDap/03kpci5a+qc5wSJjc8ZSixgG2EUmH1B8Ea2sfWclQA7eH40hiHPCtkCn6MCk4Wb+dqcXdCy2PP3g==
+  dependencies:
+    buffer "^5.7.0"
+    node-fetch "^2.6.1"
+
 table@^6.0.9:
   version "6.7.2"
   resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0"
@@ -5557,11 +5850,23 @@ typescript@^4.4.4:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
   integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
 
+undici@^4.9.3:
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/undici/-/undici-4.9.5.tgz#6531b6b2587c2c42d77c0dded83d058a328775f8"
+  integrity sha512-t59IFVYiMnFThboJL9izqwsDEfSbZDPZ/8iCYBCkEFLy63x9m4YaNt0E+r5+X993syC9M0W/ksusZC9YuAamMg==
+
 universalify@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
 
+unixify@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090"
+  integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA=
+  dependencies:
+    normalize-path "^2.1.1"
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -5608,6 +5913,11 @@ v8-to-istanbul@^8.1.0:
     convert-source-map "^1.6.0"
     source-map "^0.7.3"
 
+valid-url@1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
+  integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -5626,6 +5936,11 @@ value-or-promise@1.0.10:
   resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.10.tgz#5bf041f1e9a8e7043911875547636768a836e446"
   integrity sha512-1OwTzvcfXkAfabk60UVr5NdjtjJ0Fg0T5+B1bhxtrOEwSH2fe8y4DnLgoksfCyd8yZCOQQHB0qLMQnwgCjbXLQ==
 
+value-or-promise@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140"
+  integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==
+
 vary@^1, vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -5763,7 +6078,12 @@ write-file-atomic@^3.0.0:
     signal-exit "^3.0.2"
     typedarray-to-buffer "^3.1.5"
 
-ws@^7.4.6:
+ws@8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+"ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.4.6:
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
   integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
@@ -5841,3 +6161,8 @@ yn@3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
   integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==