Task 14: Comment on Posts — Backend
In this task, we will update the backend (Nest.js) app to enable the addition of comments to a post.
Step 1: Create Comment Entity
Recall that in TypeORM, entities are classes that map to database tables. Each entity represents a table in the database, and instances of entities represent rows within that table.
To define the Comment
entity and its relationship with the Post
and User
entities, follow these steps:
Create a new file named comment.entity.ts
within the comments
directory in the api/src
folder. In this file, define the Comment
entity and its properties, which will correspond to the columns in the database table.
import { Post } from "src/posts/post.entity";
import { User } from "src/user/user.entity";
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
@Entity()
export class Comment {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column()
content: string;
@CreateDateColumn({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
timestamp: Date;
@ManyToOne(() => User, (user) => user.posts)
@JoinColumn({ name: "userId" })
user: User;
@Column()
userId: number;
@ManyToOne(() => Post, (post) => post.comments)
@JoinColumn({ name: "postId" })
post: Post;
@Column()
postId: string;
}
In the Comment
entity, we define a many-to-one relationship with the User
entity. This means that each comment can be associated with one user, and each user can write many comments.
To establish this relationship, update the User
entity to include a collection of Comment
entities. This allows us to directly access a user's comments from the User
entity.
// Add this import at the top of the file
import { Comment } from "src/comments/comment.entity";
// Add this inside the User entity class
@OneToMany(() => Comment, (comment) => comment.user)
comments: Comment[];
Similarly, in the Comment
entity, we define a many-to-one relationship with the Post
entity. This means that each comment can be associated with one post, and each post can have multiple comments.
To complete this relationship, update the Post
entity to include a collection of Comment
entities. This allows us to directly access the comments of a post from the Post
entity.
// Add this import at the top of the file
import { Comment } from "src/comments/comment.entity";
// Add this inside the Post entity class
@OneToMany(() => Comment, (comment) => comment.post)
comments: Comment[];
Please stage and commit the changes:
git add .
git commit -m "Add Comment entity and establish relationships with Post and User entities"
Step 2: Set up Comments module, service, and controller
Let's proceed with setting up the Comments module, service, and controller. We will use the Nest CLI to streamline this process. Open the terminal and change the directory to the api
folder.
Generate Comments Module
First, we will generate the Comments module:
nest generate module comments
This command will create a comments.module.ts
file inside the api/src/comments
directory. It will also update the api/src/app.module.ts
to include the CommentsModule
.
Let’s update the comments.module.ts
: import the TypeOrmModule
and the Comment
entity to register it:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Comment } from "./comment.entity";
@Module({
imports: [TypeOrmModule.forFeature([Comment])],
})
export class CommentsModule {}
Generate Comments Service
Next, we'll generate the Comments service. The service will contain the logic for handling CRUD operations on comments.
nest generate service comments
The CLI will create comments.service.ts
and comments.service.spec.ts
files in the api/src/comments
directory. It will also update the comments.module.ts
to include the CommentsService
as a provider.
Generate Comments Controller
Finally, we will generate the Comments controller. This controller will handle incoming HTTP requests and delegate them to the Comments service for processing.
To generate the controller, run the following command in your terminal:
nest generate controller comments
This command will create two files, comments.controller.ts
and comments.controller.spec.ts
, in the api/src/comments
directory. It will also update the comments.module.ts
file to include the CommentsController
as a controller.
Commit Changes
After running these commands, the api/src/comments
folder should contain the following files:
.
├── comment.entity.ts
├── comments.controller.spec.ts
├── comments.controller.ts
├── comments.module.ts
├── comments.service.spec.ts
└── comments.service.ts
Please use the "prettier" tool to format these files. After formatting, stage and commit the changes.
git add .
git commit -m "Set up Comment module, service, and controller"
Step 3: Create Comments DTOs
In this step we will create Data Transfer Objects for Comment entity. Recall we use DTOs to define the structure of the data being transferred between different layers or components of our application.
Create Comment DTO
First, we'll create a DTO to define the structure of the data for creating a new comment. This will be used to capture the incoming request data.
Create a file named comment-create.dto.ts
within the api/src/comments
directory and define the DTO:
import { IsNotEmpty, IsString } from "class-validator";
export class CreateCommentDTO {
@IsString()
@IsNotEmpty({ message: "Content cannot be empty" })
content: string;
}
We're using class validators to ensure that the content
field is a non-empty string.
Comment Response DTO
Create a file named comment-response.dto.ts
inside the api/src/comments
directory to define the structure of the data when a comment is returned in a response.
import { User } from "src/user/user.entity";
export class CommentResponseDTO {
id: string;
content: string;
timestamp: Date;
userId: number;
postId: string;
user?: User;
}
Notice that this response optionally includes information about the user who created the comment which will be helpful for the frontend to display that data next to the comment.
Please stage and commit the changes:
git add .
git commit -m "Add comment create and comment response DTOs"
Step 4: Implement Comments Service
In this step, we will implement the CommentsService
class. To keep it simple, we will add only two operations: to create a comment, and to get all comments.
The CommentsService
Constructor
We start by adding a constructor to the CommentsService
:
import { Injectable } from "@nestjs/common";
import { Repository } from "typeorm";
import { Comment } from "./comment.entity";
import { InjectRepository } from "@nestjs/typeorm";
@Injectable()
export class CommentsService {
constructor(
@InjectRepository(Comment)
private readonly commentRepository: Repository<Comment>,
) {}
// TODO: Add operations
}
The constructor
takes in a parameter commentRepository
of type Repository<Comment>
. The @InjectRepository()
decorator is used to inject the Comment
entity repository from TypeORM. This allows us to access the methods provided by the repository, such as querying the database for comments.
By injecting the commentRepository
into the CommentsService
, we can use it to perform CRUD (Create, Read, Update, Delete) operations on the Comment
entity in the database.
Create Comment
Next, let's add an operation to create a comment:
// Creates a new instance of the Comment entity and saves it to the database.
// Returns the newly created comment.
async create(
createCommentDto: CreateCommentDTO,
postId: string,
userId: number,
): Promise<Comment> {
const comment = this.commentRepository.create({
...createCommentDto,
postId, // Associate the comment with a post
userId, // Associate the comment with a user
});
return this.commentRepository.save(comment);
}
The create
method is responsible for creating a new comment. It takes three parameters: createCommentDto
, postId
, and userId
.
createCommentDto
is an object that contains the content of the comment.postId
is the ID of the post that the comment is associated with.userId
is the ID of the user who created the comment.
Inside the method, a new instance of the Comment
entity is created and populated with the content from createCommentDto
, as well as the postId
and userId
values. The commentRepository.save()
method is then called to save the new comment to the database.
The method returns a promise that resolves to the newly created comment.
Increment Comment Counts
To keep track of the number of comments for a Post
, we need to update the commentCount
attribute whenever a comment is added or removed. Let's add an operation to the PostsService
to increment the commentCount
:
async incrementCommentCounter(id: string): Promise<Post | null> {
const post = await this.findOne(id);
if (!post) {
return null;
}
post.commentCount += 1;
await this.postRepository.save(post);
return post;
}
The incrementCommentCounter
operation is responsible for increasing the comment count for a specific post. Here's how it works:
- It takes the ID of the post as a parameter.
- It retrieves the post from the database using the
findOne
method. - If the post is not found, it returns
null
. - If the post is found, it increments the
commentCount
attribute by 1. - It then saves the updated post to the database using the
postRepository.save
method. - Finally, it returns the updated post.
We will utilize this method in the create
function of the CommentsService
to increment the comment count for the associated post.
Add PostsService
as a Provider
To be able to use incrementCommentCounter
from PostsServices
within the create
method of the CommentsService
, we need to make it accessible in the comments module. To achieve this, update CommentsModule
as shown below:
import { Module } from "@nestjs/common";
import { CommentsService } from "./comments.service";
import { CommentsController } from "./comments.controller";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Comment } from "./comment.entity";
+ import { PostsService } from "src/posts/posts.service";
+ import { Post } from "src/posts/post.entity";
@Module({
- imports: [TypeOrmModule.forFeature([Comment])],
+ imports: [TypeOrmModule.forFeature([Comment, Post])],
- providers: [CommentsService],
+ providers: [CommentsService, PostsService],
controllers: [CommentsController],
})
export class CommentsModule {}
Increment Comment Counts when Creating Comment
Let's update the constructor of the CommentsService
as follows:
constructor(
@InjectRepository(Comment)
private readonly commentRepository: Repository<Comment>,
+ private readonly postsService: PostsService,
) {}
Make sure to import the PostsService
. Then, update the create
operation as follows:
async create(
createCommentDto: CreateCommentDTO,
postId: string,
userId: number,
): Promise<Comment> {
const comment = this.commentRepository.create({
...createCommentDto,
postId, // Associate the comment with a post
userId, // Associate the comment with a user
});
+ // Increment the comment counter in the associated post
+ await this.postsService.incrementCommentCounter(postId);
return this.commentRepository.save(card);
}
Find all Comments
Next, we will add an operation to retrieve all comments:
// Returns all comments that match the given criteria.
async findAll(
limit: number,
offset: number,
postId?: string,
userId?: number,
search?: string,
withUserData?: boolean,
withPostData?: boolean,
): Promise<Comment[]> {
const content = search ? ILike(`%${search}%`) : undefined;
const relations = [];
if (withUserData) {
relations.push("user");
}
if (withPostData) {
relations.push("post");
}
const comments = await this.commentRepository.find({
take: limit,
skip: offset,
where: [
{
postId,
userId,
content,
},
],
order: {
timestamp: "DESC",
},
relations,
});
return comments;
}
The findAll
operation retrieves all comments that match the specified criteria. It accepts several parameters:
limit
andoffset
define the pagination of the results, allowing you to fetch a specific number of comments starting from a particular position.postId
anduserId
are optional filters to narrow down the comments based on the associated post or user.search
allows you to search for comments based on a specific keyword or phrase.withUserData
andwithPostData
are flags indicating whether to include the associated user and post data in the returned comments.
Inside the method, the commentRepository.find()
method is called with the provided parameters. It retrieves the comments from the database based on the specified criteria. The comments are ordered in descending order by their timestamp. Note that the find
operation of the repository is used instead of the createQueryBuilder
method. While the createQueryBuilder
method offers more flexibility and control for constructing complex queries, the find
operation provides a simpler and more convenient way to fetch entities from the database.
The method returns a promise that resolves to an array of comments matching the specified criteria.
Overall, the findAll
operation allows for flexible fetching of comments based on various criteria, supports pagination, and provides options for including associated data.
Aside: With the createQueryBuilder
method, you can manually construct queries using SQL-like syntax, including conditions, joins, ordering, and aggregations. This allows you to handle more advanced scenarios and perform custom operations that may not be possible with the find
operation alone.
On the other hand, the find
operation of the repository provides a simpler and more convenient way to fetch entities from the database. It abstracts away the complexity of query construction and provides a straightforward API for common retrieval scenarios. It allows you to specify filtering criteria, ordering, pagination, and eager loading of related entities using a declarative syntax.
In general, you can use the find
operation for most basic retrieval needs, while the createQueryBuilder
method is suitable for more complex queries and situations where fine-grained control is required.
It's important to note that both approaches have their merits, and the choice between them depends on the specific requirements and complexity of your application's data access layer.
Commit Changes
Although we could have included additional operations in the CommentsService
class, we decided to keep it simple by implementing only two operations: creating a comment and retrieving all comments.
To stage and commit the changes, run the following commands:
git add .
git commit -m "Implement findAll and create methods in CommentsService"
Step 5: Implement Comments Controller
In this step, we focus on implementing the CommentsController
class in our application. This involves creating operations for both creating a comment and retrieving all comments. Our objective is to ensure that these operations align well with the previously implemented CommentsService
.
Initial Controller Setup
We start by modifying the comments.controller.ts
file located in the api/src/comments
folder. Initially, it appears as follows:
import { Controller } from "@nestjs/common";
@Controller("comments")
export class CommentsController {}
We modify this to include necessary dependencies and guards:
import { Controller, UseGuards } from "@nestjs/common";
import { CommentsService } from "./comments.service";
import { JwtAuthGuard } from "src/guards/jwt-auth.guard";
@UseGuards(JwtAuthGuard)
@Controller("posts/:postId/comments")
export class CommentsController {
constructor(private readonly commentsService: CommentsService) {}
}
In this revised version, we have added a constructor and decorated the controller with UseGuards
and JwtAuthGuard
to secure our endpoints. This ensures that only authenticated users can interact with comments. The Controller
decorator has been updated to reflect the nested nature of comments within posts.
Adding Create Comment Operation
Next, we expand the controller to include a method for creating comments:
@Post()
async create(
@Body() createCommentDto: CreateCommentDTO,
@Param("postId") postId: string,
@UserId() userId: number,
): Promise<CommentResponseDTO> {
return await this.commentsService.create(createCommentDto, postId, userId);
}
The create
method relies on the CommentsService
to add a new comment to a post. The method extracts the necessary data from the request body and parameters, and the UserId
decorator is used to retrieve the ID of the currently authenticated user.
Note the @Param
decorator, which extracts the postId
parameter from the nested endpoint posts/:postId/comments
.
Implementing DTOs for Comment Queries
For handling queries, we introduce two new Data Transfer Objects (DTOs):
-
Define
FindCommentsQueryDTO
infind-comments-query.dto.ts
(in theapi/src/comments
folder):import { IsInt, IsOptional, IsPositive, Min, IsString, Max, IsBoolean, } from "class-validator"; export class FindCommentsQueryDTO { @IsInt() @IsPositive() @Min(1) @Max(50) @IsOptional() limit: number = 10; @IsInt() @Min(0) @IsOptional() offset: number = 0; @IsString() @IsOptional() search?: string; @IsBoolean() @IsOptional() withPostData?: boolean; @IsBoolean() @IsOptional() withUserData?: boolean; }
-
Define
FindCommentsResponseDTO
infind-comments-response.dto.ts
(in theapi/src/comments
folder):import { CommentResponseDTO } from "./comment-response.dto"; export class FindCommentsResponseDTO { limit: number; offset: number; search?: string; withPostData?: boolean; withUserData?: boolean; data: CommentResponseDTO[]; }
These DTOs are helpful for validating and structuring the input and output of our comments query operation.
Retrieving Comments
We then update comments.controller.ts
to handle comment retrieval:
@Get()
async findAll(
@Param("postId") postId: string,
@Query() query: FindCommentsQueryDTO,
): Promise<FindCommentsResponseDTO> {
const { limit, offset, search, withPostData, withUserData } = query;
const comments = await this.commentsService.findAll(
limit,
offset,
postId,
undefined,
search,
withUserData,
withPostData,
);
return {
limit,
offset,
search,
withUserData,
withPostData,
data: comments.map((comment) => {
if (withUserData) {
delete comment.user.password;
}
return comment;
}),
};
}
The findAll
method, which leverages the CommentsService
to fetch comments based on the provided query parameters. It returns a structured response encapsulating the comments data.
Testing the Application
After these changes, test the API in Postman to ensure the new functionalities work as expected. This includes creating users, posts, and comments, and retrieving comments for a specific post.
Committing Changes
Finally, we stage and commit these changes to our version control system:
git add .
git commit -m "Add create and findAll methods to CommentsController"