Skip to content

Commit

Permalink
feat: add total field to UserTopReader to get the users total amont o…
Browse files Browse the repository at this point in the history
…f top reader badges (#2381)

Co-authored-by: Ante Barić <[email protected]>
  • Loading branch information
omBratteng and capJavert authored Nov 6, 2024
1 parent 88a015e commit 04131e2
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 16 deletions.
64 changes: 51 additions & 13 deletions __tests__/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
UserPersonalizedDigestSendType,
UserPersonalizedDigestType,
UserPost,
UserStats,
UserStreak,
UserStreakAction,
UserStreakActionType,
Expand Down Expand Up @@ -5700,19 +5701,25 @@ describe('query topReaderBadgeById', () => {
});

describe('query topReaderBadge', () => {
const QUERY = `query TopReaderBadge($limit: Int = 5) {
topReaderBadge(limit: $limit) {
id
issuedAt
image
keyword {
value
flags {
title
const QUERY = /* GraphQL */ `
query TopReaderBadge($limit: Int, $userId: ID!) {
topReaderBadge(limit: $limit, userId: $userId) {
id
issuedAt
image
total
keyword {
value
flags {
title
}
}
user {
id
}
}
}
}`;
`;

beforeEach(async () => {
await saveFixtures(
Expand All @@ -5725,7 +5732,14 @@ describe('query topReaderBadge', () => {
},
})),
);
await saveFixtures(con, User, [usersFixture[1]]);
await saveFixtures(
con,
User,
[usersFixture[0], usersFixture[1]].map((user) => ({
...user,
infoConfirmed: true,
})),
);
await saveFixtures(con, UserTopReader, [
{
userId: '1',
Expand Down Expand Up @@ -5777,11 +5791,17 @@ describe('query topReaderBadge', () => {
image: 'https://daily.dev/image.jpg',
},
]);

await con.query(
`REFRESH MATERIALIZED VIEW ${con.getRepository(UserStats).metadata.tableName}`,
);
});

it('should return the 5 most recent top reader badges', async () => {
loggedUser = '1';
const res = await client.query(QUERY);
const res = await client.query(QUERY, {
variables: { userId: loggedUser },
});
const topReaderBadge: GQLUserTopReader[] = res.data.topReaderBadge;

expect(res.errors).toBeFalsy();
Expand All @@ -5795,7 +5815,7 @@ describe('query topReaderBadge', () => {
it('should limit the return to 1 top reader badge', async () => {
loggedUser = '1';
const res = await client.query(QUERY, {
variables: { limit: 1 },
variables: { limit: 1, userId: loggedUser },
});
const topReaderBadge: GQLUserTopReader[] = res.data.topReaderBadge;

Expand All @@ -5804,6 +5824,24 @@ describe('query topReaderBadge', () => {
expect(topReaderBadge[0].keyword.value).toEqual('kw_6');
});

it('should return top reader badge by userId', async () => {
loggedUser = '1';
const res = await client.query(QUERY, {
variables: { userId: '2' },
});
expect(res.errors).toBeFalsy();
expect(res.data.topReaderBadge[0].user.id).toEqual('2');
});

it('should return the total number of badges', async () => {
loggedUser = '1';
const res = await client.query(QUERY, {
variables: { userId: loggedUser },
});
expect(res.errors).toBeFalsy();
expect(res.data.topReaderBadge[0].total).toEqual(6);
});

describe('topReader field on User', () => {
const QUERY = /* GraphQL */ `
query User($id: ID!) {
Expand Down
10 changes: 10 additions & 0 deletions src/entity/user/UserStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ import { ghostUser } from '../../common';
)`,
'commentUpvotes',
)
.addSelect(
`(SELECT COALESCE(COUNT(*), 0)
FROM "user_top_reader" utp
WHERE utp."userId" = u."id"
)`,
'topReaderBadges',
)
.from('user', 'u')
.andWhere('u.infoConfirmed = TRUE')
.andWhere(`u.id != :ghostId`, { ghostId: ghostUser.id }),
Expand All @@ -58,4 +65,7 @@ export class UserStats {

@ViewColumn()
commentUpvotes: number;

@ViewColumn()
topReaderBadges: number;
}
8 changes: 8 additions & 0 deletions src/graphorm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Feature,
FeatureType,
SettingsFlagsPublic,
UserStats,
} from '../entity';
import {
SourceMemberRoles,
Expand Down Expand Up @@ -213,6 +214,13 @@ const obj = new GraphORM({
parentColumn: 'userId',
},
},
total: {
select: (_, alias, qb) =>
qb
.select('us."topReaderBadges"')
.from(UserStats, 'us')
.where(`us."id" = ${alias}."userId"`),
},
},
},
UserStreak: {
Expand Down
72 changes: 72 additions & 0 deletions src/migration/1730717128269-UserStatsTopReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class UserStatsTopReader1730717128269 implements MigrationInterface {
name = 'UserStatsTopReader1730717128269'

public async up(queryRunner: QueryRunner): Promise<void> {
// Create the new view as a temporary view
await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats_tmp" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0)
FROM "user"
WHERE "referralId" = u."id"
) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0)
FROM "post" p
WHERE (p."authorId" = u."id" OR p."scoutId" = u."id")
AND p."visible" = TRUE
AND p."deleted" = FALSE
) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0)
FROM "post" p
WHERE (p."authorId" = u."id" OR p."scoutId" = u."id")
AND p."visible" = TRUE
AND p."deleted" = FALSE
) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0)
FROM "comment" c
WHERE c."userId" = u."id"
) AS "commentUpvotes", (SELECT COALESCE(COUNT(*), 0)
FROM "user_top_reader" utp
WHERE utp."userId" = u."id"
) AS "topReaderBadges" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != '404'`);

// Rename the current view to the old view
await queryRunner.query(`ALTER TABLE "public"."user_stats" RENAME TO "user_stats_old"`);
// Rename the temporary view to the current view
await queryRunner.query(`ALTER TABLE "public"."user_stats_tmp" RENAME TO "user_stats"`);
// Drop the old view
await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats_old"`);

// Metadata
await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]);
await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user_top_reader\" utp\n WHERE utp.\"userId\" = u.\"id\"\n ) AS \"topReaderBadges\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != '404'"]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Create the new view as a temporary view
await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats_tmp" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0)
FROM "user"
WHERE "referralId" = u."id"
) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0)
FROM "post" p
WHERE (p."authorId" = u."id" OR p."scoutId" = u."id")
AND p."visible" = TRUE
AND p."deleted" = FALSE
) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0)
FROM "post" p
WHERE (p."authorId" = u."id" OR p."scoutId" = u."id")
AND p."visible" = TRUE
AND p."deleted" = FALSE
) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0)
FROM "comment" c
WHERE c."userId" = u."id"
) AS "commentUpvotes" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != '404'`);
// Rename the current view to the old view
await queryRunner.query(`ALTER TABLE "public"."user_stats" RENAME TO "user_stats_old"`);
// Rename the temporary view to the current view
await queryRunner.query(`ALTER TABLE "public"."user_stats_tmp" RENAME TO "user_stats"`);
// Drop the old view
await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats_old"`);

// Metadata
await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]);
await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != '404'"]);
}

}
11 changes: 8 additions & 3 deletions src/schema/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ export const typeDefs = /* GraphQL */ `
URL to the badge image
"""
image: String
"""
Total number of badges
"""
total: Int
}
extend type Query {
Expand Down Expand Up @@ -874,7 +879,7 @@ export const typeDefs = /* GraphQL */ `
"""
Get the latest top reader badges for the user
"""
topReaderBadge(limit: Int): [UserTopReader] @auth
topReaderBadge(limit: Int, userId: ID!): [UserTopReader]
"""
Get the top reader badge based on badge ID
Expand Down Expand Up @@ -1705,7 +1710,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
},
topReaderBadge: async (
_,
{ limit = 5 }: { limit: number },
{ limit = 5, userId }: { limit: number; userId: string },
ctx: AuthContext,
info: GraphQLResolveInfo,
) => {
Expand All @@ -1715,7 +1720,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
(builder) => {
builder.queryBuilder = builder.queryBuilder
.andWhere(`${builder.alias}.userId = :userId`, {
userId: ctx.userId,
userId,
})
.orderBy({
'"issuedAt"': 'DESC',
Expand Down

0 comments on commit 04131e2

Please sign in to comment.