From 68058a5202b63522248df5bf96de9d19c1e15830 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 13 Sep 2024 21:43:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20stargazers=20=EC=A1=B0=ED=9A=8C=20api?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../gitanimals/{render => }/Application.kt | 2 +- .../star/controller/StargazerController.kt | 19 ++++ .../org/gitanimals/star/domain/Stargazer.kt | 12 +++ .../star/domain/StargazerService.kt | 26 ++++++ .../star/domain/StargazersRepository.kt | 5 + .../star/infra/GithubStargazerApi.kt | 92 +++++++++++++++++++ .../star/infra/StargazerBatchJob.kt | 36 ++++++++ .../github-graphql/stargazer.graphql | 18 ++++ .../render/app/UserStatisticScheduleTest.kt | 2 +- 10 files changed, 211 insertions(+), 3 deletions(-) rename src/main/kotlin/org/gitanimals/{render => }/Application.kt (96%) create mode 100644 src/main/kotlin/org/gitanimals/star/controller/StargazerController.kt create mode 100644 src/main/kotlin/org/gitanimals/star/domain/Stargazer.kt create mode 100644 src/main/kotlin/org/gitanimals/star/domain/StargazerService.kt create mode 100644 src/main/kotlin/org/gitanimals/star/domain/StargazersRepository.kt create mode 100644 src/main/kotlin/org/gitanimals/star/infra/GithubStargazerApi.kt create mode 100644 src/main/kotlin/org/gitanimals/star/infra/StargazerBatchJob.kt create mode 100644 src/main/resources/github-graphql/stargazer.graphql diff --git a/build.gradle b/build.gradle index be38460..c811a92 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ test { } application { - mainClassName = 'org.gitanimals.render.Application' + mainClassName = 'org.gitanimals.Application' } sentry { diff --git a/src/main/kotlin/org/gitanimals/render/Application.kt b/src/main/kotlin/org/gitanimals/Application.kt similarity index 96% rename from src/main/kotlin/org/gitanimals/render/Application.kt rename to src/main/kotlin/org/gitanimals/Application.kt index 19d376d..aec0040 100644 --- a/src/main/kotlin/org/gitanimals/render/Application.kt +++ b/src/main/kotlin/org/gitanimals/Application.kt @@ -1,4 +1,4 @@ -package org.gitanimals.render +package org.gitanimals import org.rooftop.netx.meta.EnableSaga import org.springframework.boot.SpringApplication diff --git a/src/main/kotlin/org/gitanimals/star/controller/StargazerController.kt b/src/main/kotlin/org/gitanimals/star/controller/StargazerController.kt new file mode 100644 index 0000000..815928c --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/controller/StargazerController.kt @@ -0,0 +1,19 @@ +package org.gitanimals.star.controller + +import org.gitanimals.star.domain.StargazerService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController + +@RestController +class StargazerController( + private val stargazerService: StargazerService, +) { + + @GetMapping("/stargazers/{login}/press") + fun isPressStar(@PathVariable("login") login: String): Map { + val isPressStar = stargazerService.existsByLogin(login) + + return mapOf("isPressStar" to isPressStar) + } +} diff --git a/src/main/kotlin/org/gitanimals/star/domain/Stargazer.kt b/src/main/kotlin/org/gitanimals/star/domain/Stargazer.kt new file mode 100644 index 0000000..3b9dee1 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/domain/Stargazer.kt @@ -0,0 +1,12 @@ +package org.gitanimals.star.domain + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id + +@Entity +class Stargazer( + @Id + @Column(name = "login") + val login: String, +) diff --git a/src/main/kotlin/org/gitanimals/star/domain/StargazerService.kt b/src/main/kotlin/org/gitanimals/star/domain/StargazerService.kt new file mode 100644 index 0000000..8a3fcdd --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/domain/StargazerService.kt @@ -0,0 +1,26 @@ +package org.gitanimals.star.domain + +import jakarta.persistence.EntityManager +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class StargazerService( + private val stargazersRepository: StargazersRepository, + private val entityManager: EntityManager, +) { + + @Cacheable(value = ["exists_by_login_cache"]) + fun existsByLogin(login: String): Boolean = stargazersRepository.existsById(login) + + @Transactional + fun updateAll(logins: List) { + stargazersRepository.deleteAllInBatch() + + logins.map { entityManager.persist(Stargazer(it)) } + entityManager.flush() + entityManager.clear() + } +} diff --git a/src/main/kotlin/org/gitanimals/star/domain/StargazersRepository.kt b/src/main/kotlin/org/gitanimals/star/domain/StargazersRepository.kt new file mode 100644 index 0000000..b69c731 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/domain/StargazersRepository.kt @@ -0,0 +1,5 @@ +package org.gitanimals.star.domain + +import org.springframework.data.jpa.repository.JpaRepository + +interface StargazersRepository : JpaRepository diff --git a/src/main/kotlin/org/gitanimals/star/infra/GithubStargazerApi.kt b/src/main/kotlin/org/gitanimals/star/infra/GithubStargazerApi.kt new file mode 100644 index 0000000..4786397 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/infra/GithubStargazerApi.kt @@ -0,0 +1,92 @@ +package org.gitanimals.star.infra + +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import java.nio.charset.Charset + +@Component +class GithubStargazerApi( + @Value("\${github.token}") private val token: String, +) { + + private val restClient = RestClient.create("https://api.github.com/graphql") + + fun getStargazers(): List { + val stargazers = mutableListOf(getStargazer("")) + + while (stargazers.last().pageInfo.hasNextPage) { + val stargazer = getStargazer(stargazers.last().pageInfo.endCursor) + stargazers.add(stargazer) + } + + return stargazers + } + + private fun getStargazer(endCursor: String): StargazersResponse { + return restClient.post() + .header(HttpHeaders.AUTHORIZATION, "Bearer $token") + .body( + mapOf( + "query" to stargazerQuery.replaceFirst("*{endCursor}", endCursor) + ) + ).exchange { _, response -> + assertIsSuccess(response) + + response.bodyTo(GithubStargazerGraphqlResponse::class.java)!! + .data + .repository + .stargazers + } + } + + private fun assertIsSuccess(response: RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse) { + require(response.statusCode.is2xxSuccessful) { + "Bad request cause status : \"${response.statusText}\" message : \"${ + response.bodyTo( + String::class.java + ) + }\"" + } + } + + companion object { + private val stargazerQuery: String = + ClassPathResource("github-graphql/stargazer.graphql") + .getContentAsString(Charset.defaultCharset()) + } +} + +data class GithubStargazerGraphqlResponse( + val data: Data, +) { + data class Data( + val repository: Repository, + ) { + data class Repository( + val stargazers: StargazersResponse, + ) + } +} + +data class StargazersResponse( + val edges: List, + val pageInfo: PageInfo, +) { + + data class StarPushedPeople( + val starredAt: String, + val node: Node, + ) { + data class Node( + val login: String, + ) + } + + data class PageInfo( + val endCursor: String, + val hasNextPage: Boolean, + ) +} diff --git a/src/main/kotlin/org/gitanimals/star/infra/StargazerBatchJob.kt b/src/main/kotlin/org/gitanimals/star/infra/StargazerBatchJob.kt new file mode 100644 index 0000000..b24ae1b --- /dev/null +++ b/src/main/kotlin/org/gitanimals/star/infra/StargazerBatchJob.kt @@ -0,0 +1,36 @@ +package org.gitanimals.star.infra + +import org.gitanimals.star.domain.StargazerService +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +class StargazerBatchJob( + private val githubStargazerApi: GithubStargazerApi, + private val stargazerService: StargazerService, +) { + + @EventListener(ApplicationStartedEvent::class) + fun initStargazer() { + updateStargazer() + } + + @Scheduled(cron = EVERY_DAY) + fun updateStargazer() { + val stargazers = githubStargazerApi.getStargazers() + stargazerService.updateAll( + stargazers.flatMap { stargazer -> + stargazer.edges.map { edge -> + edge.node.login + } + } + ) + } + + + companion object { + private const val EVERY_DAY = "0 0 * * * ?" + } +} diff --git a/src/main/resources/github-graphql/stargazer.graphql b/src/main/resources/github-graphql/stargazer.graphql new file mode 100644 index 0000000..bf9186f --- /dev/null +++ b/src/main/resources/github-graphql/stargazer.graphql @@ -0,0 +1,18 @@ +query { + repository(owner: "git-goods", name: "gitanimals") { + stargazers(first: 100, after: "*{endCursor}") { + edges { + starredAt + node { + login + name + url + } + } + pageInfo { + endCursor + hasNextPage + } + } + } +} diff --git a/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt b/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt index f076544..dcff858 100644 --- a/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt +++ b/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt @@ -2,7 +2,7 @@ package org.gitanimals.render.app import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.spec.style.DescribeSpec -import org.gitanimals.render.Application +import org.gitanimals.Application import org.gitanimals.render.supports.RedisContainer import org.gitanimals.render.supports.SagaCapture import org.springframework.boot.test.context.SpringBootTest