diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt index b670a40bb..db0ae516d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt @@ -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 + fun findAll(search: String? = null, pageable: Pageable): Page /** * 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, filterOnLibraryIds: Collection?): Collection + fun findAllByLibraries(belongsToLibraryIds: Collection, filterOnLibraryIds: Collection?, search: String? = null, pageable: Pageable): Page /** * Find all SeriesCollection that contains the provided containsSeriesId, diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt index 2420e995f..ac1ea5b2b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt @@ -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?): 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 = - selectBase() - .groupBy(*groupFields) - .orderBy(cs.NUMBER.asc()) - .fetchAndMap() + override fun findAll(search: String?, pageable: Pageable): Page { + val conditions = search?.let { c.NAME.containsIgnoreCase(it) } + ?: DSL.trueCondition() - override fun findAllByLibraries(belongsToLibraryIds: Collection, filterOnLibraryIds: Collection?): Collection { + 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, filterOnLibraryIds: Collection?, search: String?, pageable: Pageable): Page { 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?): Collection { @@ -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.fetchAndMap() = - fetchGroups({ it.into(c) }, { it.into(cs) }) - .map { (cr, csr) -> - val seriesIds = csr.mapNotNull { it.seriesId } + private fun ResultQuery.fetchAndMap(filterOnLibraryIds: Collection?): List = + 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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/UnpagedSorted.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/UnpagedSorted.kt new file mode 100644 index 000000000..65c0c3dcb --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/UnpagedSorted.kt @@ -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 +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt index 1fe2a8c37..b145f3703 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/opds/OpdsController.kt @@ -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() } ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt index 9e3c3fcd0..50ec09c04 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionController.kt @@ -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? - ): List = - 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?, + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable + ): Page { + 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( diff --git a/komga/src/test/kotlin/org/gotson/komga/architecture/DomainDrivenDesignRulesTest.kt b/komga/src/test/kotlin/org/gotson/komga/architecture/DomainDrivenDesignRulesTest.kt index 5e28471ed..c9555bb0e 100644 --- a/komga/src/test/kotlin/org/gotson/komga/architecture/DomainDrivenDesignRulesTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/architecture/DomainDrivenDesignRulesTest.kt @@ -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() diff --git a/komga/src/test/kotlin/org/gotson/komga/architecture/NamingConventionTest.kt b/komga/src/test/kotlin/org/gotson/komga/architecture/NamingConventionTest.kt index 5fdeadb88..f5604d0a4 100644 --- a/komga/src/test/kotlin/org/gotson/komga/architecture/NamingConventionTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/architecture/NamingConventionTest.kt @@ -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() diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDaoTest.kt index 62be37cbd..f998d95e8 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDaoTest.kt @@ -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) diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionControllerTest.kt index 05fe6b4d7..dc3bfcd1a 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesCollectionControllerTest.kt @@ -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) } } }