diff --git a/src/cron/updateHighlightedViews.ts b/src/cron/updateHighlightedViews.ts index a6a9520cf7..944e3f4201 100644 --- a/src/cron/updateHighlightedViews.ts +++ b/src/cron/updateHighlightedViews.ts @@ -8,6 +8,10 @@ import { TrendingSource } from '../entity/TrendingSource'; import { TrendingTag } from '../entity/TrendingTag'; import { PopularVideoPost } from '../entity/PopularVideoPost'; import { UserStats } from '../entity'; +import { TrendingUserPost } from '../entity/TrendingUserPost'; +import { TrendingUserSource } from '../entity/TrendingUserSource'; +import { PopularUserPost } from '../entity/PopularUserPost'; +import { PopularUserSource } from '../entity/PopularUserSource'; const cron: Cron = { name: 'update-highlighted-views', @@ -15,9 +19,13 @@ const cron: Cron = { const viewsToRefresh = [ TrendingPost, TrendingSource, + TrendingUserPost, + TrendingUserSource, TrendingTag, PopularPost, PopularSource, + PopularUserPost, + PopularUserSource, PopularTag, PopularVideoPost, PopularVideoSource, diff --git a/src/entity/PopularPost.ts b/src/entity/PopularPost.ts index 7713c6fb73..3f0a2b2834 100644 --- a/src/entity/PopularPost.ts +++ b/src/entity/PopularPost.ts @@ -1,18 +1,21 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { SourceType } from './Source'; @ViewEntity({ materialized: true, expression: (dataSource: DataSource) => dataSource .createQueryBuilder() - .select('"sourceId"') - .addSelect('"tagsStr"') - .addSelect('"createdAt"') - .addSelect('upvotes - downvotes r') + .select('p."sourceId"') + .addSelect('p."tagsStr"') + .addSelect('p."createdAt"') + .addSelect('p.upvotes - p.downvotes', 'r') .from('post', 'p') + .innerJoin('source', 's', 's.id = p."sourceId"') .where( - 'not p.private and p."createdAt" > now() - interval \'60 day\' and upvotes > downvotes', + 'not p.private and p."createdAt" > now() - interval \'60 day\' and p.upvotes > p.downvotes', ) + .andWhere('s.type != :type', { type: SourceType.User }) .orderBy('r', 'DESC') .limit(1000), }) diff --git a/src/entity/PopularUserPost.ts b/src/entity/PopularUserPost.ts new file mode 100644 index 0000000000..23e917407d --- /dev/null +++ b/src/entity/PopularUserPost.ts @@ -0,0 +1,34 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { SourceType } from './Source'; + +@ViewEntity({ + materialized: true, + expression: (dataSource: DataSource) => + dataSource + .createQueryBuilder() + .select('p."sourceId"') + .addSelect('p."tagsStr"') + .addSelect('p."createdAt"') + .addSelect('p.upvotes - p.downvotes', 'r') + .from('post', 'p') + .innerJoin('source', 's', 's.id = p."sourceId"') + .where( + 'not p.private and p."createdAt" > now() - interval \'60 day\' and p.upvotes > p.downvotes', + ) + .andWhere('s.type = :type', { type: SourceType.User }) + .orderBy('r', 'DESC') + .limit(1000), +}) +export class PopularUserPost { + @ViewColumn() + sourceId: string; + + @ViewColumn() + tagsStr: string; + + @ViewColumn() + createdAt: Date; + + @ViewColumn() + r: number; +} diff --git a/src/entity/PopularUserSource.ts b/src/entity/PopularUserSource.ts new file mode 100644 index 0000000000..0a53a4a1ae --- /dev/null +++ b/src/entity/PopularUserSource.ts @@ -0,0 +1,22 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { PopularUserPost } from './PopularUserPost'; + +@ViewEntity({ + materialized: true, + expression: (dataSource: DataSource) => + dataSource + .createQueryBuilder() + .select('"sourceId"') + .addSelect('avg(r) r') + .from(PopularUserPost, 'base') + .groupBy('"sourceId"') + .having('count(*) > 5') + .orderBy('r', 'DESC'), +}) +export class PopularUserSource { + @ViewColumn() + sourceId: string; + + @ViewColumn() + r: number; +} diff --git a/src/entity/TrendingPost.ts b/src/entity/TrendingPost.ts index 4812e9dfa1..7dfdcbfa93 100644 --- a/src/entity/TrendingPost.ts +++ b/src/entity/TrendingPost.ts @@ -1,20 +1,24 @@ import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { SourceType } from './Source'; @ViewEntity({ materialized: true, expression: (dataSource: DataSource) => dataSource .createQueryBuilder() - .select('"sourceId"') - .addSelect('"tagsStr"') - .addSelect('"createdAt"') + .select('p."sourceId"') + .addSelect('p."tagsStr"') + .addSelect('p."createdAt"') .addSelect( - `log(10, upvotes - downvotes) + extract(epoch from ("createdAt" - now() + interval '7 days')) / 200000 r`, + `log(10, p.upvotes - p.downvotes) + extract(epoch from (p."createdAt" - now() + interval '7 days')) / 200000`, + 'r', ) .from('post', 'p') + .innerJoin('source', 's', 's.id = p."sourceId"') .where( - `not p.private and p."createdAt" > now() - interval '7 day' and upvotes > downvotes`, + `not p.private and p."createdAt" > now() - interval '7 day' and p.upvotes > p.downvotes`, ) + .andWhere('s.type != :type', { type: SourceType.User }) .orderBy('r', 'DESC') .limit(100), }) diff --git a/src/entity/TrendingUserPost.ts b/src/entity/TrendingUserPost.ts new file mode 100644 index 0000000000..da43e27a63 --- /dev/null +++ b/src/entity/TrendingUserPost.ts @@ -0,0 +1,37 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { SourceType } from './Source'; + +@ViewEntity({ + materialized: true, + expression: (dataSource: DataSource) => + dataSource + .createQueryBuilder() + .select('p."sourceId"') + .addSelect('p."tagsStr"') + .addSelect('p."createdAt"') + .addSelect( + `log(10, p.upvotes - p.downvotes) + extract(epoch from (p."createdAt" - now() + interval '7 days')) / 200000`, + 'r', + ) + .from('post', 'p') + .innerJoin('source', 's', 's.id = p."sourceId"') + .where( + `not p.private and p."createdAt" > now() - interval '7 day' and p.upvotes > p.downvotes`, + ) + .andWhere('s.type = :type', { type: SourceType.User }) + .orderBy('r', 'DESC') + .limit(100), +}) +export class TrendingUserPost { + @ViewColumn() + sourceId: string; + + @ViewColumn() + tagsStr: string; + + @ViewColumn() + createdAt: Date; + + @ViewColumn() + r: number; +} diff --git a/src/entity/TrendingUserSource.ts b/src/entity/TrendingUserSource.ts new file mode 100644 index 0000000000..d703b82f23 --- /dev/null +++ b/src/entity/TrendingUserSource.ts @@ -0,0 +1,22 @@ +import { DataSource, ViewColumn, ViewEntity } from 'typeorm'; +import { TrendingUserPost } from './TrendingUserPost'; + +@ViewEntity({ + materialized: true, + expression: (dataSource: DataSource) => + dataSource + .createQueryBuilder() + .select('"sourceId"') + .addSelect('avg(r) r') + .from(TrendingUserPost, 'base') + .groupBy('"sourceId"') + .having('count(*) > 1') + .orderBy('r', 'DESC'), +}) +export class TrendingUserSource { + @ViewColumn() + sourceId: string; + + @ViewColumn() + r: number; +} diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index f8f7fc2527..0b968348a4 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -1660,6 +1660,28 @@ const obj = new GraphORM({ }, }, }, + TrendingUserSource: { + fields: { + handle: { + customQuery: (_, alias, qb) => + qb + .select('u.username') + .from(User, 'u') + .where(`u.id = ${alias}."sourceId"`), + }, + }, + }, + PopularUserSource: { + fields: { + handle: { + customQuery: (_, alias, qb) => + qb + .select('u.username') + .from(User, 'u') + .where(`u.id = ${alias}."sourceId"`), + }, + }, + }, }); export default obj; diff --git a/src/migration/1760104116722-PopularUserSources.ts b/src/migration/1760104116722-PopularUserSources.ts new file mode 100644 index 0000000000..f7c9150c80 --- /dev/null +++ b/src/migration/1760104116722-PopularUserSources.ts @@ -0,0 +1,316 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class PopularUserSources1760104116722 implements MigrationInterface { + name = 'PopularUserSources1760104116722' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "popular_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "popular_post" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "trending_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "trending_post" + `); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "trending_post" AS + SELECT + p."sourceId", + p."tagsStr", + p."createdAt", + log(10, "p"."upvotes" - "p"."downvotes") + extract(epoch from (p."createdAt" - now() + interval '7 days')) / 200000 AS "r" + FROM "public"."post" "p" + INNER JOIN "public"."source" "s" + ON "s"."id" = p."sourceId" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '7 day' + AND "p"."upvotes" > "p"."downvotes" + AND "s"."type" != :type + ORDER BY + r DESC + LIMIT 100 + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "trending_post", "SELECT p.\"sourceId\", p.\"tagsStr\", p.\"createdAt\", log(10, \"p\".\"upvotes\" - \"p\".\"downvotes\") + extract(epoch from (p.\"createdAt\" - now() + interval '7 days')) / 200000 AS \"r\" FROM \"public\".\"post\" \"p\" INNER JOIN \"public\".\"source\" \"s\" ON \"s\".\"id\" = p.\"sourceId\" WHERE not \"p\".\"private\" and p.\"createdAt\" > now() - interval '7 day' and \"p\".\"upvotes\" > \"p\".\"downvotes\" AND \"s\".\"type\" != :type ORDER BY r DESC LIMIT 100"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "popular_post" AS + SELECT + p."sourceId", + p."tagsStr", + p."createdAt", + "p"."upvotes" - "p"."downvotes" AS "r" + FROM "public"."post" "p" + INNER JOIN "public"."source" "s" + ON "s"."id" = p."sourceId" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '60 day' + AND "p"."upvotes" > "p"."downvotes" + AND "s"."type" != :type + ORDER BY + r DESC + LIMIT 1000 + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "popular_post", "SELECT p.\"sourceId\", p.\"tagsStr\", p.\"createdAt\", \"p\".\"upvotes\" - \"p\".\"downvotes\" AS \"r\" FROM \"public\".\"post\" \"p\" INNER JOIN \"public\".\"source\" \"s\" ON \"s\".\"id\" = p.\"sourceId\" WHERE not \"p\".\"private\" and p.\"createdAt\" > now() - interval '60 day' and \"p\".\"upvotes\" > \"p\".\"downvotes\" AND \"s\".\"type\" != :type ORDER BY r DESC LIMIT 1000"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "trending_user_post" AS + SELECT + p."sourceId", + p."tagsStr", + p."createdAt", + log(10, "p"."upvotes" - "p"."downvotes") + extract(epoch from (p."createdAt" - now() + interval '7 days')) / 200000 AS "r" + FROM "public"."post" "p" + INNER JOIN "public"."source" "s" + ON "s"."id" = p."sourceId" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '7 day' + AND "p"."upvotes" > "p"."downvotes" + AND "s"."type" = :type + ORDER BY + r DESC + LIMIT 100 + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "trending_user_post", "SELECT p.\"sourceId\", p.\"tagsStr\", p.\"createdAt\", log(10, \"p\".\"upvotes\" - \"p\".\"downvotes\") + extract(epoch from (p.\"createdAt\" - now() + interval '7 days')) / 200000 AS \"r\" FROM \"public\".\"post\" \"p\" INNER JOIN \"public\".\"source\" \"s\" ON \"s\".\"id\" = p.\"sourceId\" WHERE not \"p\".\"private\" and p.\"createdAt\" > now() - interval '7 day' and \"p\".\"upvotes\" > \"p\".\"downvotes\" AND \"s\".\"type\" = :type ORDER BY r DESC LIMIT 100"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "trending_user_source" AS + SELECT + "sourceId", + avg(r) r + FROM "public"."trending_user_post" "base" + GROUP BY + "sourceId" + HAVING + count(*) > 1 + ORDER BY + r DESC + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "trending_user_source", "SELECT \"sourceId\", avg(r) r FROM \"public\".\"trending_user_post\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 1 ORDER BY r DESC"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "popular_user_post" AS + SELECT + p."sourceId", + p."tagsStr", + p."createdAt", + "p"."upvotes" - "p"."downvotes" AS "r" + FROM "public"."post" "p" + INNER JOIN "public"."source" "s" + ON "s"."id" = p."sourceId" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '60 day' + AND "p"."upvotes" > "p"."downvotes" + AND "s"."type" = :type + ORDER BY + r DESC + LIMIT 1000 + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "popular_user_post", "SELECT p.\"sourceId\", p.\"tagsStr\", p.\"createdAt\", \"p\".\"upvotes\" - \"p\".\"downvotes\" AS \"r\" FROM \"public\".\"post\" \"p\" INNER JOIN \"public\".\"source\" \"s\" ON \"s\".\"id\" = p.\"sourceId\" WHERE not \"p\".\"private\" and p.\"createdAt\" > now() - interval '60 day' and \"p\".\"upvotes\" > \"p\".\"downvotes\" AND \"s\".\"type\" = :type ORDER BY r DESC LIMIT 1000"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "popular_user_source" AS + SELECT + "sourceId", + avg(r) r + FROM "public"."popular_user_post" "base" + GROUP BY + "sourceId" + HAVING + count(*) > 5 + ORDER BY + r DESC + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "popular_user_source", "SELECT \"sourceId\", avg(r) r FROM \"public\".\"popular_user_post\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 5 ORDER BY r DESC"]); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "user_stats", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "user_stats" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "popular_video_source", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "popular_video_source" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "popular_user_source", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "popular_user_source" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "popular_user_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "popular_user_post" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "trending_user_source", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "trending_user_source" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "trending_user_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "trending_user_post" + `); + + await queryRunner.query(/* sql */` + DROP INDEX IF EXISTS "public"."IDX_user_stats_id" + `); + + await queryRunner.query(/* sql */` + DROP INDEX IF EXISTS "public"."IDX_sourceTag_tag" + `); + + await queryRunner.query(/* sql */` + DROP INDEX IF EXISTS "public"."IDX_sourceTag_sourceId" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "user_stats", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "user_stats" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "popular_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "popular_post" + `); + + await queryRunner.query(/* sql */` + DELETE FROM "public"."typeorm_metadata" + WHERE "type" = $1 + AND "name" = $2 + AND "schema" = $3 + `, ["MATERIALIZED_VIEW", "trending_post", "public"]); + + await queryRunner.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "trending_post" + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "source_tag_view", "SELECT \"s\".\"id\" as \"sourceId\", \"pk\".\"keyword\" AS tag, count(\"pk\".\"keyword\") AS count FROM \"public\".\"source\" \"s\" INNER JOIN \"public\".\"post\" \"p\" ON \"p\".\"sourceId\" = \"s\".\"id\" AND \"p\".\"createdAt\" > '1970-01-01' INNER JOIN \"public\".\"post_keyword\" \"pk\" ON \"pk\".\"postId\" = \"p\".\"id\" AND \"pk\".\"status\" = 'allow' WHERE (\"s\".\"active\" = true AND \"s\".\"private\" = false) GROUP BY \"s\".\"id\", tag"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "trending_post" AS + SELECT + "sourceId", + "tagsStr", + "createdAt", + log(10, upvotes - downvotes) + extract(epoch from ("createdAt" - now() + interval '7 days')) / 200000 r + FROM "public"."post" "p" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '7 day' + AND upvotes > downvotes + ORDER BY + r DESC + LIMIT 100 + `); + + await queryRunner.query(/* sql */` + INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4) + `, ["public", "MATERIALIZED_VIEW", "trending_post", "SELECT \"sourceId\", \"tagsStr\", \"createdAt\", log(10, upvotes - downvotes) + extract(epoch from (\"createdAt\" - now() + interval '7 days')) / 200000 r FROM \"public\".\"post\" \"p\" WHERE not \"p\".\"private\" and p.\"createdAt\" > now() - interval '7 day' and upvotes > downvotes ORDER BY r DESC LIMIT 100"]); + + await queryRunner.query(/* sql */` + CREATE MATERIALIZED VIEW "popular_post" AS + SELECT + "sourceId", + "tagsStr", + "createdAt", + upvotes - downvotes r + FROM "public"."post" "p" + WHERE + NOT "p"."private" + AND p."createdAt" > now() - interval '60 day' + AND upvotes > downvotes + ORDER BY + r DESC + LIMIT 1000 + `); + } +} diff --git a/src/schema/sources.ts b/src/schema/sources.ts index c7aab5e153..999eddbf56 100644 --- a/src/schema/sources.ts +++ b/src/schema/sources.ts @@ -88,6 +88,8 @@ import { remoteConfig } from '../remoteConfig'; import { GQLCommentAwardArgs } from './comments'; import { UserTransaction } from '../entity/user/UserTransaction'; import { UserVote } from '../types'; +import { TrendingUserSource } from '../entity/TrendingUserSource'; +import { PopularUserSource } from '../entity/PopularUserSource'; export interface GQLSourceCategory { id: string; @@ -495,6 +497,26 @@ export const typeDefs = /* GraphQL */ ` limit: Int ): [Source] @cacheControl(maxAge: 600) + """ + Get the most trending user sources + """ + trendingUserSources( + """ + Limit the number of sources returned + """ + limit: Int + ): [Source] @cacheControl(maxAge: 600) + + """ + Get the most popular user sources + """ + popularUserSources( + """ + Limit the number of sources returned + """ + limit: Int + ): [Source] @cacheControl(maxAge: 600) + """ Get top video sources """ @@ -1923,6 +1945,20 @@ export const resolvers: IResolvers = traceResolvers< getFormattedSources(TrendingSource, args, ctx, info), popularSources: async (_, args, ctx: Context, info): Promise => getFormattedSources(PopularSource, args, ctx, info), + trendingUserSources: async ( + _, + args, + ctx: Context, + info, + ): Promise => + getFormattedSources(TrendingUserSource, args, ctx, info), + popularUserSources: async ( + _, + args, + ctx: Context, + info, + ): Promise => + getFormattedSources(PopularUserSource, args, ctx, info), topVideoSources: async ( _, args,