Skip to content

Commit b9e6ea7

Browse files
Layered Phase 2 - Comments (#169)
### Description <!-- Provide a comprehensive description here about what your PR aims to solve. --> <!-- You may also add additional context --> - Refactor Comments into our Layered architecture by creating dedicated mappers, dto, interface, and controller just like article - separate mappers into atomic files --- ### PR Checklist <!-- Please do not remove this section --> <!-- Mark each item with an "x" ([ ] becomes [x]) --> - [x] Read the Developer's Guide in [CONTRIBUTING.md](https://github.com/agnyz/bedstack/blob/main/CONTRIBUTING.md) - [x] Use a concise title to represent the changes introduced in this PR - [x] Provide a detailed description of the changes introduced in this PR, and, if necessary, some screenshots - [x] Reference an issue or discussion where the feature or changes have been previously discussed - [x] Add a failing test that passes with the changes introduced in this PR, or explain why it's not feasible - [x] Add documentation for the feature or changes introduced in this PR to the docs; you can run them with `bun docs` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a dedicated comments API with endpoints to create, retrieve, and delete comments on articles. - Added structured response formats for single and multiple comments. - **Refactor** - Separated comment-related routes from the articles API to a standalone comments controller. - Improved data mapping for articles and comments using dedicated mapper functions and centralized exports. - **Bug Fixes** - Corrected import paths for comments schema to ensure consistency. - **Chores** - Enhanced type safety and maintainability with new and updated interfaces for comments. - Updated documentation and response schemas for improved API clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 5fa0d49 commit b9e6ea7

36 files changed

+436
-372
lines changed

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { tagsPlugin } from '@tags/tags.plugin';
1111
import { usersPlugin } from '@users/users.plugin';
1212
import { Elysia } from 'elysia';
1313
import { description, title, version } from '../package.json';
14+
import { commentsController } from './comments/comments.controller';
1415

1516
// the file name is in the spirit of NestJS, where app module is the device in charge of putting together all the pieces of the app
1617
// see: https://docs.nestjs.com/modules
@@ -60,6 +61,7 @@ export const setupApp = () => {
6061
.use(usersPlugin)
6162
.use(profilesPlugin)
6263
.use(articlesController)
64+
.use(commentsController)
6365
.use(tagsPlugin),
6466
);
6567
};

src/articles/articles.controller.ts

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { setupArticles } from '@articles/articles.module';
2-
import { CommentResponseDto, CreateCommentDto } from '@comments/dto';
3-
import { Elysia, t } from 'elysia';
2+
import { Elysia } from 'elysia';
43
import {
54
ArticleFeedQueryDto,
65
ArticleResponseDto,
@@ -9,12 +8,7 @@ import {
98
ListArticlesQueryDto,
109
UpdateArticleDto,
1110
} from './dto';
12-
import {
13-
toCommentResponse,
14-
toCreateArticleInput,
15-
toFeedResponse,
16-
toResponse,
17-
} from './mappers/articles.mapper';
11+
import { toCreateArticleInput, toFeedResponse, toResponse } from './mappers';
1812

1913
export const articlesController = new Elysia().use(setupArticles).group(
2014
'/articles',
@@ -177,66 +171,6 @@ export const articlesController = new Elysia().use(setupArticles).group(
177171
},
178172
},
179173
)
180-
.post(
181-
'/:slug/comments',
182-
async ({ body, params, store, request }) => {
183-
const comment = await store.commentsService.createComment(
184-
params.slug,
185-
body.comment,
186-
await store.authService.getUserIdFromHeader(request.headers),
187-
);
188-
return toCommentResponse(comment);
189-
},
190-
{
191-
beforeHandle: app.store.authService.requireLogin,
192-
body: CreateCommentDto,
193-
response: CommentResponseDto,
194-
detail: {
195-
summary: 'Add Comments to an Article',
196-
},
197-
},
198-
)
199-
.get(
200-
'/:slug/comments',
201-
async ({ params, store, request }) => {
202-
const userId = await store.authService.getOptionalUserIdFromHeader(
203-
request.headers,
204-
);
205-
const comments = await store.commentsService.getComments(
206-
params.slug,
207-
userId === null ? undefined : userId,
208-
);
209-
return { comments: comments.map(toCommentResponse) };
210-
},
211-
{
212-
response: t.Object({
213-
comments: t.Array(CommentResponseDto),
214-
}),
215-
detail: {
216-
summary: 'Get Comments from an Article',
217-
},
218-
},
219-
)
220-
.delete(
221-
'/:slug/comments/:id',
222-
async ({ params, store, request }) => {
223-
await store.commentsService.deleteComment(
224-
params.slug,
225-
Number.parseInt(params.id, 10),
226-
await store.authService.getUserIdFromHeader(request.headers),
227-
);
228-
},
229-
{
230-
beforeHandle: app.store.authService.requireLogin,
231-
params: t.Object({
232-
slug: t.String(),
233-
id: t.String(),
234-
}),
235-
detail: {
236-
summary: 'Delete Comment',
237-
},
238-
},
239-
)
240174
.post(
241175
'/:slug/favorite',
242176
async ({ params, store, request }) => {

src/articles/articles.module.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { db } from '@/database.providers';
22
import { ArticlesRepository } from '@articles/articles.repository';
33
import { ArticlesService } from '@articles/articles.service';
44
import { AuthService } from '@auth/auth.service';
5-
import { CommentsRepository } from '@comments/comments.repository';
6-
import { CommentsService } from '@comments/comments.service';
75
import { ProfilesRepository } from '@profiles/profiles.repository';
86
import { ProfilesService } from '@profiles/profiles.service';
97
import { TagsRepository } from '@tags/tags.repository';
@@ -12,7 +10,6 @@ import { Elysia } from 'elysia';
1210

1311
export const setupArticles = () => {
1412
const articlesRepository = new ArticlesRepository(db);
15-
const commentsRepository = new CommentsRepository(db);
1613
const profilesRepository = new ProfilesRepository(db);
1714
const tagsRepositry = new TagsRepository(db);
1815
const profilesService = new ProfilesService(profilesRepository);
@@ -22,14 +19,9 @@ export const setupArticles = () => {
2219
profilesService,
2320
tagsService,
2421
);
25-
const commentsService = new CommentsService(
26-
commentsRepository,
27-
profilesService,
28-
);
2922
const authService = new AuthService();
3023
return new Elysia().state(() => ({
3124
articlesService,
3225
authService,
33-
commentsService,
3426
}));
3527
};

src/articles/articles.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { users } from '@/users/users.model';
2-
import { comments } from '@comments/schema';
1+
import { comments } from '@comments/comments.schema';
32
import { articleTags } from '@tags/tags.model';
3+
import { users } from '@users/users.model';
44
import { relations } from 'drizzle-orm';
55
import {
66
integer,

src/articles/articles.service.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import { AuthorizationError, BadRequestError, ConflictError } from '@/errors';
2-
import type { ProfilesService } from '@/profiles/profiles.service';
3-
import { slugify } from '@/utils/slugify';
41
import type { ArticlesRepository } from '@articles/articles.repository';
2+
import { AuthorizationError, BadRequestError, ConflictError } from '@errors';
3+
import type { ProfilesService } from '@profiles/profiles.service';
54
import type { TagsService } from '@tags/tags.service';
5+
import { slugify } from '@utils/slugify';
66
import { NotFoundError } from 'elysia';
77
import type {
88
CreateArticleInput,
99
IArticle,
1010
IArticleFeed,
1111
UpdateArticleInput,
1212
} from './interfaces';
13-
import {
14-
toDomain,
15-
toFeedDomain,
16-
toNewArticleRow,
17-
} from './mappers/articles.mapper';
13+
import { toDomain, toFeedDomain, toNewArticleRow } from './mappers';
1814

1915
type FindFilters = {
2016
tag?: string;

src/articles/interfaces/article-row.interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Profile } from '@/profiles/profiles.schema';
21
import type { ArticleTag } from '@/tags/tags.schema';
2+
import type { Profile } from '@profiles/profiles.schema';
33
import type { InferSelectModel } from 'drizzle-orm';
44
import type { articles, favoriteArticles } from '../articles.schema';
55

src/articles/mappers/articles.mapper.ts

Lines changed: 0 additions & 161 deletions
This file was deleted.

src/articles/mappers/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from './to-domain.mapper';
2+
export * from './to-response.mapper';
3+
export * from './to-create-article-input.mapper';
4+
export * from './to-new-article-row.mapper';
5+
export * from './to-feed-domain.mapper';
6+
export * from './to-feed-response.mapper';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { CreateArticleDto } from '../dto';
2+
import type { CreateArticleInput } from '../interfaces';
3+
4+
export function toCreateArticleInput({
5+
article,
6+
}: CreateArticleDto): CreateArticleInput {
7+
return {
8+
title: article.title,
9+
description: article.description,
10+
body: article.body,
11+
tagList: article.tagList ?? [],
12+
};
13+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ArticleRow, IArticle } from '../interfaces';
2+
3+
type ToDomainOptions = {
4+
tagList?: string[];
5+
currentUserId?: number;
6+
};
7+
8+
export function toDomain(
9+
article: ArticleRow,
10+
{ tagList, currentUserId }: ToDomainOptions,
11+
): IArticle {
12+
return {
13+
id: article.id,
14+
slug: article.slug,
15+
title: article.title,
16+
description: article.description,
17+
tagList: tagList ?? article.tags.map((t) => t.tagName),
18+
createdAt: article.createdAt,
19+
updatedAt: article.updatedAt,
20+
favorited: article.favoritedBy.some(
21+
(user) => user.userId === currentUserId,
22+
),
23+
favoritesCount: article.favoritedBy.length,
24+
body: article.body,
25+
author: {
26+
username: article.author.username,
27+
bio: article.author.bio,
28+
image: article.author.image,
29+
following: article.author.followers.some(
30+
(follower) => follower.followerId === currentUserId,
31+
),
32+
},
33+
};
34+
}

0 commit comments

Comments
 (0)