Skip to content

Commit

Permalink
feat(api): collections are pageable
Browse files Browse the repository at this point in the history
related to gotson#216
  • Loading branch information
gotson committed Jun 26, 2020
1 parent 02e9168 commit 449a27e
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.gotson.komga.domain.persistence

import org.gotson.komga.domain.model.SeriesCollection
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable

interface SeriesCollectionRepository {
fun findByIdOrNull(collectionId: Long): SeriesCollection?
fun findAll(): Collection<SeriesCollection>
fun findAll(search: String? = null, pageable: Pageable): Page<SeriesCollection>

/**
* Find one SeriesCollection by collectionId,
Expand All @@ -16,7 +18,7 @@ interface SeriesCollectionRepository {
* Find all SeriesCollection with at least one Series belonging to the provided belongsToLibraryIds,
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
*/
fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection>
fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?, search: String? = null, pageable: Pageable): Page<SeriesCollection>

/**
* Find all SeriesCollection that contains the provided containsSeriesId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import org.gotson.komga.jooq.tables.records.CollectionRecord
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.ResultQuery
import org.jooq.impl.DSL
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Component
import java.time.LocalDateTime

Expand All @@ -20,50 +25,79 @@ class SeriesCollectionDao(
private val cs = Tables.COLLECTION_SERIES
private val s = Tables.SERIES

private val groupFields = arrayOf(*c.fields(), *cs.fields())
private val sorts = mapOf(
"name" to DSL.lower(c.NAME)
)


override fun findByIdOrNull(collectionId: Long): SeriesCollection? =
selectBase()
.where(c.ID.eq(collectionId))
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.fetchAndMap(null)
.firstOrNull()

override fun findByIdOrNull(collectionId: Long, filterOnLibraryIds: Collection<Long>?): SeriesCollection? =
selectBase()
.where(c.ID.eq(collectionId))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.fetchAndMap(filterOnLibraryIds)
.firstOrNull()

override fun findAll(): Collection<SeriesCollection> =
selectBase()
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
override fun findAll(search: String?, pageable: Pageable): Page<SeriesCollection> {
val conditions = search?.let { c.NAME.containsIgnoreCase(it) }
?: DSL.trueCondition()

override fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection> {
val count = dsl.selectCount()
.from(c)
.where(conditions)
.fetchOne(0, Long::class.java)

val orderBy = pageable.sort.toOrderBy(sorts)

val items = selectBase()
.where(conditions)
.orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap(null)

return PageImpl(
items,
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageable.sort)
else PageRequest.of(0, count.toInt(), pageable.sort),
count.toLong()
)
}

override fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?, search: String?, pageable: Pageable): Page<SeriesCollection> {
val ids = dsl.select(c.ID)
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.where(s.LIBRARY_ID.`in`(belongsToLibraryIds))
.apply { search?.let { and(c.NAME.containsIgnoreCase(it)) } }
.fetch(0, Long::class.java)

return selectBase()
val count = dsl.selectCount()
.from(c)
.where(c.ID.`in`(ids))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.fetchOne(0, Long::class.java)

val orderBy = pageable.sort.toOrderBy(sorts)

val items = selectBase()
.where(c.ID.`in`(ids))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.apply { search?.let { and(c.NAME.containsIgnoreCase(it)) } }
.orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap(filterOnLibraryIds)

return PageImpl(
items,
if (pageable.isPaged) PageRequest.of(pageable.pageNumber, pageable.pageSize, pageable.sort)
else PageRequest.of(0, count.toInt()),
count.toLong()
)
}

override fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection> {
Expand All @@ -75,24 +109,27 @@ class SeriesCollectionDao(

return selectBase()
.where(c.ID.`in`(ids))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.fetchAndMap(filterOnLibraryIds)
}

private fun selectBase() =
dsl.select(*groupFields)
dsl.selectDistinct(*c.fields())
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))

private fun ResultQuery<Record>.fetchAndMap() =
fetchGroups({ it.into(c) }, { it.into(cs) })
.map { (cr, csr) ->
val seriesIds = csr.mapNotNull { it.seriesId }
private fun ResultQuery<Record>.fetchAndMap(filterOnLibraryIds: Collection<Long>?): List<SeriesCollection> =
fetchInto(c)
.map { cr ->
val seriesIds = dsl.select(*cs.fields())
.from(cs)
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.where(cs.COLLECTION_ID.eq(cr.id))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(cs.NUMBER.asc())
.fetchInto(cs)
.mapNotNull { it.seriesId }
cr.toDomain(seriesIds)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.gotson.komga.infrastructure.jooq

import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort

class UnpagedSorted(
private val sort: Sort
) : Pageable {

override fun getPageNumber(): Int {
throw UnsupportedOperationException()
}

override fun hasPrevious(): Boolean = false

override fun getSort(): Sort = sort

override fun isPaged(): Boolean = false

override fun next(): Pageable = this

override fun getPageSize(): Int {
throw UnsupportedOperationException()
}

override fun getOffset(): Long {
throw UnsupportedOperationException()
}

override fun first(): Pageable = this

override fun previousOrFirst(): Pageable = this
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.gotson.komga.domain.persistence.MediaRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.interfaces.opds.dto.OpdsAuthor
import org.gotson.komga.interfaces.opds.dto.OpdsEntryAcquisition
Expand All @@ -32,6 +33,7 @@ import org.gotson.komga.interfaces.opds.dto.OpdsLinkPageStreaming
import org.gotson.komga.interfaces.opds.dto.OpdsLinkRel
import org.gotson.komga.interfaces.opds.dto.OpdsLinkSearch
import org.gotson.komga.interfaces.opds.dto.OpenSearchDescription
import org.springframework.data.domain.Sort
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
Expand Down Expand Up @@ -223,11 +225,12 @@ class OpdsController(
fun getCollections(
@AuthenticationPrincipal principal: KomgaPrincipal
): OpdsFeed {
val pageRequest = UnpagedSorted(Sort.by(Sort.Order.asc("name")))
val collections =
if (principal.user.sharedAllLibraries) {
collectionRepository.findAll()
collectionRepository.findAll(pageable = pageRequest)
} else {
collectionRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds)
collectionRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, pageable = pageRequest)
}
return OpdsFeedNavigation(
id = ID_COLLECTIONS_ALL,
Expand All @@ -238,7 +241,7 @@ class OpdsController(
OpdsLinkFeedNavigation(OpdsLinkRel.SELF, "$routeBase$ROUTE_COLLECTIONS_ALL"),
linkStart
),
entries = collections.map { it.toOpdsEntry() }
entries = collections.content.map { it.toOpdsEntry() }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@ import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
import org.gotson.komga.infrastructure.image.MosaicGenerator
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
import org.gotson.komga.infrastructure.security.KomgaPrincipal
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.interfaces.rest.dto.CollectionCreationDto
import org.gotson.komga.interfaces.rest.dto.CollectionDto
import org.gotson.komga.interfaces.rest.dto.CollectionUpdateDto
import org.gotson.komga.interfaces.rest.dto.SeriesDto
import org.gotson.komga.interfaces.rest.dto.restrictUrl
import org.gotson.komga.interfaces.rest.dto.toDto
import org.gotson.komga.interfaces.rest.persistence.SeriesDtoRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.http.CacheControl
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
Expand Down Expand Up @@ -53,17 +58,30 @@ class SeriesCollectionController(
private val mosaicGenerator: MosaicGenerator
) {

@PageableWithoutSortAsQueryParam
@GetMapping
fun getAll(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?
): List<CollectionDto> =
when {
principal.user.sharedAllLibraries && libraryIds == null -> collectionRepository.findAll()
principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraries(libraryIds, null)
!principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraries(libraryIds, principal.user.sharedLibrariesIds)
else -> collectionRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds)
}.sortedBy { it.name.toLowerCase() }.map { it.toDto() }
@RequestParam(name = "search", required = false) searchTerm: String?,
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
@Parameter(hidden = true) page: Pageable
): Page<CollectionDto> {
val pageRequest =
if (unpaged) UnpagedSorted(Sort.by(Sort.Order.asc("name")))
else PageRequest.of(
page.pageNumber,
page.pageSize,
Sort.by(Sort.Order.asc("name"))
)

return when {
principal.user.sharedAllLibraries && libraryIds == null -> collectionRepository.findAll(searchTerm, pageable = pageRequest)
principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraries(libraryIds, null, searchTerm, pageable = pageRequest)
!principal.user.sharedAllLibraries && libraryIds != null -> collectionRepository.findAllByLibraries(libraryIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
else -> collectionRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
}.map { it.toDto() }
}

@GetMapping("{id}")
fun getOne(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ import org.gotson.komga.Application
@AnalyzeClasses(packagesOf = [Application::class], importOptions = [ImportOption.DoNotIncludeTests::class])
class DomainDrivenDesignRulesTest {

@ArchTest
val domain_persistence_can_only_contain_interfaces: ArchRule =
classes()
.that().resideInAPackage("..domain..persistence..")
.should().beInterfaces()

@ArchTest
val domain_model_should_not_access_other_packages: ArchRule =
noClasses()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ import org.springframework.web.bind.annotation.RestController

class NamingConventionTest {

@ArchTest
val domain_persistence_should_have_names_ending_with_repository: ArchRule =
classes()
.that().resideInAPackage("..domain..persistence..")
.should().haveNameMatching(".*Repository")

@ArchTest
val services_should_not_have_names_containing_service_or_manager: ArchRule =
noClasses()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.domain.Pageable
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.time.LocalDateTime

Expand Down Expand Up @@ -164,11 +165,11 @@ class SeriesCollectionDaoTest(
))

// when
val foundLibrary1Filtered = collectionDao.findAllByLibraries(listOf(library.id), listOf(library.id))
val foundLibrary1Unfiltered = collectionDao.findAllByLibraries(listOf(library.id), null)
val foundLibrary2Filtered = collectionDao.findAllByLibraries(listOf(library2.id), listOf(library2.id))
val foundLibrary2Unfiltered = collectionDao.findAllByLibraries(listOf(library2.id), null)
val foundBothUnfiltered = collectionDao.findAllByLibraries(listOf(library.id, library2.id), null)
val foundLibrary1Filtered = collectionDao.findAllByLibraries(listOf(library.id), listOf(library.id), pageable = Pageable.unpaged()).content
val foundLibrary1Unfiltered = collectionDao.findAllByLibraries(listOf(library.id), null, pageable = Pageable.unpaged()).content
val foundLibrary2Filtered = collectionDao.findAllByLibraries(listOf(library2.id), listOf(library2.id), pageable = Pageable.unpaged()).content
val foundLibrary2Unfiltered = collectionDao.findAllByLibraries(listOf(library2.id), null, pageable = Pageable.unpaged()).content
val foundBothUnfiltered = collectionDao.findAllByLibraries(listOf(library.id, library2.id), null, pageable = Pageable.unpaged()).content

// then
assertThat(foundLibrary1Filtered).hasSize(2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ class SeriesCollectionControllerTest(
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk }
jsonPath("$.length()") { value(3) }
jsonPath("$[?(@.name == 'Lib1')].filtered") { value(false) }
jsonPath("$[?(@.name == 'Lib2')].filtered") { value(false) }
jsonPath("$[?(@.name == 'Lib1+2')].filtered") { value(false) }
jsonPath("$.totalElements") { value(3) }
jsonPath("$.content[?(@.name == 'Lib1')].filtered") { value(false) }
jsonPath("$.content[?(@.name == 'Lib2')].filtered") { value(false) }
jsonPath("$.content[?(@.name == 'Lib1+2')].filtered") { value(false) }
}
}

Expand All @@ -129,9 +129,9 @@ class SeriesCollectionControllerTest(
mockMvc.get("/api/v1/collections")
.andExpect {
status { isOk }
jsonPath("$.length()") { value(2) }
jsonPath("$[?(@.name == 'Lib1')].filtered") { value(false) }
jsonPath("$[?(@.name == 'Lib1+2')].filtered") { value(true) }
jsonPath("$.totalElements") { value(2) }
jsonPath("$.content[?(@.name == 'Lib1')].filtered") { value(false) }
jsonPath("$.content[?(@.name == 'Lib1+2')].filtered") { value(true) }
}
}

Expand Down

0 comments on commit 449a27e

Please sign in to comment.