Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,18 @@ class MemberController : HasPageableRoute() {
memberService.delete(member)
return Response.noContent()
}

@Path("/me/watched-episodes/batch")
@Post
@JWTAuthenticated
private suspend fun filterFollowedEpisodeUUIDs(
@JWTUser memberUuid: UUID,
@BodyParam episodes: Array<UUID>
): Response {
if (episodes.isEmpty())
return Response.badRequest(MessageDto.error("No episodes provided"))
if (episodes.size > 50)
return Response.badRequest(MessageDto.error("Too many episodes provided"))
return Response.ok(memberFollowEpisodeService.filterFollowedEpisodeUUIDs(memberUuid, episodes.toList()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ class MemberFollowEpisodeRepository : AbstractRepository<MemberFollowEpisode>()
}
}

suspend fun filterFollowedEpisodeUUIDs(memberUuid: UUID, list: List<UUID>): List<UUID> {
return dispatch {
Comment on lines +64 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If list is empty, executing the database query is unnecessary and could potentially lead to SQL syntax errors depending on the database dialect's handling of empty IN clauses. Adding an early return for empty lists improves efficiency and robustness.

Suggested change
suspend fun filterFollowedEpisodeUUIDs(memberUuid: UUID, list: List<UUID>): List<UUID> {
return dispatch {
suspend fun filterFollowedEpisodeUUIDs(memberUuid: UUID, list: List<UUID>): List<UUID> {
if (list.isEmpty()) return emptyList()
return dispatch {

val cb = it.criteriaBuilder
val query = cb.createQuery(UUID::class.java)
val root = query.from(entityClass)
query.distinct(true)
.select(root[MemberFollowEpisode_.episode][EpisodeMapping_.uuid])

query.where(
cb.and(
cb.equal(root[MemberFollowEpisode_.member][Member_.uuid], memberUuid),
root[MemberFollowEpisode_.episode][EpisodeMapping_.uuid].`in`(list)
)
)

createReadOnlyQuery(it, query)
.resultList
}
}

suspend fun findAllByEpisode(episode: EpisodeMapping): List<MemberFollowEpisode> {
return dispatch {
val cb = it.criteriaBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class MemberFollowEpisodeService : AbstractService<MemberFollowEpisode, MemberFo

suspend fun findAllFollowedEpisodesUUID(memberUuid: UUID) = repository.findAllFollowedEpisodesUUID(memberUuid)

suspend fun filterFollowedEpisodeUUIDs(memberUuid: UUID, list: List<UUID>) = repository.filterFollowedEpisodeUUIDs(memberUuid, list)

suspend fun findAllByEpisode(episodeMapping: EpisodeMapping) =
repository.findAllByEpisode(episodeMapping)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,89 @@ class MemberControllerTest : AbstractControllerTest() {
}
}
}

@Test
fun batchFollowWatchedEpisodesEmptyList() {
testApplication {
application {
module()
}

val (_, token) = registerAndLogin()

client.post("/api/v1/members/me/watched-episodes/batch") {
header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody("[]")
}.apply {
assertEquals(HttpStatusCode.BadRequest, status)
val responseMap = ObjectParser.fromJson(bodyAsText(), Map::class.java)
assertEquals("No episodes provided", responseMap["message"])
}
}
}

@Test
fun batchFollowWatchedEpisodesTooMany() {
testApplication {
application {
module()
}

val (_, token) = registerAndLogin()
val largeList = (1..51).map { UUID.randomUUID() }

client.post("/api/v1/members/me/watched-episodes/batch") {
header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(ObjectParser.toJson(largeList))
}.apply {
assertEquals(HttpStatusCode.BadRequest, status)
val responseMap = ObjectParser.fromJson(bodyAsText(), Map::class.java)
assertEquals("Too many episodes provided", responseMap["message"])
}
}
}

@Test
fun batchFollowWatchedEpisodesSuccess() {
testApplication {
application {
module()
}

val (_, token) = registerAndLogin()
val anime = animeService.findAll().first()
val allEpisodes = episodeMappingService.findAllByAnime(anime)

val episodeFollowed1 = allEpisodes[0]
val episodeFollowed2 = allEpisodes[1]
val episodeNotFollowed = allEpisodes[2]

client.put("/api/v1/members/episodes") {
header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(ObjectParser.toJson(GenericDto(episodeFollowed1.uuid!!)))
}
client.put("/api/v1/members/episodes") {
header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(ObjectParser.toJson(GenericDto(episodeFollowed2.uuid!!)))
}

val requestList = listOf(episodeFollowed1.uuid!!, episodeFollowed2.uuid!!, episodeNotFollowed.uuid!!)

client.post("/api/v1/members/me/watched-episodes/batch") {
header(HttpHeaders.Authorization, "Bearer $token")
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(ObjectParser.toJson(requestList))
}.apply {
assertEquals(HttpStatusCode.OK, status)
val responseList = ObjectParser.fromJson(bodyAsText(), List::class.java)
assertEquals(2, responseList.size)
assertTrue(responseList.contains(episodeFollowed1.uuid.toString()))
assertTrue(responseList.contains(episodeFollowed2.uuid.toString()))
}
}
}
}
Loading