Skip to content

Commit 13a432c

Browse files
authored
Get articles comments (#206)
* selectBySlug query implemented * selectBySlug router implemented * selectBySlug test implemented
1 parent 147b864 commit 13a432c

File tree

7 files changed

+114
-4
lines changed

7 files changed

+114
-4
lines changed

src/main/kotlin/io/github/nomisrev/env/Dependencies.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies {
3232
val hikari = hikari(env.dataSource)
3333
val sqlDelight = sqlDelight(hikari)
3434
val userRepo = userPersistence(sqlDelight.usersQueries, sqlDelight.followingQueries)
35-
val articleRepo = articleRepo(sqlDelight.articlesQueries, sqlDelight.tagsQueries)
35+
val articleRepo =
36+
articleRepo(sqlDelight.articlesQueries, sqlDelight.commentsQueries, sqlDelight.tagsQueries)
3637
val tagPersistence = tagPersistence(sqlDelight.tagsQueries)
3738
val favouritePersistence = favouritePersistence(sqlDelight.favoritesQueries)
3839
val jwtService = jwtService(env.auth, userRepo)

src/main/kotlin/io/github/nomisrev/repo/ArticlePersistence.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import io.github.nomisrev.routes.Profile
1111
import io.github.nomisrev.service.Slug
1212
import io.github.nomisrev.sqldelight.Articles
1313
import io.github.nomisrev.sqldelight.ArticlesQueries
14+
import io.github.nomisrev.sqldelight.CommentsQueries
15+
import io.github.nomisrev.sqldelight.SelectForSlug
1416
import io.github.nomisrev.sqldelight.TagsQueries
1517
import java.time.OffsetDateTime
1618

@@ -37,9 +39,11 @@ interface ArticlePersistence {
3739
suspend fun getFeed(userId: UserId, limit: FeedLimit, offset: FeedOffset): List<Article>
3840

3941
suspend fun getArticleBySlug(slug: Slug): Either<ArticleBySlugNotFound, Articles>
42+
43+
suspend fun getCommentsForSlug(slug: Slug): List<SelectForSlug>
4044
}
4145

42-
fun articleRepo(articles: ArticlesQueries, tagsQueries: TagsQueries) =
46+
fun articleRepo(articles: ArticlesQueries, comments: CommentsQueries, tagsQueries: TagsQueries) =
4347
object : ArticlePersistence {
4448
override suspend fun create(
4549
authorId: UserId,
@@ -108,4 +112,7 @@ fun articleRepo(articles: ArticlesQueries, tagsQueries: TagsQueries) =
108112
val article = articles.selectBySlug(slug.value).executeAsOneOrNull()
109113
ensureNotNull(article) { ArticleBySlugNotFound(slug.value) }
110114
}
115+
116+
override suspend fun getCommentsForSlug(slug: Slug): List<SelectForSlug> =
117+
comments.selectForSlug(slug.value).executeAsList()
111118
}

src/main/kotlin/io/github/nomisrev/routes/articles.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import io.ktor.server.application.call
1313
import io.ktor.server.request.receive
1414
import io.ktor.server.resources.get
1515
import io.ktor.server.resources.post
16+
import io.ktor.server.response.respond
1617
import io.ktor.server.routing.Route
1718
import java.time.OffsetDateTime
1819
import kotlinx.serialization.KSerializer
@@ -61,6 +62,8 @@ data class Comment(
6162
val author: Profile
6263
)
6364

65+
@Serializable data class MultipleCommentsResponse(val comments: List<Comment>)
66+
6467
@Serializable
6568
data class NewArticle(
6669
val title: String,
@@ -97,6 +100,9 @@ data class ArticleResource(val parent: RootResource = RootResource) {
97100
data class ArticlesResource(val parent: RootResource = RootResource) {
98101
@Resource("{slug}")
99102
data class Slug(val parent: ArticlesResource = ArticlesResource(), val slug: String)
103+
104+
@Resource("{slug}/comments")
105+
data class Comments(val parent: ArticlesResource = ArticlesResource(), val slug: String)
100106
}
101107

102108
fun Route.articleRoutes(
@@ -159,6 +165,15 @@ fun Route.articleRoutes(
159165
}
160166
}
161167

168+
fun Route.commentRoutes(articleService: ArticleService, jwtService: JwtService) {
169+
get<ArticlesResource.Comments> { slug ->
170+
jwtAuth(jwtService) { (_, _) ->
171+
val comments = articleService.getCommentsForSlug(Slug(slug.slug))
172+
call.respond(MultipleCommentsResponse(comments))
173+
}
174+
}
175+
}
176+
162177
private object OffsetDateTimeIso8601Serializer : KSerializer<OffsetDateTime> {
163178
override val descriptor: SerialDescriptor =
164179
PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)

src/main/kotlin/io/github/nomisrev/routes/root.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ fun Application.routes(deps: Dependencies) = routing {
1010
tagRoutes(deps.tagPersistence)
1111
articleRoutes(deps.articleService, deps.jwtService)
1212
profileRoutes(deps.userPersistence, deps.jwtService)
13+
commentRoutes(deps.articleService, deps.jwtService)
1314
}
1415

1516
@Resource("/api") data object RootResource

src/main/kotlin/io/github/nomisrev/service/ArticleService.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.github.nomisrev.repo.TagPersistence
99
import io.github.nomisrev.repo.UserId
1010
import io.github.nomisrev.repo.UserPersistence
1111
import io.github.nomisrev.routes.Article
12+
import io.github.nomisrev.routes.Comment
1213
import io.github.nomisrev.routes.FeedLimit
1314
import io.github.nomisrev.routes.FeedOffset
1415
import io.github.nomisrev.routes.MultipleArticlesResponse
@@ -38,6 +39,8 @@ interface ArticleService {
3839

3940
/** Get article by Slug */
4041
suspend fun getArticleBySlug(slug: Slug): Either<DomainError, Article>
42+
43+
suspend fun getCommentsForSlug(slug: Slug): List<Comment>
4144
}
4245

4346
fun articleService(
@@ -117,4 +120,15 @@ fun articleService(
117120
articleTags
118121
)
119122
}
123+
124+
override suspend fun getCommentsForSlug(slug: Slug): List<Comment> =
125+
articlePersistence.getCommentsForSlug(slug).map { comment ->
126+
Comment(
127+
comment.comment__id,
128+
comment.comment__createdAt,
129+
comment.comment__updatedAt,
130+
comment.comment__body,
131+
Profile(comment.author__username, comment.author__bio, comment.author__image, false)
132+
)
133+
}
120134
}

src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ WHERE article_id = :articleId;
2121
delete:
2222
DELETE FROM comments
2323
WHERE id = :id;
24+
25+
selectForSlug:
26+
SELECT comments.id AS comment__id, comments.article_id AS comment__articleId, comments.body AS comment__body, comments.author AS comment__author, comments.createdAt AS comment__createdAt, comments.updatedAt AS comment__updatedAt,
27+
users.username AS author__username, users.bio AS author__bio, users.image AS author__image
28+
FROM comments
29+
INNER JOIN articles ON comments.article_id = articles.id
30+
INNER JOIN users ON comments.author = users.id
31+
WHERE articles.slug = :slug;

src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,61 @@ package io.github.nomisrev.routes
33
import arrow.core.flatMap
44
import io.github.nefilim.kjwt.JWSHMAC512Algorithm
55
import io.github.nefilim.kjwt.JWT
6+
import io.github.nomisrev.KotestProject
7+
import io.github.nomisrev.auth.JwtToken
68
import io.github.nomisrev.repo.UserId
79
import io.github.nomisrev.service.CreateArticle
10+
import io.github.nomisrev.service.Login
811
import io.github.nomisrev.service.RegisterUser
912
import io.github.nomisrev.withServer
1013
import io.kotest.assertions.arrow.core.shouldBeRight
1114
import io.kotest.assertions.arrow.core.shouldBeSome
1215
import io.kotest.core.spec.style.StringSpec
1316
import io.ktor.client.call.body
1417
import io.ktor.client.plugins.resources.get
18+
import io.ktor.client.request.bearerAuth
1519
import io.ktor.http.HttpStatusCode
20+
import kotlin.properties.Delegates
1621

1722
class ArticlesRouteSpec :
1823
StringSpec({
1924
// User
2025
val validUsername = "username2"
2126
val validEmail = "valid2@domain.com"
2227
val validPw = "123456789"
28+
// User 3
29+
val validUsername3 = "username3"
30+
val validEmail3 = "valid3@domain.com"
31+
val validPw3 = "123456789"
32+
2333
// Article
2434
val validTags = setOf("arrow", "kotlin", "ktor", "sqldelight")
2535
val validTitle = "Fake Article Arrow "
2636
val validDescription = "This is a fake article description."
2737
val validBody = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
2838

39+
var token: JwtToken by Delegates.notNull()
40+
var userId: UserId by Delegates.notNull()
41+
42+
beforeAny {
43+
KotestProject.dependencies
44+
.get()
45+
.userService
46+
.register(RegisterUser(validUsername3, validEmail3, validPw3))
47+
.shouldBeRight()
48+
}
49+
50+
beforeTest {
51+
token =
52+
KotestProject.dependencies
53+
.get()
54+
.userService
55+
.login(Login(validEmail3, validPw3))
56+
.shouldBeRight()
57+
.first
58+
userId = KotestProject.dependencies.get().jwtService.verifyJwtToken(token).shouldBeRight()
59+
}
60+
2961
"Article by slug not found" {
3062
withServer {
3163
val response = get(ArticlesResource.Slug(slug = "slug"))
@@ -39,7 +71,7 @@ class ArticlesRouteSpec :
3971

4072
"Can get an article by slug" {
4173
withServer { dependencies ->
42-
val userId =
74+
val user1Id =
4375
dependencies.userService
4476
.register(RegisterUser(validUsername, validEmail, validPw))
4577
.flatMap { JWT.decodeT(it.value, JWSHMAC512Algorithm) }
@@ -49,7 +81,7 @@ class ArticlesRouteSpec :
4981
val article =
5082
dependencies.articleService
5183
.createArticle(
52-
CreateArticle(UserId(userId), validTitle, validDescription, validBody, validTags)
84+
CreateArticle(UserId(user1Id), validTitle, validDescription, validBody, validTags)
5385
)
5486
.shouldBeRight()
5587

@@ -59,4 +91,36 @@ class ArticlesRouteSpec :
5991
assert(response.body<SingleArticleResponse>().article == article)
6092
}
6193
}
94+
95+
"can get comments for an article by slug when authenticated" {
96+
withServer { dependencies ->
97+
val article =
98+
dependencies.articleService
99+
.createArticle(
100+
CreateArticle(userId, validTitle, validDescription, validBody, validTags)
101+
)
102+
.shouldBeRight()
103+
104+
val response =
105+
get(ArticlesResource.Comments(slug = article.slug)) { bearerAuth(token.value) }
106+
107+
assert(response.status == HttpStatusCode.OK)
108+
assert(response.body<MultipleCommentsResponse>().comments == emptyList<Comment>())
109+
}
110+
}
111+
112+
"can not get comments for an article when not authenticated" {
113+
withServer { dependencies ->
114+
val article =
115+
dependencies.articleService
116+
.createArticle(
117+
CreateArticle(userId, validTitle, validDescription, validBody, validTags)
118+
)
119+
.shouldBeRight()
120+
121+
val response = get(ArticlesResource.Comments(slug = article.slug))
122+
123+
assert(response.status == HttpStatusCode.Unauthorized)
124+
}
125+
}
62126
})

0 commit comments

Comments
 (0)