Skip to content

Commit 2b68771

Browse files
authored
Add support for comment trees (#14)
1 parent d8be5fd commit 2b68771

18 files changed

+1701
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class RealmFeedItemComment1663081197272 implements MigrationInterface {
4+
name = 'RealmFeedItemComment1663081197272'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "realm_feed_item_comment" (
9+
"id" SERIAL NOT NULL,
10+
"authorId" uuid NOT NULL,
11+
"data" jsonb NOT NULL,
12+
"feedItemId" integer NOT NULL,
13+
"environment" character varying NOT NULL,
14+
"metadata" jsonb NOT NULL,
15+
"parentCommentId" integer,
16+
"realmPublicKeyStr" character varying NOT NULL,
17+
"created" TIMESTAMP NOT NULL DEFAULT now(),
18+
"deleted" TIMESTAMP,
19+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
20+
CONSTRAINT "PK_241ee9ad70d478bea24f5cad849" PRIMARY KEY ("id")
21+
)
22+
`);
23+
await queryRunner.query(`
24+
CREATE TABLE "realm_feed_item_comment_vote" (
25+
"commentId" integer NOT NULL,
26+
"userId" uuid NOT NULL,
27+
"realmPublicKeyStr" character varying NOT NULL,
28+
"data" jsonb NOT NULL,
29+
"created" TIMESTAMP NOT NULL DEFAULT now(),
30+
"deleted" TIMESTAMP,
31+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
32+
CONSTRAINT "PK_e163f06bd5b24e570a1e3502598" PRIMARY KEY ("commentId", "userId", "realmPublicKeyStr")
33+
)
34+
`);
35+
await queryRunner.query(`
36+
ALTER TABLE "realm_feed_item_comment"
37+
ADD CONSTRAINT "FK_d9738a44f8f8e8436c9bbdcf0d6" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION
38+
`);
39+
await queryRunner.query(`
40+
ALTER TABLE "realm_feed_item_comment"
41+
ADD CONSTRAINT "FK_8f105becc4961c4627e11ec0ed9" FOREIGN KEY ("feedItemId") REFERENCES "realm_feed_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION
42+
`);
43+
}
44+
45+
public async down(queryRunner: QueryRunner): Promise<void> {
46+
await queryRunner.query(`
47+
ALTER TABLE "realm_feed_item_comment" DROP CONSTRAINT "FK_8f105becc4961c4627e11ec0ed9"
48+
`);
49+
await queryRunner.query(`
50+
ALTER TABLE "realm_feed_item_comment" DROP CONSTRAINT "FK_d9738a44f8f8e8436c9bbdcf0d6"
51+
`);
52+
await queryRunner.query(`
53+
DROP TABLE "realm_feed_item_comment_vote"
54+
`);
55+
await queryRunner.query(`
56+
DROP TABLE "realm_feed_item_comment"
57+
`);
58+
}
59+
60+
}

src/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ConfigModule } from '@src/config/config.module';
1919
import { ConfigService } from '@src/config/config.service';
2020
import { HolaplexModule } from '@src/holaplex/holaplex.module';
2121
import { OnChainModule } from '@src/on-chain/on-chain.module';
22+
import { RealmFeedItemCommentModule } from '@src/realm-feed-item-comment/realm-feed-item-comment.module';
2223
import { RealmFeedItemModule } from '@src/realm-feed-item/realm-feed-item.module';
2324
import { RealmFeedModule } from '@src/realm-feed/realm-feed.module';
2425
import { RealmGovernanceModule } from '@src/realm-governance/realm-governance.module';
@@ -78,6 +79,7 @@ import { UserModule } from '@src/user/user.module';
7879
OnChainModule,
7980
RealmGovernanceModule,
8081
TaskDedupeModule,
82+
RealmFeedItemCommentModule,
8183
],
8284
controllers: [AppController],
8385
providers: [
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Kind, ValueNode, GraphQLScalarType } from 'graphql';
2+
3+
const RADIX = 36;
4+
5+
export const RealmFeedItemCommentIDScalar = new GraphQLScalarType({
6+
name: "RealmFeedItemCommentID",
7+
description: 'An opaque id used to identify `RealmFeedItemComment`s',
8+
// @ts-ignore
9+
parseLiteral: (ast: ValueNode): string => {
10+
// @ts-ignore
11+
return ast.kind === Kind.STRING ? parseInt(ast.value, RADIX) : null;
12+
},
13+
// @ts-ignore
14+
parseValue: (value: string): number => {
15+
return parseInt(value, RADIX);
16+
},
17+
// @ts-ignore
18+
serialize: (value: number): string => {
19+
return value.toString(RADIX);
20+
},
21+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Field, ObjectType } from '@nestjs/graphql';
2+
3+
import { RichTextDocumentScalar } from '@lib/scalars/RichTextDocument';
4+
import { RichTextDocument } from '@lib/types/RichTextDocument';
5+
import { RealmFeedItemCommentIDScalar } from '@src/lib/scalars/RealmFeedItemCommentID';
6+
import { RealmFeedItemIDScalar } from '@src/lib/scalars/RealmFeedItemID';
7+
import { RealmMember } from '@src/realm-member/dto/RealmMember';
8+
9+
import { RealmFeedItemCommentVoteType } from './RealmFeedItemCommentVoteType';
10+
11+
@ObjectType({
12+
description: 'A comment on a feed item',
13+
})
14+
export class RealmFeedItemComment {
15+
@Field(() => RealmMember, {
16+
description: 'The creator of the comment',
17+
nullable: true,
18+
})
19+
author?: RealmMember;
20+
21+
@Field(() => Date, {
22+
description: 'When the comment was created',
23+
})
24+
created: Date;
25+
26+
@Field(() => RichTextDocumentScalar, {
27+
description: 'Comment body text',
28+
})
29+
document: RichTextDocument;
30+
31+
@Field(() => RealmFeedItemIDScalar, {
32+
description: 'ID of the feed item the comment is in',
33+
})
34+
feedItemId: number;
35+
36+
@Field(() => RealmFeedItemCommentIDScalar, {
37+
description: 'ID of the comment',
38+
})
39+
id: number;
40+
41+
@Field(() => RealmFeedItemCommentVoteType, {
42+
description: "The requesting user's vote on the comment",
43+
nullable: true,
44+
})
45+
myVote?: RealmFeedItemCommentVoteType;
46+
47+
@Field(() => RealmFeedItemCommentIDScalar, {
48+
description: 'ID of the parent comment',
49+
nullable: true,
50+
})
51+
parentCommentId?: number | null;
52+
53+
@Field(() => [RealmFeedItemComment], {
54+
description: 'Replies to the comment',
55+
nullable: true,
56+
})
57+
replies?: RealmFeedItemComment[] | null;
58+
59+
@Field(() => Number, {
60+
description: 'The number of immediate replies to this comment',
61+
})
62+
repliesCount: number;
63+
64+
@Field(() => Number, {
65+
description: 'The total raw score for the comment',
66+
})
67+
score: number;
68+
69+
@Field(() => Date, {
70+
description: 'When the comment was last updated',
71+
})
72+
updated: Date;
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { registerEnumType } from '@nestjs/graphql';
2+
3+
/**
4+
* A vote for a comment, affecting the comment's score
5+
*/
6+
export enum RealmFeedItemCommentVoteType {
7+
Approve = 'Approve',
8+
Disapprove = 'Disapprove',
9+
}
10+
11+
registerEnumType(RealmFeedItemCommentVoteType, {
12+
name: 'RealmFeedItemCommentVoteType',
13+
description: 'A vote on a comment',
14+
valuesMap: {
15+
[RealmFeedItemCommentVoteType.Approve]: {
16+
description: 'The comment was approved',
17+
},
18+
[RealmFeedItemCommentVoteType.Disapprove]: {
19+
description: 'The comment was disapproved',
20+
},
21+
},
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ObjectType, registerEnumType } from '@nestjs/graphql';
2+
3+
import { EdgeType, ConnectionType } from '@lib/gqlTypes/Connection';
4+
5+
import { RealmFeedItemComment } from './RealmFeedItemComment';
6+
7+
@ObjectType()
8+
export class RealmFeedItemCommentEdge extends EdgeType(
9+
'RealmFeedItemComment',
10+
RealmFeedItemComment as any,
11+
) {}
12+
13+
@ObjectType()
14+
export class RealmFeedItemCommentConnection extends ConnectionType<RealmFeedItemCommentEdge>(
15+
'RealmFeedItemComment',
16+
RealmFeedItemCommentEdge,
17+
) {}
18+
19+
export enum RealmFeedItemCommentSort {
20+
New = 'New',
21+
Relevance = 'Relevance',
22+
TopAllTime = 'TopAllTime',
23+
}
24+
25+
registerEnumType(RealmFeedItemCommentSort, {
26+
name: 'RealmFeedItemCommentSort',
27+
description: 'Sort order for a list of comments',
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
Column,
3+
CreateDateColumn,
4+
DeleteDateColumn,
5+
Entity,
6+
PrimaryGeneratedColumn,
7+
UpdateDateColumn,
8+
ManyToOne,
9+
} from 'typeorm';
10+
11+
import { Environment } from '@lib/types/Environment';
12+
import { RichTextDocument } from '@lib/types/RichTextDocument';
13+
import { RealmFeedItem } from '@src/realm-feed-item/entities/RealmFeedItem.entity';
14+
import { User } from '@src/user/entities/User.entity';
15+
16+
export interface Data {
17+
authorPublicKeyStr?: string;
18+
document: RichTextDocument;
19+
}
20+
21+
export interface Metadata {
22+
relevanceScore: number;
23+
topAllTimeScore: number;
24+
rawScore: number;
25+
}
26+
27+
@Entity()
28+
export class RealmFeedItemComment {
29+
@PrimaryGeneratedColumn()
30+
id: number;
31+
32+
@Column()
33+
authorId: string;
34+
35+
@Column('jsonb')
36+
data: Data;
37+
38+
@Column()
39+
feedItemId: number;
40+
41+
@Column('varchar')
42+
environment: Environment;
43+
44+
@Column('jsonb')
45+
metadata: Metadata;
46+
47+
@Column({ nullable: true })
48+
parentCommentId?: number;
49+
50+
@Column()
51+
realmPublicKeyStr: string;
52+
53+
@ManyToOne('User', 'posts')
54+
author: User;
55+
56+
@ManyToOne('RealmFeedItem', 'comments')
57+
feedItem: RealmFeedItem;
58+
59+
@CreateDateColumn()
60+
created: Date;
61+
62+
@DeleteDateColumn()
63+
deleted: Date;
64+
65+
@UpdateDateColumn()
66+
updated: Date;
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
Column,
3+
CreateDateColumn,
4+
DeleteDateColumn,
5+
Entity,
6+
PrimaryColumn,
7+
UpdateDateColumn,
8+
} from 'typeorm';
9+
10+
import { RealmFeedItemCommentVoteType } from '../dto/RealmFeedItemCommentVoteType';
11+
12+
export interface Data {
13+
type: RealmFeedItemCommentVoteType;
14+
relevanceWeight: number;
15+
}
16+
17+
@Entity()
18+
export class RealmFeedItemCommentVote {
19+
@PrimaryColumn()
20+
commentId: number;
21+
22+
@PrimaryColumn('uuid')
23+
userId: string;
24+
25+
@PrimaryColumn()
26+
realmPublicKeyStr: string;
27+
28+
@Column('jsonb')
29+
data: Data;
30+
31+
@CreateDateColumn()
32+
created: Date;
33+
34+
@DeleteDateColumn()
35+
deleted: Date;
36+
37+
@UpdateDateColumn()
38+
updated: Date;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
4+
import { RealmFeedItemComment } from './entities/RealmFeedItemComment.entity';
5+
import { RealmFeedItemCommentVote } from './entities/RealmFeedItemCommentVote.entity';
6+
import { RealmFeedItemCommentResolver } from './realm-feed-item-comment.resolver';
7+
import { RealmFeedItemCommentService } from './realm-feed-item-comment.service';
8+
9+
@Module({
10+
imports: [TypeOrmModule.forFeature([RealmFeedItemComment, RealmFeedItemCommentVote])],
11+
providers: [RealmFeedItemCommentService, RealmFeedItemCommentResolver],
12+
exports: [RealmFeedItemCommentService],
13+
})
14+
export class RealmFeedItemCommentModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
3+
import { RealmFeedItemCommentResolver } from './realm-feed-item-comment.resolver';
4+
5+
describe('RealmFeedItemCommentResolver', () => {
6+
let resolver: RealmFeedItemCommentResolver;
7+
8+
beforeEach(async () => {
9+
const module: TestingModule = await Test.createTestingModule({
10+
providers: [RealmFeedItemCommentResolver],
11+
}).compile();
12+
13+
resolver = module.get<RealmFeedItemCommentResolver>(RealmFeedItemCommentResolver);
14+
});
15+
16+
it('should be defined', () => {
17+
expect(resolver).toBeDefined();
18+
});
19+
});

0 commit comments

Comments
 (0)