feat(api): collections are pageable

related to #216
This commit is contained in:
Gauthier Roebroeck 2020-06-26 17:52:48 +08:00
parent 02e916898e
commit 449a27e136
9 changed files with 153 additions and 71 deletions

View file

@ -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,
@ -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,

View file

@ -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
@ -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> {
@ -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)
}

View file

@ -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
}

View file

@ -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
@ -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
@ -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,
@ -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() }
)
}

View file

@ -11,8 +11,10 @@ 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
@ -20,7 +22,10 @@ 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
@ -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(

View file

@ -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()

View file

@ -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()

View file

@ -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
@ -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)

View file

@ -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) }
}
}
@ -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) }
}
}