From f82bf94e10c3ca9be6f71b94d2af8b44fda75566 Mon Sep 17 00:00:00 2001 From: aruay99 Date: Sat, 22 Mar 2025 20:58:04 +0500 Subject: [PATCH 1/9] feat: add favorite/unfavorite of articles --- .gitignore | 1 + src/articles/articles.plugin.ts | 30 +++++++++++++ src/articles/articles.repository.ts | 66 +++++++++++++++++++++++++++++ src/articles/articles.service.ts | 36 ++++++++++++++++ 4 files changed, 133 insertions(+) diff --git a/.gitignore b/.gitignore index aeb1af58..c1ce007e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ yarn-error.log* # build files server +.qodo diff --git a/src/articles/articles.plugin.ts b/src/articles/articles.plugin.ts index a990a1b7..7e63daba 100644 --- a/src/articles/articles.plugin.ts +++ b/src/articles/articles.plugin.ts @@ -113,5 +113,35 @@ export const articlesPlugin = new Elysia().use(setupArticles).group( summary: 'Delete Article', }, }, + ) + .post( + '/:slug/favorite', + async ({ params, store, request }) => + store.articlesService.favoriteArticle( + params.slug, + await store.authService.getUserIdFromHeader(request.headers), + ), + { + beforeHandle: app.store.authService.requireLogin, + response: ReturnedArticleResponseSchema, + detail: { + summary: 'Favorite Article', + }, + }, + ) + .delete( + '/:slug/favorite', + async ({ params, store, request }) => + store.articlesService.unfavoriteArticle( + params.slug, + await store.authService.getUserIdFromHeader(request.headers), + ), + { + beforeHandle: app.store.authService.requireLogin, + response: ReturnedArticleResponseSchema, + detail: { + summary: 'Unfavorite Article', + }, + }, ), ); diff --git a/src/articles/articles.repository.ts b/src/articles/articles.repository.ts index 266f810f..9ffe8560 100644 --- a/src/articles/articles.repository.ts +++ b/src/articles/articles.repository.ts @@ -6,6 +6,10 @@ import type { Database } from '@/database.providers'; import { userFollows, users } from '@/users/users.model'; import { articles, favoriteArticles } from '@articles/articles.model'; import { and, arrayContains, count, desc, eq, inArray, sql } from 'drizzle-orm'; +import { NotFoundError } from 'elysia'; + + + export class ArticlesRepository { constructor(private readonly db: Database) {} @@ -193,4 +197,66 @@ export class ArticlesRepository { and(eq(articles.slug, slug), eq(articles.authorId, currentUserId)), ); } + + async favoriteArticle(slug: string, currentUserId: number) { + const article = await this.findBySlug(slug); + if (!article) { + throw new NotFoundError('Article not found'); + } + + // Check if user already favorited the article + const existingFavorite = await this.db + .select() + .from(favoriteArticles) + .where( + and( + eq(favoriteArticles.articleId, article.id), + eq(favoriteArticles.userId, currentUserId), + ), + ) + .limit(1); + + if (existingFavorite.length > 0) { + return article; // Already favorited, return the article without creating duplicate + } + + await this.db.insert(favoriteArticles).values({ + articleId: article.id, + userId: currentUserId, + }); + return article; + } + + async unfavoriteArticle(slug: string, currentUserId: number) { + const article = await this.findBySlug(slug); + if (!article) { + throw new NotFoundError('Article not found'); + } + + // Check if user has favorited the article + const existingFavorite = await this.db + .select() + .from(favoriteArticles) + .where( + and( + eq(favoriteArticles.articleId, article.id), + eq(favoriteArticles.userId, currentUserId), + ), + ) + .limit(1); + + if (existingFavorite.length === 0) { + return article; // Not favorited, return the article without trying to unfavorite + } + + await this.db + .delete(favoriteArticles) + .where( + and( + eq(favoriteArticles.articleId, article.id), + eq(favoriteArticles.userId, currentUserId), + ), + ); + return article; + } } diff --git a/src/articles/articles.service.ts b/src/articles/articles.service.ts index 44b86a7b..9f95973b 100644 --- a/src/articles/articles.service.ts +++ b/src/articles/articles.service.ts @@ -118,4 +118,40 @@ export class ArticlesService { }, }; } + + async favoriteArticle(slug: string, currentUserId: number) { + const article = await this.repository.favoriteArticle(slug, currentUserId); + const baseResponse = await this.generateArticleResponse(article, currentUserId); + + // If the article is already favorited, return the current state + if (baseResponse.article.favorited) { + return baseResponse; + } + + return { + article: { + ...baseResponse.article, + favorited: true, + favoritesCount: baseResponse.article.favoritesCount + 1, + }, + }; + } + + async unfavoriteArticle(slug: string, currentUserId: number) { + const article = await this.repository.unfavoriteArticle(slug, currentUserId); + const baseResponse = await this.generateArticleResponse(article, currentUserId); + + // If the article is not favorited, return the current state + if (!baseResponse.article.favorited) { + return baseResponse; + } + + return { + article: { + ...baseResponse.article, + favorited: false, + favoritesCount: baseResponse.article.favoritesCount - 1, + }, + }; + } } From 091491b6f79c32e6af530b88ab294a02cb3cf453 Mon Sep 17 00:00:00 2001 From: aruay99 Date: Mon, 24 Mar 2025 00:58:00 +0500 Subject: [PATCH 2/9] fix: fix issues from the review --- .gitignore | 2 +- src/articles/articles.plugin.ts | 10 ++++++ src/articles/articles.repository.ts | 56 ++++++----------------------- src/articles/articles.service.ts | 32 ++++------------- 4 files changed, 28 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index c1ce007e..8f201b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ yarn-error.log* # build files server -.qodo + diff --git a/src/articles/articles.plugin.ts b/src/articles/articles.plugin.ts index 7e63daba..42d1d0f8 100644 --- a/src/articles/articles.plugin.ts +++ b/src/articles/articles.plugin.ts @@ -126,6 +126,11 @@ export const articlesPlugin = new Elysia().use(setupArticles).group( response: ReturnedArticleResponseSchema, detail: { summary: 'Favorite Article', + security: [ + { + tokenAuth: [], + }, + ], }, }, ) @@ -141,6 +146,11 @@ export const articlesPlugin = new Elysia().use(setupArticles).group( response: ReturnedArticleResponseSchema, detail: { summary: 'Unfavorite Article', + security: [ + { + tokenAuth: [], + }, + ], }, }, ), diff --git a/src/articles/articles.repository.ts b/src/articles/articles.repository.ts index 9ffe8560..1e377904 100644 --- a/src/articles/articles.repository.ts +++ b/src/articles/articles.repository.ts @@ -6,10 +6,6 @@ import type { Database } from '@/database.providers'; import { userFollows, users } from '@/users/users.model'; import { articles, favoriteArticles } from '@articles/articles.model'; import { and, arrayContains, count, desc, eq, inArray, sql } from 'drizzle-orm'; -import { NotFoundError } from 'elysia'; - - - export class ArticlesRepository { constructor(private readonly db: Database) {} @@ -200,54 +196,23 @@ export class ArticlesRepository { async favoriteArticle(slug: string, currentUserId: number) { const article = await this.findBySlug(slug); - if (!article) { - throw new NotFoundError('Article not found'); - } - - // Check if user already favorited the article - const existingFavorite = await this.db - .select() - .from(favoriteArticles) - .where( - and( - eq(favoriteArticles.articleId, article.id), - eq(favoriteArticles.userId, currentUserId), - ), - ) - .limit(1); + if (!article) return null; - if (existingFavorite.length > 0) { - return article; // Already favorited, return the article without creating duplicate - } + const result = await this.db + .insert(favoriteArticles) + .values({ + articleId: article.id, + userId: currentUserId, + }) + .onConflictDoNothing() + .returning(); - await this.db.insert(favoriteArticles).values({ - articleId: article.id, - userId: currentUserId, - }); return article; } async unfavoriteArticle(slug: string, currentUserId: number) { const article = await this.findBySlug(slug); - if (!article) { - throw new NotFoundError('Article not found'); - } - - // Check if user has favorited the article - const existingFavorite = await this.db - .select() - .from(favoriteArticles) - .where( - and( - eq(favoriteArticles.articleId, article.id), - eq(favoriteArticles.userId, currentUserId), - ), - ) - .limit(1); - - if (existingFavorite.length === 0) { - return article; // Not favorited, return the article without trying to unfavorite - } + if (!article) return null; await this.db .delete(favoriteArticles) @@ -257,6 +222,7 @@ export class ArticlesRepository { eq(favoriteArticles.userId, currentUserId), ), ); + return article; } } diff --git a/src/articles/articles.service.ts b/src/articles/articles.service.ts index 9f95973b..d7e220dd 100644 --- a/src/articles/articles.service.ts +++ b/src/articles/articles.service.ts @@ -121,37 +121,17 @@ export class ArticlesService { async favoriteArticle(slug: string, currentUserId: number) { const article = await this.repository.favoriteArticle(slug, currentUserId); - const baseResponse = await this.generateArticleResponse(article, currentUserId); - - // If the article is already favorited, return the current state - if (baseResponse.article.favorited) { - return baseResponse; + if (!article) { + throw new NotFoundError('Article not found'); } - - return { - article: { - ...baseResponse.article, - favorited: true, - favoritesCount: baseResponse.article.favoritesCount + 1, - }, - }; + return await this.generateArticleResponse(article, currentUserId); } async unfavoriteArticle(slug: string, currentUserId: number) { const article = await this.repository.unfavoriteArticle(slug, currentUserId); - const baseResponse = await this.generateArticleResponse(article, currentUserId); - - // If the article is not favorited, return the current state - if (!baseResponse.article.favorited) { - return baseResponse; + if (!article) { + throw new NotFoundError('Article not found'); } - - return { - article: { - ...baseResponse.article, - favorited: false, - favoritesCount: baseResponse.article.favoritesCount - 1, - }, - }; + return await this.generateArticleResponse(article, currentUserId); } } From edb869f01ceaf779a675abbbf4282829a35b708b Mon Sep 17 00:00:00 2001 From: aruay99 Date: Tue, 15 Apr 2025 20:35:44 +0500 Subject: [PATCH 3/9] fix: article fav/unfav on repository level --- .gitignore | 1 + src/articles/articles.repository.ts | 26 +++++++++++++++----------- src/main.ts | 5 ++++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 8f201b9e..7a048827 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ yarn-error.log* # build files server +.qodo diff --git a/src/articles/articles.repository.ts b/src/articles/articles.repository.ts index 1e377904..19fdb9e8 100644 --- a/src/articles/articles.repository.ts +++ b/src/articles/articles.repository.ts @@ -196,24 +196,27 @@ export class ArticlesRepository { async favoriteArticle(slug: string, currentUserId: number) { const article = await this.findBySlug(slug); - if (!article) return null; + if (!article) { + return null; + } - const result = await this.db + // Insert the favorite and get the updated article state + await this.db .insert(favoriteArticles) - .values({ - articleId: article.id, - userId: currentUserId, - }) - .onConflictDoNothing() - .returning(); + .values({ articleId: article.id, userId: currentUserId }) + .onConflictDoNothing(); - return article; + // Return the updated article state + return this.findBySlug(slug); } async unfavoriteArticle(slug: string, currentUserId: number) { const article = await this.findBySlug(slug); - if (!article) return null; + if (!article) { + return null; + } + // Delete the favorite and get the updated article state await this.db .delete(favoriteArticles) .where( @@ -223,6 +226,7 @@ export class ArticlesRepository { ), ); - return article; + // Return the updated article state + return this.findBySlug(slug); } } diff --git a/src/main.ts b/src/main.ts index e3edb75c..9252a553 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,10 @@ import { Elysia } from 'elysia'; const app = new Elysia() .use(setupApp) .get('/', ({ redirect }) => redirect('/swagger')) - .listen(3000); + .listen({ + port: 3000, + hostname: 'localhost' + }); console.log( `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, From ee5c06d780664cf967ad7c3d991d76e762c4582b Mon Sep 17 00:00:00 2001 From: aruay99 Date: Tue, 15 Apr 2025 21:20:39 +0500 Subject: [PATCH 4/9] fix: codereview fixes --- .gitignore | 2 -- src/main.ts | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 7a048827..aeb1af58 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,3 @@ yarn-error.log* # build files server - -.qodo diff --git a/src/main.ts b/src/main.ts index 9252a553..69ae661c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,11 +4,7 @@ import { Elysia } from 'elysia'; const app = new Elysia() .use(setupApp) .get('/', ({ redirect }) => redirect('/swagger')) - .listen({ - port: 3000, - hostname: 'localhost' - }); - + .listen(3000); console.log( `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, ); From ecea32ad164d721cfec69eb7b3c9dfc589ff17cb Mon Sep 17 00:00:00 2001 From: Aruay Berdikulova <57131628+aruaycodes@users.noreply.github.com> Date: Tue, 15 Apr 2025 21:31:28 +0500 Subject: [PATCH 5/9] Update src/articles/articles.repository.ts Co-authored-by: Yam Borodetsky --- src/articles/articles.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/articles/articles.repository.ts b/src/articles/articles.repository.ts index 19fdb9e8..20353921 100644 --- a/src/articles/articles.repository.ts +++ b/src/articles/articles.repository.ts @@ -195,6 +195,7 @@ export class ArticlesRepository { } async favoriteArticle(slug: string, currentUserId: number) { + // TODO: Use a transaction to optimize from 1-3 ops to 1 op const article = await this.findBySlug(slug); if (!article) { return null; From 277d1772399027de610dcd73d80e926c33a4d37b Mon Sep 17 00:00:00 2001 From: Aruay Berdikulova <57131628+aruaycodes@users.noreply.github.com> Date: Tue, 15 Apr 2025 21:31:42 +0500 Subject: [PATCH 6/9] Update src/articles/articles.repository.ts Co-authored-by: Yam Borodetsky --- src/articles/articles.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/articles/articles.repository.ts b/src/articles/articles.repository.ts index 20353921..43e29564 100644 --- a/src/articles/articles.repository.ts +++ b/src/articles/articles.repository.ts @@ -212,6 +212,7 @@ export class ArticlesRepository { } async unfavoriteArticle(slug: string, currentUserId: number) { + // TODO: Use a transaction to optimize from 1-3 ops to 1 op const article = await this.findBySlug(slug); if (!article) { return null; From c0be0ce6e98032f8ff19c3fb8d0a28faa1f56a9b Mon Sep 17 00:00:00 2001 From: aruay99 Date: Tue, 15 Apr 2025 21:32:36 +0500 Subject: [PATCH 7/9] fix: added a line yam likes --- src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.ts b/src/main.ts index 69ae661c..d64617de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ const app = new Elysia() .use(setupApp) .get('/', ({ redirect }) => redirect('/swagger')) .listen(3000); + console.log( `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, ); From 2fd8b07c218107260fb6101e53422cf6680f4730 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Tue, 15 Apr 2025 21:37:05 +0500 Subject: [PATCH 8/9] Update src/main.ts Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index d64617de..69ae661c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,6 @@ const app = new Elysia() .use(setupApp) .get('/', ({ redirect }) => redirect('/swagger')) .listen(3000); - console.log( `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, ); From 7f38fca06bbbc399507981fe9b191e14685f3397 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Tue, 15 Apr 2025 21:38:24 +0500 Subject: [PATCH 9/9] add newline --- src/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.ts b/src/main.ts index 69ae661c..e3edb75c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ const app = new Elysia() .use(setupApp) .get('/', ({ redirect }) => redirect('/swagger')) .listen(3000); + console.log( `🦊 Elysia is running! Access Swagger UI at http://${app.server?.hostname}:${app.server?.port}/swagger`, );