Skip to content

Commit 6d8fa4b

Browse files
authored
feat: featured award (#2796)
1 parent 565a607 commit 6d8fa4b

File tree

8 files changed

+267
-0
lines changed

8 files changed

+267
-0
lines changed

__tests__/posts.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import {
9595
UserTransactionProcessor,
9696
UserTransactionStatus,
9797
} from '../src/entity/user/UserTransaction';
98+
import { Product, ProductType } from '../src/entity/Product';
9899

99100
jest.mock('../src/common/pubsub', () => ({
100101
...(jest.requireActual('../src/common/pubsub') as Record<string, unknown>),
@@ -7584,3 +7585,166 @@ describe('field language', () => {
75847585
expect(res.data.post.language).toEqual('en');
75857586
});
75867587
});
7588+
7589+
describe('featuredAward field', () => {
7590+
const QUERY = `
7591+
query Post($id: ID!) {
7592+
post(id: $id) {
7593+
featuredAward {
7594+
award {
7595+
id
7596+
name
7597+
image
7598+
value
7599+
}
7600+
}
7601+
}
7602+
}`;
7603+
7604+
beforeEach(async () => {
7605+
await saveFixtures(con, Product, [
7606+
{
7607+
id: '5978781a-0e22-4702-bb32-1c59a13023c4',
7608+
name: 'Award 1',
7609+
image: 'https://daily.dev/award.jpg',
7610+
type: ProductType.Award,
7611+
value: 42,
7612+
},
7613+
{
7614+
id: '03916c91-8030-499e-8d3d-ae0a4c065012',
7615+
name: 'Award 2',
7616+
image: 'https://daily.dev/award.jpg',
7617+
type: ProductType.Award,
7618+
value: 10,
7619+
},
7620+
{
7621+
id: '24d83ece-1f82-4a82-ae48-85e5b003c9af',
7622+
name: 'Award 3',
7623+
image: 'https://daily.dev/award.jpg',
7624+
type: ProductType.Award,
7625+
value: 20,
7626+
},
7627+
]);
7628+
});
7629+
7630+
it('should return featuredAward', async () => {
7631+
const [transaction, transaction2, transaction3] = await con
7632+
.getRepository(UserTransaction)
7633+
.save([
7634+
{
7635+
processor: UserTransactionProcessor.Njord,
7636+
receiverId: '1',
7637+
status: UserTransactionStatus.Success,
7638+
productId: '03916c91-8030-499e-8d3d-ae0a4c065012',
7639+
senderId: '1',
7640+
fee: 0,
7641+
value: 10,
7642+
valueIncFees: 10,
7643+
},
7644+
{
7645+
processor: UserTransactionProcessor.Njord,
7646+
receiverId: '1',
7647+
status: UserTransactionStatus.Success,
7648+
productId: '24d83ece-1f82-4a82-ae48-85e5b003c9af',
7649+
senderId: '3',
7650+
fee: 0,
7651+
value: 20,
7652+
valueIncFees: 20,
7653+
},
7654+
{
7655+
processor: UserTransactionProcessor.Njord,
7656+
receiverId: '1',
7657+
status: UserTransactionStatus.Success,
7658+
productId: '5978781a-0e22-4702-bb32-1c59a13023c4',
7659+
senderId: '2',
7660+
fee: 0,
7661+
value: 42,
7662+
valueIncFees: 42,
7663+
},
7664+
]);
7665+
7666+
await con.getRepository(UserPost).save([
7667+
{
7668+
postId: 'p1',
7669+
userId: transaction.senderId,
7670+
vote: UserVote.None,
7671+
hidden: false,
7672+
flags: {
7673+
awardId: transaction.productId,
7674+
},
7675+
awardTransactionId: transaction.id,
7676+
},
7677+
{
7678+
postId: 'p1',
7679+
userId: transaction2.senderId,
7680+
vote: UserVote.None,
7681+
hidden: false,
7682+
flags: {
7683+
awardId: transaction2.productId,
7684+
},
7685+
awardTransactionId: transaction2.id,
7686+
},
7687+
{
7688+
postId: 'p2',
7689+
userId: transaction3.senderId,
7690+
vote: UserVote.None,
7691+
hidden: false,
7692+
flags: {
7693+
awardId: transaction3.productId,
7694+
},
7695+
awardTransactionId: transaction3.id,
7696+
},
7697+
]);
7698+
7699+
const res = await client.query(QUERY, {
7700+
variables: { id: 'p1' },
7701+
});
7702+
7703+
expect(res.errors).toBeFalsy();
7704+
7705+
expect(res.data.post.featuredAward).toMatchObject({
7706+
award: {
7707+
id: '24d83ece-1f82-4a82-ae48-85e5b003c9af',
7708+
name: 'Award 3',
7709+
image: 'https://daily.dev/award.jpg',
7710+
value: 20,
7711+
},
7712+
});
7713+
});
7714+
7715+
it('should not return featuredAward if no awards', async () => {
7716+
const [transaction] = await con.getRepository(UserTransaction).save([
7717+
{
7718+
processor: UserTransactionProcessor.Njord,
7719+
receiverId: '1',
7720+
status: UserTransactionStatus.Success,
7721+
productId: '03916c91-8030-499e-8d3d-ae0a4c065012',
7722+
senderId: '1',
7723+
fee: 0,
7724+
value: 10,
7725+
valueIncFees: 10,
7726+
},
7727+
]);
7728+
7729+
await con.getRepository(UserPost).save([
7730+
{
7731+
postId: 'p1',
7732+
userId: transaction.senderId,
7733+
vote: UserVote.None,
7734+
hidden: false,
7735+
flags: {
7736+
awardId: transaction.productId,
7737+
},
7738+
awardTransactionId: transaction.id,
7739+
},
7740+
]);
7741+
7742+
const res = await client.query(QUERY, {
7743+
variables: { id: 'p2' },
7744+
});
7745+
7746+
expect(res.errors).toBeFalsy();
7747+
7748+
expect(res.data.post.featuredAward).toBeNull();
7749+
});
7750+
});

src/entity/Product.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum ProductType {
2222
}
2323

2424
@Entity()
25+
@Index('idx_product_value_desc', { synchronize: false })
2526
export class Product {
2627
@PrimaryGeneratedColumn('uuid')
2728
id: string;

src/entity/user/UserComment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type UserCommentFlags = Partial<{
1919
@Entity()
2020
@Index(['commentId', 'userId'], { unique: true })
2121
@Index(['userId', 'vote', 'votedAt'])
22+
@Index('idx_user_comment_flags_awardId', { synchronize: false })
2223
export class UserComment {
2324
get awarded(): boolean {
2425
return !!this.awardTransactionId;

src/entity/user/UserPost.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type UserPostFlagsPublic = Pick<UserPostFlags, 'feedbackDismiss'>;
2323
@Index(['postId', 'userId'], { unique: true })
2424
@Index(['userId', 'vote', 'votedAt'])
2525
@Index('IDX_user_post_postid_userid_hidden', ['postId', 'userId', 'hidden'])
26+
@Index('idx_user_post_flags_awardId', { synchronize: false })
2627
export class UserPost {
2728
get awarded(): boolean {
2829
return !!this.awardTransactionId;

src/graphorm/index.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { isPlusMember } from '../paddle';
5050
import { remoteConfig } from '../remoteConfig';
5151
import { whereNotUserBlocked } from '../common/contentPreference';
5252
import { type GetBalanceResult } from '../common/njord';
53+
import { Product } from '../entity/Product';
5354

5455
const existsByUserAndPost =
5556
(entity: string, build?: (queryBuilder: QueryBuilder) => QueryBuilder) =>
@@ -574,6 +575,23 @@ const obj = new GraphORM({
574575
});
575576
},
576577
},
578+
featuredAward: {
579+
relation: {
580+
isMany: false,
581+
customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => {
582+
return qb
583+
.innerJoin(
584+
Product,
585+
'awardProduct',
586+
`"awardProduct".id = ("${childAlias}".flags->>'awardId')::uuid`,
587+
)
588+
.where(`"${childAlias}"."postId" = "${parentAlias}".id`)
589+
.andWhere(`"${childAlias}".flags->>'awardId' is not null`)
590+
.orderBy('"awardProduct".value', 'DESC')
591+
.limit(1);
592+
},
593+
},
594+
},
577595
},
578596
},
579597
SourceCategory: {
@@ -822,6 +840,23 @@ const obj = new GraphORM({
822840
.limit(1),
823841
},
824842
},
843+
featuredAward: {
844+
relation: {
845+
isMany: false,
846+
customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => {
847+
return qb
848+
.innerJoin(
849+
Product,
850+
'awardProduct',
851+
`"awardProduct".id = ("${childAlias}".flags->>'awardId')::uuid`,
852+
)
853+
.where(`"${childAlias}"."commentId" = "${parentAlias}".id`)
854+
.andWhere(`"${childAlias}".flags->>'awardId' is not null`)
855+
.orderBy('"awardProduct".value', 'DESC')
856+
.limit(1);
857+
},
858+
},
859+
},
825860
},
826861
},
827862
FeedSettings: {
@@ -984,6 +1019,16 @@ const obj = new GraphORM({
9841019
select: '"awardTransactionId" IS NOT NULL',
9851020
rawSelect: true,
9861021
},
1022+
award: {
1023+
relation: {
1024+
isMany: false,
1025+
customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => {
1026+
return qb.where(
1027+
`${childAlias}.id = ("${parentAlias}".flags->>'awardId')::uuid`,
1028+
);
1029+
},
1030+
},
1031+
},
9871032
},
9881033
},
9891034
PostQuestion: {
@@ -1029,6 +1074,16 @@ const obj = new GraphORM({
10291074
select: '"awardTransactionId" IS NOT NULL',
10301075
rawSelect: true,
10311076
},
1077+
award: {
1078+
relation: {
1079+
isMany: false,
1080+
customRelation: (ctx, parentAlias, childAlias, qb): QueryBuilder => {
1081+
return qb.where(
1082+
`${childAlias}.id = ("${parentAlias}".flags->>'awardId')::uuid`,
1083+
);
1084+
},
1085+
},
1086+
},
10321087
},
10331088
},
10341089
Feed: {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class AwardIdIndex1746564832985 implements MigrationInterface {
4+
name = 'AwardIdIndex1746564832985';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(
8+
`CREATE INDEX IF NOT EXISTS "idx_user_post_flags_awardId" ON "user_post" (("flags"->>'awardId'))`,
9+
);
10+
11+
await queryRunner.query(
12+
`CREATE INDEX IF NOT EXISTS "idx_user_comment_flags_awardId" ON "user_comment" (("flags"->>'awardId'))`,
13+
);
14+
15+
await queryRunner.query(
16+
`CREATE INDEX IF NOT EXISTS "idx_product_value_desc" ON product ("value" DESC)`,
17+
);
18+
}
19+
20+
public async down(queryRunner: QueryRunner): Promise<void> {
21+
await queryRunner.query(`DROP INDEX IF EXISTS "idx_product_value_desc"`);
22+
23+
await queryRunner.query(
24+
`DROP INDEX IF EXISTS "idx_user_comment_flags_awardId"`,
25+
);
26+
27+
await queryRunner.query(
28+
`DROP INDEX IF EXISTS "idx_user_post_flags_awardId"`,
29+
);
30+
}
31+
}

src/schema/comments.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ export const typeDefs = /* GraphQL */ `
195195
Awarded product
196196
"""
197197
award: Product
198+
199+
"""
200+
Featured award for the comment, currently the most expensive one
201+
"""
202+
featuredAward: UserComment
198203
}
199204
200205
type CommentEdge {
@@ -271,6 +276,8 @@ export const typeDefs = /* GraphQL */ `
271276
comment: Comment!
272277
273278
awarded: Boolean!
279+
280+
award: Product
274281
}
275282
276283
extend type Query {

src/schema/posts.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ export const typeDefs = /* GraphQL */ `
400400
post: Post!
401401
402402
awarded: Boolean!
403+
404+
award: Product
403405
}
404406
405407
type PostTranslation {
@@ -662,6 +664,11 @@ export const typeDefs = /* GraphQL */ `
662664
Language of the post
663665
"""
664666
language: String
667+
668+
"""
669+
Featured award for the post, currently the most expensive one
670+
"""
671+
featuredAward: UserPost
665672
}
666673
667674
type PostConnection {

0 commit comments

Comments
 (0)