diff --git a/komga/docs/openapi.json b/komga/docs/openapi.json index 87034b6e..533814df 100644 --- a/komga/docs/openapi.json +++ b/komga/docs/openapi.json @@ -23,11 +23,11 @@ "description": "Local development server", "variables": { "port": { - "default": "25600", "enum": [ "8080", "25600" - ] + ], + "default": "25600" } } } @@ -233,8 +233,9 @@ }, "/api/v1/age-ratings": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getAgeRatings", + "deprecated": true, + "description": "Use GET /v2/genres instead. Deprecated since 1.x.0", + "operationId": "getAgeRatings_1", "parameters": [ { "in": "query", @@ -285,6 +286,7 @@ }, "summary": "List age ratings", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -432,7 +434,9 @@ }, "/api/v1/authors/names": { "get": { - "operationId": "getAuthorsNames", + "deprecated": true, + "description": "Use GET /v2/authors/names instead. Deprecated since 1.x.0", + "operationId": "getAuthorsNames_1", "parameters": [ { "in": "query", @@ -471,13 +475,16 @@ }, "summary": "List authors\u0027 names", "tags": [ + "Deprecated", "Referential metadata" ] } }, "/api/v1/authors/roles": { "get": { - "operationId": "getAuthorsRoles", + "deprecated": true, + "description": "Use GET /v2/authors/roles instead. Deprecated since 1.x.0", + "operationId": "getAuthorsRoles_1", "responses": { "200": { "content": { @@ -505,6 +512,7 @@ }, "summary": "List authors\u0027 roles", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -3698,8 +3706,9 @@ }, "/api/v1/genres": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getGenres", + "deprecated": true, + "description": "Use GET /v2/genres instead. Deprecated since 1.x.0", + "operationId": "getGenres_1", "parameters": [ { "in": "query", @@ -3750,6 +3759,7 @@ }, "summary": "List genres", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -3817,8 +3827,9 @@ }, "/api/v1/languages": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getLanguages", + "deprecated": true, + "description": "Use GET /v2/languages instead. Deprecated since 1.x.0", + "operationId": "getLanguages_1", "parameters": [ { "in": "query", @@ -3869,6 +3880,7 @@ }, "summary": "List languages", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -4747,8 +4759,9 @@ }, "/api/v1/publishers": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getPublishers", + "deprecated": true, + "description": "Use GET /v2/genres instead. Deprecated since 1.x.0", + "operationId": "getPublishers_1", "parameters": [ { "in": "query", @@ -4799,6 +4812,7 @@ }, "summary": "List publishers", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -6560,8 +6574,9 @@ }, "/api/v1/series/release-dates": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getSeriesReleaseDates", + "deprecated": true, + "description": "Use GET /v2/genres instead. Deprecated since 1.x.0", + "operationId": "getSeriesReleaseDates_1", "parameters": [ { "in": "query", @@ -6612,6 +6627,7 @@ }, "summary": "List series release dates", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -7560,8 +7576,9 @@ }, "/api/v1/sharing-labels": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getSharingLabels", + "deprecated": true, + "description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", + "operationId": "getSharingLabels_1", "parameters": [ { "in": "query", @@ -7612,6 +7629,7 @@ }, "summary": "List sharing labels", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -7656,8 +7674,9 @@ }, "/api/v1/tags": { "get": { - "description": "Can be filtered by various criteria", - "operationId": "getTags", + "deprecated": true, + "description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", + "operationId": "getTags_1", "parameters": [ { "in": "query", @@ -7708,13 +7727,15 @@ }, "summary": "List tags", "tags": [ + "Deprecated", "Referential metadata" ] } }, "/api/v1/tags/book": { "get": { - "description": "Can be filtered by various criteria", + "deprecated": true, + "description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", "operationId": "getBookTags", "parameters": [ { @@ -7774,13 +7795,15 @@ }, "summary": "List book tags", "tags": [ + "Deprecated", "Referential metadata" ] } }, "/api/v1/tags/series": { "get": { - "description": "Can be filtered by various criteria", + "deprecated": true, + "description": "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", "operationId": "getSeriesTags", "parameters": [ { @@ -7828,6 +7851,7 @@ }, "summary": "List series tags", "tags": [ + "Deprecated", "Referential metadata" ] } @@ -8010,6 +8034,88 @@ ] } }, + "/api/v2/age-ratings": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getAgeRatings", + "parameters": [ + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageInteger" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List age ratings", + "tags": [ + "Referential metadata" + ] + } + }, "/api/v2/authors": { "get": { "description": "Can be filtered by various criteria", @@ -8048,7 +8154,11 @@ "name": "collection_id", "required": false, "schema": { - "type": "string" + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } }, { @@ -8056,7 +8166,11 @@ "name": "series_id", "required": false, "schema": { - "type": "string" + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } }, { @@ -8064,7 +8178,11 @@ "name": "readlist_id", "required": false, "schema": { - "type": "string" + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } }, { @@ -8120,6 +8238,586 @@ ] } }, + "/api/v2/authors/names": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getAuthorsNames", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "role", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "series_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "readlist_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List authors names", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/authors/roles": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getAuthorsRoles", + "parameters": [ + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "series_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "readlist_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List authors roles", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/genres": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getGenres", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List genres", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/languages": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getLanguages", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List languages", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/publishers": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getPublishers", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List publishers", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/series/release-dates": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getSeriesReleaseDates", + "parameters": [ + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List series release dates", + "tags": [ + "Referential metadata" + ] + } + }, "/api/v2/series/{seriesId}/read-progress/tachiyomi": { "get": { "description": "Mihon specific, due to how read progress is handled in Mihon.", @@ -8205,6 +8903,223 @@ ] } }, + "/api/v2/sharing-labels": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getSharingLabels", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List sharing labels", + "tags": [ + "Referential metadata" + ] + } + }, + "/api/v2/tags": { + "get": { + "description": "Can be filtered by various criteria", + "operationId": "getTags", + "parameters": [ + { + "in": "query", + "name": "search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "library_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "collection_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "series_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "readlist_id", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + { + "in": "query", + "name": "include", + "required": false, + "schema": { + "type": "string", + "enum": [ + "SERIES", + "BOOK", + "BOTH" + ] + } + }, + { + "in": "query", + "name": "unpaged", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "description": "Zero-based page index (0..N)", + "in": "query", + "name": "page", + "schema": { + "type": "integer" + } + }, + { + "description": "The size of the page to be returned", + "in": "query", + "name": "size", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + } + }, + "description": "Bad Request" + } + }, + "summary": "List tags", + "tags": [ + "Referential metadata" + ] + } + }, "/api/v2/users": { "get": { "description": "Required role: **ADMIN**", @@ -11787,6 +12702,53 @@ } } }, + "PageInteger": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "empty": { + "type": "boolean" + }, + "first": { + "type": "boolean" + }, + "last": { + "type": "boolean" + }, + "number": { + "type": "integer", + "format": "int32" + }, + "numberOfElements": { + "type": "integer", + "format": "int32" + }, + "pageable": { + "$ref": "#/components/schemas/PageableObject" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "sort": { + "$ref": "#/components/schemas/SortObject" + }, + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int32" + } + } + }, "PagePageHashKnownDto": { "type": "object", "properties": { @@ -12017,6 +12979,52 @@ } } }, + "PageString": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "type": "string" + } + }, + "empty": { + "type": "boolean" + }, + "first": { + "type": "boolean" + }, + "last": { + "type": "boolean" + }, + "number": { + "type": "integer", + "format": "int32" + }, + "numberOfElements": { + "type": "integer", + "format": "int32" + }, + "pageable": { + "$ref": "#/components/schemas/PageableObject" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "sort": { + "$ref": "#/components/schemas/SortObject" + }, + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int32" + } + } + }, "PageableObject": { "type": "object", "properties": { diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20260225161438__tags_view.sql b/komga/src/flyway/resources/db/migration/sqlite/V20260225161438__tags_view.sql new file mode 100644 index 00000000..8e387d9b --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20260225161438__tags_view.sql @@ -0,0 +1,6 @@ +CREATE VIEW SERIES_AND_BOOK_TAG AS +SELECT BMAT.TAG, BMAT.SERIES_ID +FROM BOOK_METADATA_AGGREGATION_TAG BMAT +UNION ALL +SELECT SMT.TAG, SMT.SERIES_ID +FROM SERIES_METADATA_TAG SMT; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/FilterBy.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/FilterBy.kt index 33cc8e2e..5ac8bab1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/FilterBy.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/FilterBy.kt @@ -11,3 +11,9 @@ data class FilterBy( val type: FilterByEntity, val ids: Set, ) + +enum class FilterTags { + SERIES, + BOOK, + BOTH, +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt index 16694a8e..a2c9c8a9 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt @@ -2,40 +2,47 @@ package org.gotson.komga.domain.persistence import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.FilterBy +import org.gotson.komga.domain.model.FilterTags import org.gotson.komga.domain.model.SearchContext import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import java.time.LocalDate interface ReferentialRepository { + @Deprecated("Use findAuthors instead") fun findAllAuthorsByName( search: String, filterOnLibraryIds: Collection?, ): List + @Deprecated("Use findAuthors instead") fun findAllAuthorsByNameAndLibrary( search: String, libraryId: String, filterOnLibraryIds: Collection?, ): List + @Deprecated("Use findAuthors instead") fun findAllAuthorsByNameAndCollection( search: String, collectionId: String, filterOnLibraryIds: Collection?, ): List + @Deprecated("Use findAuthors instead") fun findAllAuthorsByNameAndSeries( search: String, seriesId: String, filterOnLibraryIds: Collection?, ): List + @Deprecated("Use findAuthorsNames instead") fun findAllAuthorsNamesByName( search: String, filterOnLibraryIds: Collection?, ): List + @Deprecated("Use findAuthorsRoles instead") fun findAllAuthorsRoles(filterOnLibraryIds: Collection?): List fun findAuthors( @@ -46,116 +53,206 @@ interface ReferentialRepository { pageable: Pageable, ): Page + fun findAuthorsRoles( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + fun findAuthorsNames( + context: SearchContext, + search: String?, + role: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findGenres instead") fun findAllGenres(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findGenres instead") fun findAllGenresByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findGenres instead") fun findAllGenresByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + fun findGenres( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findTags instead") fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findTags instead") fun findAllSeriesAndBookTagsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findTags instead") fun findAllSeriesAndBookTagsByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findTags instead") fun findAllSeriesTags(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findTags instead") fun findAllSeriesTagsByLibrary( libraryId: String, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findTags instead") fun findAllSeriesTagsByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findTags instead") fun findAllBookTags(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findTags instead") fun findAllBookTagsBySeries( seriesId: String, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findTags instead") fun findAllBookTagsByReadList( readListId: String, filterOnLibraryIds: Collection?, ): Set + fun findTags( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + filterTags: FilterTags, + pageable: Pageable, + ): Page + + @Deprecated("Use findLanguages instead") fun findAllLanguages(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findLanguages instead") fun findAllLanguagesByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findLanguages instead") fun findAllLanguagesByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + fun findLanguages( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findPublishers instead") fun findAllPublishers(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findPublishers instead") fun findAllPublishers( filterOnLibraryIds: Collection?, pageable: Pageable, ): Page + @Deprecated("Use findPublishers instead") fun findAllPublishersByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findPublishers instead") fun findAllPublishersByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + fun findPublishers( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findAgeRatings instead") fun findAllAgeRatings(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findAgeRatings instead") fun findAllAgeRatingsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findAgeRatings instead") fun findAllAgeRatingsByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + fun findAgeRatings( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findSeriesReleaseDates instead") fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findSeriesReleaseDates instead") fun findAllSeriesReleaseDatesByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findSeriesReleaseDates instead") fun findAllSeriesReleaseDatesByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + fun findSeriesReleaseDates( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page + + @Deprecated("Use findSharingLabels instead") fun findAllSharingLabels(filterOnLibraryIds: Collection?): Set + @Deprecated("Use findSharingLabels instead") fun findAllSharingLabelsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, ): Set + @Deprecated("Use findSharingLabels instead") fun findAllSharingLabelsByCollection( collectionId: String, filterOnLibraryIds: Collection?, ): Set + + fun findSharingLabels( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt index 316531d5..06308004 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt @@ -19,7 +19,7 @@ private val logger = KotlinLogging.logger {} */ class BookSearchHelper( val context: SearchContext, -) : ContentRestrictionsSearchHelper() { +) { fun toCondition(searchCondition: SearchCondition.Book?): Pair> { val base = toCondition() val search = toConditionInternal(searchCondition) @@ -27,7 +27,7 @@ class BookSearchHelper( } fun toCondition(): Pair> { - val restrictions = toConditionInternal(context.restrictions) + val restrictions = ContentRestrictionsSearchHelper(context.restrictions).toCondition() val authorizedLibraries = toConditionInternal(context.libraryIds) return restrictions.first.and(authorizedLibraries.first) to (restrictions.second + authorizedLibraries.second) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ContentRestrictionsSearchHelper.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ContentRestrictionsSearchHelper.kt index fa80c354..828ae911 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ContentRestrictionsSearchHelper.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ContentRestrictionsSearchHelper.kt @@ -6,8 +6,10 @@ import org.gotson.komga.jooq.main.Tables import org.jooq.Condition import org.jooq.impl.DSL -abstract class ContentRestrictionsSearchHelper { - protected fun toConditionInternal(restrictions: ContentRestrictions): Pair> { +class ContentRestrictionsSearchHelper( + val restrictions: ContentRestrictions, +) { + fun toCondition(): Pair> { val ageAllowed = if (restrictions.ageRestriction?.restriction == AllowExclude.ALLOW_ONLY) { Tables.SERIES_METADATA.AGE_RATING.isNotNull diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesSearchHelper.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesSearchHelper.kt index 7bbfc0dd..07ad9256 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesSearchHelper.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesSearchHelper.kt @@ -17,7 +17,7 @@ private val logger = KotlinLogging.logger {} */ class SeriesSearchHelper( val context: SearchContext, -) : ContentRestrictionsSearchHelper() { +) { fun toCondition(searchCondition: SearchCondition.Series?): Pair> { val base = toCondition() val search = toConditionInternal(searchCondition) @@ -25,7 +25,7 @@ class SeriesSearchHelper( } fun toCondition(): Pair> { - val restrictions = toConditionInternal(context.restrictions) + val restrictions = ContentRestrictionsSearchHelper(context.restrictions).toCondition() val authorizedLibraries = toConditionInternal(context.libraryIds) return restrictions.first.and(authorizedLibraries.first) to (restrictions.second + authorizedLibraries.second) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt index efeb9802..f8a74be1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/Utils.kt @@ -10,6 +10,9 @@ import org.jooq.Condition import org.jooq.Field import org.jooq.SortField import org.jooq.impl.DSL +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort import java.io.ByteArrayOutputStream import java.util.zip.GZIPInputStream @@ -136,3 +139,18 @@ fun ObjectMapper.deserializeMediaExtension( fun rlbAlias(readListId: String) = Tables.READLIST_BOOK.`as`("RLB_$readListId") fun csAlias(collectionId: String) = Tables.COLLECTION_SERIES.`as`("CS_$collectionId") + +fun buildPage( + items: List, + pageable: Pageable, + count: Int, + sort: Sort?, +): PageImpl = + PageImpl( + items, + if (pageable.isPaged) + PageRequest.of(pageable.pageNumber, pageable.pageSize, sort ?: pageable.sort) + else + PageRequest.of(0, maxOf(count, 20), sort ?: pageable.sort), + count.toLong(), + ) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDao.kt index fa15685e..fb3ad40f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDao.kt @@ -3,24 +3,35 @@ package org.gotson.komga.infrastructure.jooq.main import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.FilterBy import org.gotson.komga.domain.model.FilterByEntity +import org.gotson.komga.domain.model.FilterTags import org.gotson.komga.domain.model.SearchContext import org.gotson.komga.domain.persistence.ReferentialRepository +import org.gotson.komga.infrastructure.jooq.ContentRestrictionsSearchHelper +import org.gotson.komga.infrastructure.jooq.RequiredJoin import org.gotson.komga.infrastructure.jooq.SplitDslDaoBase +import org.gotson.komga.infrastructure.jooq.buildPage import org.gotson.komga.infrastructure.jooq.udfStripAccents import org.gotson.komga.infrastructure.jooq.unicode3 import org.gotson.komga.jooq.main.Tables import org.gotson.komga.jooq.main.tables.records.BookMetadataAggregationAuthorRecord import org.gotson.komga.jooq.main.tables.records.BookMetadataAuthorRecord import org.gotson.komga.language.stripAccents +import org.jooq.Condition import org.jooq.DSLContext -import org.jooq.impl.DSL.noCondition +import org.jooq.OrderField +import org.jooq.SelectFieldOrAsterisk +import org.jooq.TableField +import org.jooq.impl.DSL import org.jooq.impl.DSL.select +import org.jooq.impl.TableImpl +import org.jooq.impl.TableRecordImpl import org.springframework.beans.factory.annotation.Qualifier 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.data.domain.Sort +import org.springframework.data.domain.Sort.Order import org.springframework.stereotype.Component import java.time.LocalDate @@ -43,7 +54,9 @@ class ReferentialDao( private val cs = Tables.COLLECTION_SERIES private val rb = Tables.READLIST_BOOK private val sl = Tables.SERIES_METADATA_SHARING + private val at = Tables.SERIES_AND_BOOK_TAG + @Deprecated("Use findAuthors instead") override fun findAllAuthorsByName( search: String, filterOnLibraryIds: Collection?, @@ -58,6 +71,7 @@ class ReferentialDao( .fetchInto(a) .map { it.toDomain() } + @Deprecated("Use findAuthors instead") override fun findAllAuthorsByNameAndLibrary( search: String, libraryId: String, @@ -75,6 +89,7 @@ class ReferentialDao( .fetchInto(bmaa) .map { it.toDomain() } + @Deprecated("Use findAuthors instead") override fun findAllAuthorsByNameAndCollection( search: String, collectionId: String, @@ -93,6 +108,7 @@ class ReferentialDao( .fetchInto(bmaa) .map { it.toDomain() } + @Deprecated("Use findAuthors instead") override fun findAllAuthorsByNameAndSeries( search: String, seriesId: String, @@ -115,55 +131,23 @@ class ReferentialDao( role: String?, filterBy: FilterBy?, pageable: Pageable, - ): Page { - val query = - dslRO - .selectDistinct(bmaa.NAME, bmaa.ROLE) - .from(bmaa) - .apply { if (!context.libraryIds.isNullOrEmpty() || filterBy?.type == FilterByEntity.LIBRARY) leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } - .apply { if (filterBy?.type == FilterByEntity.COLLECTION) leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID)) } - .apply { - if (filterBy?.type == FilterByEntity.READLIST) - leftJoin(b) - .on(bmaa.SERIES_ID.eq(b.SERIES_ID)) - .leftJoin(rb) - .on(b.ID.eq(rb.BOOK_ID)) - }.where(noCondition()) - .apply { search?.let { and(bmaa.NAME.udfStripAccents().contains(search.stripAccents())) } } - .apply { role?.let { and(bmaa.ROLE.eq(role)) } } - .apply { context.libraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } - .apply { - filterBy?.let { - when (it.type) { - FilterByEntity.LIBRARY -> and(s.LIBRARY_ID.`in`(it.ids)) - FilterByEntity.COLLECTION -> and(cs.COLLECTION_ID.`in`(it.ids)) - FilterByEntity.SERIES -> and(bmaa.SERIES_ID.`in`(it.ids)) - FilterByEntity.READLIST -> and(rb.READLIST_ID.`in`(it.ids)) - } - } - } + ): Page = findGeneric(context, search, filterBy, pageable, bmaa, bmaa.NAME, bmaa.SERIES_ID, { it?.toDomain() }, Sort.by("name"), listOf(bmaa.ROLE), role?.let { bmaa.ROLE.eq(role) }) - val count = dslRO.fetchCount(query) - val sort = bmaa.NAME.unicode3() + override fun findAuthorsRoles( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page = findGeneric(context, null, filterBy, pageable, bmaa, null, bmaa.SERIES_ID, { it?.role }, Sort.by("role"), listOf(bmaa.ROLE), sortField = bmaa.ROLE) - val items = - query - .orderBy(sort) - .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } - .fetchInto(a) - .map { it.toDomain() } - - val pageSort = pageable.sort - return PageImpl( - items, - if (pageable.isPaged) - PageRequest.of(pageable.pageNumber, pageable.pageSize, pageSort) - else - PageRequest.of(0, maxOf(count, 20), pageSort), - count.toLong(), - ) - } + override fun findAuthorsNames( + context: SearchContext, + search: String?, + role: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page = findGeneric(context, search, filterBy, pageable, bmaa, bmaa.NAME, bmaa.SERIES_ID, { it?.name }, Sort.by("name"), listOf(bmaa.ROLE), role?.let { bmaa.ROLE.eq(role) }) + @Deprecated("Use findAuthorsNames instead") override fun findAllAuthorsNamesByName( search: String, filterOnLibraryIds: Collection?, @@ -177,6 +161,7 @@ class ReferentialDao( .orderBy(a.NAME.unicode3()) .fetch(a.NAME) + @Deprecated("Use findAuthorsRoles instead") override fun findAllAuthorsRoles(filterOnLibraryIds: Collection?): List = dslRO .selectDistinct(a.ROLE) @@ -190,6 +175,7 @@ class ReferentialDao( }.orderBy(a.ROLE) .fetch(a.ROLE) + @Deprecated("Use findGenres instead") override fun findAllGenres(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(g.GENRE) @@ -203,6 +189,7 @@ class ReferentialDao( }.orderBy(g.GENRE.unicode3()) .fetchSet(g.GENRE) + @Deprecated("Use findGenres instead") override fun findAllGenresByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -217,6 +204,7 @@ class ReferentialDao( .orderBy(g.GENRE.unicode3()) .fetchSet(g.GENRE) + @Deprecated("Use findGenres instead") override fun findAllGenresByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -232,6 +220,18 @@ class ReferentialDao( .orderBy(g.GENRE.unicode3()) .fetchSet(g.GENRE) + override fun findGenres( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, search, filterBy, pageable, g, g.GENRE, g.SERIES_ID, { it?.genre }, Sort.by("genre")) + } + + @Deprecated("Use findTags instead") override fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection?): Set = dslRO .select(bt.TAG.`as`("tag")) @@ -245,6 +245,7 @@ class ReferentialDao( .sortedBy { it.stripAccents().lowercase() } .toSet() + @Deprecated("Use findTags instead") override fun findAllSeriesAndBookTagsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -267,6 +268,7 @@ class ReferentialDao( .sortedBy { it.stripAccents().lowercase() } .toSet() + @Deprecated("Use findTags instead") override fun findAllSeriesAndBookTagsByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -293,6 +295,7 @@ class ReferentialDao( .sortedBy { it.stripAccents().lowercase() } .toSet() + @Deprecated("Use findTags instead") override fun findAllSeriesTags(filterOnLibraryIds: Collection?): Set = dslRO .select(st.TAG) @@ -306,6 +309,7 @@ class ReferentialDao( }.orderBy(st.TAG.unicode3()) .fetchSet(st.TAG) + @Deprecated("Use findTags instead") override fun findAllSeriesTagsByLibrary( libraryId: String, filterOnLibraryIds: Collection?, @@ -320,6 +324,7 @@ class ReferentialDao( .orderBy(st.TAG.unicode3()) .fetchSet(st.TAG) + @Deprecated("Use findTags instead") override fun findAllBookTagsBySeries( seriesId: String, filterOnLibraryIds: Collection?, @@ -334,6 +339,7 @@ class ReferentialDao( .orderBy(bt.TAG.unicode3()) .fetchSet(bt.TAG) + @Deprecated("Use findTags instead") override fun findAllBookTagsByReadList( readListId: String, filterOnLibraryIds: Collection?, @@ -350,6 +356,20 @@ class ReferentialDao( .orderBy(bt.TAG.unicode3()) .fetchSet(bt.TAG) + override fun findTags( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + filterTags: FilterTags, + pageable: Pageable, + ): Page = + when (filterTags) { + FilterTags.SERIES -> findGeneric(context, search, filterBy, pageable, st, st.TAG, st.SERIES_ID, { it?.tag }, Sort.by("tag")) + FilterTags.BOOK -> findGeneric(context, search, filterBy, pageable, bmat, bmat.TAG, bmat.SERIES_ID, { it?.tag }, Sort.by("tag")) + FilterTags.BOTH -> findGeneric(context, search, filterBy, pageable, at, at.TAG, at.SERIES_ID, { it?.tag }, Sort.by("tag")) + } + + @Deprecated("Use findTags instead") override fun findAllSeriesTagsByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -365,6 +385,7 @@ class ReferentialDao( .orderBy(st.TAG.unicode3()) .fetchSet(st.TAG) + @Deprecated("Use findTags instead") override fun findAllBookTags(filterOnLibraryIds: Collection?): Set = dslRO .select(bt.TAG) @@ -378,6 +399,7 @@ class ReferentialDao( }.orderBy(bt.TAG.unicode3()) .fetchSet(bt.TAG) + @Deprecated("Use findLanguages instead") override fun findAllLanguages(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(sd.LANGUAGE) @@ -388,6 +410,7 @@ class ReferentialDao( .orderBy(sd.LANGUAGE) .fetchSet(sd.LANGUAGE) + @Deprecated("Use findLanguages instead") override fun findAllLanguagesByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -403,6 +426,7 @@ class ReferentialDao( .orderBy(sd.LANGUAGE) .fetchSet(sd.LANGUAGE) + @Deprecated("Use findLanguages instead") override fun findAllLanguagesByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -419,6 +443,18 @@ class ReferentialDao( .orderBy(sd.LANGUAGE) .fetchSet(sd.LANGUAGE) + override fun findLanguages( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, search, filterBy, pageable, sd, sd.LANGUAGE, sd.SERIES_ID, { it?.language }, Sort.by("language"), extraCondition = sd.LANGUAGE.ne("")) + } + + @Deprecated("Use findPublishers instead") override fun findAllPublishers(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(sd.PUBLISHER) @@ -429,6 +465,7 @@ class ReferentialDao( .orderBy(sd.PUBLISHER.unicode3()) .fetchSet(sd.PUBLISHER) + @Deprecated("Use findPublishers instead") override fun findAllPublishers( filterOnLibraryIds: Collection?, pageable: Pageable, @@ -461,6 +498,7 @@ class ReferentialDao( ) } + @Deprecated("Use findPublishers instead") override fun findAllPublishersByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -476,6 +514,7 @@ class ReferentialDao( .orderBy(sd.PUBLISHER.unicode3()) .fetchSet(sd.PUBLISHER) + @Deprecated("Use findPublishers instead") override fun findAllPublishersByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -492,6 +531,18 @@ class ReferentialDao( .orderBy(sd.PUBLISHER.unicode3()) .fetchSet(sd.PUBLISHER) + override fun findPublishers( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, search, filterBy, pageable, sd, sd.PUBLISHER, sd.SERIES_ID, { it?.publisher }, Sort.by("publisher"), extraCondition = sd.PUBLISHER.ne("")) + } + + @Deprecated("Use findAgeRatings instead") override fun findAllAgeRatings(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(sd.AGE_RATING) @@ -505,6 +556,7 @@ class ReferentialDao( }.orderBy(sd.AGE_RATING) .fetchSet(sd.AGE_RATING) + @Deprecated("Use findAgeRatings instead") override fun findAllAgeRatingsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -519,6 +571,7 @@ class ReferentialDao( .orderBy(sd.AGE_RATING) .fetchSet(sd.AGE_RATING) + @Deprecated("Use findAgeRatings instead") override fun findAllAgeRatingsByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -534,6 +587,17 @@ class ReferentialDao( .orderBy(sd.AGE_RATING) .fetchSet(sd.AGE_RATING) + override fun findAgeRatings( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, null, filterBy, pageable, sd, null, sd.SERIES_ID, { it?.ageRating }, Sort.by("ageRating"), listOf(sd.AGE_RATING), sortField = sd.AGE_RATING) + } + + @Deprecated("Use findSeriesReleaseDates instead") override fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(bma.RELEASE_DATE) @@ -544,6 +608,7 @@ class ReferentialDao( .orderBy(bma.RELEASE_DATE.desc()) .fetchSet(bma.RELEASE_DATE) + @Deprecated("Use findSeriesReleaseDates instead") override fun findAllSeriesReleaseDatesByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -559,6 +624,7 @@ class ReferentialDao( .orderBy(bma.RELEASE_DATE.desc()) .fetchSet(bma.RELEASE_DATE) + @Deprecated("Use findSeriesReleaseDates instead") override fun findAllSeriesReleaseDatesByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -575,6 +641,17 @@ class ReferentialDao( .orderBy(bma.RELEASE_DATE.desc()) .fetchSet(bma.RELEASE_DATE) + override fun findSeriesReleaseDates( + context: SearchContext, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, null, filterBy, pageable, bma, null, bma.SERIES_ID, { it?.releaseDate?.year?.toString() }, Sort.by(Order.desc("year")), listOf(bma.RELEASE_DATE), sortField = bma.RELEASE_DATE.desc()) + } + + @Deprecated("Use findSharingLabels instead") override fun findAllSharingLabels(filterOnLibraryIds: Collection?): Set = dslRO .selectDistinct(sl.LABEL) @@ -588,6 +665,7 @@ class ReferentialDao( }.orderBy(sl.LABEL.unicode3()) .fetchSet(sl.LABEL) + @Deprecated("Use findSharingLabels instead") override fun findAllSharingLabelsByLibraries( libraryIds: Set, filterOnLibraryIds: Collection?, @@ -602,6 +680,7 @@ class ReferentialDao( .orderBy(sl.LABEL.unicode3()) .fetchSet(sl.LABEL) + @Deprecated("Use findSharingLabels instead") override fun findAllSharingLabelsByCollection( collectionId: String, filterOnLibraryIds: Collection?, @@ -617,6 +696,89 @@ class ReferentialDao( .orderBy(sl.LABEL.unicode3()) .fetchSet(sl.LABEL) + override fun findSharingLabels( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + ): Page { + filterBy?.let { require(it.type in setOf(FilterByEntity.LIBRARY, FilterByEntity.COLLECTION)) } + + return findGeneric(context, search, filterBy, pageable, sl, sl.LABEL, sl.SERIES_ID, { it?.label }, Sort.by("label")) + } + + private fun , T : TableImpl, O : Any> findGeneric( + context: SearchContext, + search: String?, + filterBy: FilterBy?, + pageable: Pageable, + table: T, + searchableField: TableField?, + seriesIdField: TableField<*, String>, + mapper: (R?) -> O?, + sort: Sort, + extraFields: List = emptyList(), + extraCondition: Condition? = DSL.noCondition(), + sortField: OrderField<*>? = null, + ): Page { + val restrictionCondition = ContentRestrictionsSearchHelper(context.restrictions).toCondition() + + val query = + dslRO + .selectDistinct(*(listOfNotNull(searchableField) + extraFields).toTypedArray()) + .from(table) + .apply { + restrictionCondition.second.forEach { join -> + when (join) { + RequiredJoin.SeriesMetadata -> if (table != sd) innerJoin(sd).on(seriesIdField.eq(sd.SERIES_ID)) + // shouldn't be required + RequiredJoin.BookMetadata -> Unit + RequiredJoin.BookMetadataAggregation -> Unit + is RequiredJoin.Collection -> Unit + RequiredJoin.Media -> Unit + is RequiredJoin.ReadList -> Unit + is RequiredJoin.ReadProgress -> Unit + } + } + }.apply { if (!context.libraryIds.isNullOrEmpty() || filterBy?.type == FilterByEntity.LIBRARY) leftJoin(s).on(seriesIdField.eq(s.ID)) } + .apply { if (filterBy?.type == FilterByEntity.COLLECTION) leftJoin(cs).on(seriesIdField.eq(cs.SERIES_ID)) } + .apply { + if (filterBy?.type == FilterByEntity.READLIST) + leftJoin(b) + .on(seriesIdField.eq(b.SERIES_ID)) + .leftJoin(rb) + .on(b.ID.eq(rb.BOOK_ID)) + }.where(restrictionCondition.first) + .apply { extraCondition?.let { and(it) } } + .apply { if (search != null && searchableField != null) and(searchableField.udfStripAccents().contains(search.stripAccents())) } + .apply { context.libraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } + .apply { + filterBy?.let { + when (it.type) { + FilterByEntity.LIBRARY -> and(s.LIBRARY_ID.`in`(it.ids)) + FilterByEntity.COLLECTION -> and(cs.COLLECTION_ID.`in`(it.ids)) + FilterByEntity.SERIES -> and(seriesIdField.`in`(it.ids)) + FilterByEntity.READLIST -> and(rb.READLIST_ID.`in`(it.ids)) + } + } + } + + val count = dslRO.fetchCount(query) + + val items = + query + .apply { + if (sortField != null) + orderBy(sortField) + else if (searchableField != null) + orderBy(searchableField.unicode3()) + }.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } + .fetchInto(table) + .mapNotNull { mapper(it) } + + return buildPage(items, pageable, count, sort) + } + private fun BookMetadataAuthorRecord.toDomain(): Author = Author( name = name, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV1Controller.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV1Controller.kt index 22853819..d6e2aab2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV1Controller.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV1Controller.kt @@ -21,7 +21,7 @@ class ReferentialV1Controller( private val referentialRepository: ReferentialRepository, ) { @GetMapping("authors") - @Deprecated("Use GET /v2/authors instead", ReplaceWith("getAuthors")) + @Deprecated("Use GET /v2/authors instead") @Operation(summary = "List authors", description = "Use GET /api/v2/authors instead. Deprecated since 1.20.0.", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getAuthorsDeprecated( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -39,20 +39,26 @@ class ReferentialV1Controller( }.map { it.toDto() } @GetMapping("authors/names") - @Operation(summary = "List authors' names") + @Deprecated("Use GET /v2/authors/names instead") + // TODO: add deprecation release + @Operation(summary = "List authors' names", description = "Use GET /v2/authors/names instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getAuthorsNames( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", defaultValue = "") search: String, ): List = referentialRepository.findAllAuthorsNamesByName(search, principal.user.getAuthorizedLibraryIds(null)) @GetMapping("authors/roles") - @Operation(summary = "List authors' roles") + @Deprecated("Use GET /v2/authors/roles instead") + // TODO: add deprecation release + @Operation(summary = "List authors' roles", description = "Use GET /v2/authors/roles instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getAuthorsRoles( @AuthenticationPrincipal principal: KomgaPrincipal, ): List = referentialRepository.findAllAuthorsRoles(principal.user.getAuthorizedLibraryIds(null)) @GetMapping("genres") - @Operation(summary = "List genres", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/genres instead") + // TODO: add deprecation release + @Operation(summary = "List genres", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getGenres( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -65,7 +71,9 @@ class ReferentialV1Controller( } @GetMapping("sharing-labels") - @Operation(summary = "List sharing labels", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/sharing-labels instead") + // TODO: add deprecation release + @Operation(summary = "List sharing labels", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getSharingLabels( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -78,7 +86,9 @@ class ReferentialV1Controller( } @GetMapping("tags") - @Operation(summary = "List tags", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/tags instead") + // TODO: add deprecation release + @Operation(summary = "List tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -91,7 +101,9 @@ class ReferentialV1Controller( } @GetMapping("tags/book") - @Operation(summary = "List book tags", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/tags instead") + // TODO: add deprecation release + @Operation(summary = "List book tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getBookTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "series_id", required = false) seriesId: String?, @@ -106,7 +118,9 @@ class ReferentialV1Controller( } @GetMapping("tags/series") - @Operation(summary = "List series tags", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/tags instead") + // TODO: add deprecation release + @Operation(summary = "List series tags", description = "Use GET /v2/sharing-labels instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getSeriesTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -119,7 +133,9 @@ class ReferentialV1Controller( } @GetMapping("languages") - @Operation(summary = "List languages", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/languages instead") + // TODO: add deprecation release + @Operation(summary = "List languages", description = "Use GET /v2/languages instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getLanguages( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -132,7 +148,9 @@ class ReferentialV1Controller( } @GetMapping("publishers") - @Operation(summary = "List publishers", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/publishers instead") + // TODO: add deprecation release + @Operation(summary = "List publishers", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getPublishers( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -145,7 +163,9 @@ class ReferentialV1Controller( } @GetMapping("age-ratings") - @Operation(summary = "List age ratings", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/age-ratings instead") + // TODO: add deprecation release + @Operation(summary = "List age ratings", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getAgeRatings( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), @@ -158,7 +178,9 @@ class ReferentialV1Controller( }.map { it?.toString() ?: "None" }.toSet() @GetMapping("series/release-dates") - @Operation(summary = "List series release dates", description = "Can be filtered by various criteria") + @Deprecated("Use GET /v2/age-ratings instead") + // TODO: add deprecation release + @Operation(summary = "List series release dates", description = "Use GET /v2/genres instead. Deprecated since 1.x.0", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getSeriesReleaseDates( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV2Controller.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV2Controller.kt index 82222a62..fad4d4b0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV2Controller.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialV2Controller.kt @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.domain.model.FilterBy import org.gotson.komga.domain.model.FilterByEntity +import org.gotson.komga.domain.model.FilterTags import org.gotson.komga.domain.model.SearchContext import org.gotson.komga.domain.persistence.ReferentialRepository import org.gotson.komga.infrastructure.openapi.OpenApiConfiguration @@ -36,9 +37,9 @@ class ReferentialV2Controller( @RequestParam(name = "search", required = false) search: String?, @RequestParam(name = "role", required = false) role: String?, @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), - @RequestParam(name = "collection_id", required = false) collectionId: String?, - @RequestParam(name = "series_id", required = false) seriesId: String?, - @RequestParam(name = "readlist_id", required = false) readListId: String?, + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "series_id", required = false) seriesIds: Set = emptySet(), + @RequestParam(name = "readlist_id", required = false) readListIds: Set = emptySet(), @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, @Parameter(hidden = true) page: Pageable, ): Page { @@ -51,12 +52,302 @@ class ReferentialV2Controller( page.pageSize, ) - return when { - libraryIds.isNotEmpty() -> referentialRepository.findAuthors(SearchContext(principal.user), search, role, FilterBy(FilterByEntity.LIBRARY, libraryIds), pageRequest) - collectionId != null -> referentialRepository.findAuthors(SearchContext(principal.user), search, role, FilterBy(FilterByEntity.COLLECTION, setOf(collectionId)), pageRequest) - seriesId != null -> referentialRepository.findAuthors(SearchContext(principal.user), search, role, FilterBy(FilterByEntity.SERIES, setOf(seriesId)), pageRequest) - readListId != null -> referentialRepository.findAuthors(SearchContext(principal.user), search, role, FilterBy(FilterByEntity.READLIST, setOf(readListId)), pageRequest) - else -> referentialRepository.findAuthors(SearchContext(principal.user), search, role, null, pageRequest) - }.map { it.toDto() } + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + seriesIds.isNotEmpty() -> FilterBy(FilterByEntity.SERIES, seriesIds) + readListIds.isNotEmpty() -> FilterBy(FilterByEntity.READLIST, readListIds) + else -> null + } + + return referentialRepository.findAuthors(SearchContext(principal.user), search, role, filterBy, pageRequest).map { it.toDto() } + } + + @PageableWithoutSortAsQueryParam + @GetMapping("authors/roles") + @Operation(summary = "List authors roles", description = "Can be filtered by various criteria") + fun getAuthorsRoles( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "series_id", required = false) seriesIds: Set = emptySet(), + @RequestParam(name = "readlist_id", required = false) readListIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + seriesIds.isNotEmpty() -> FilterBy(FilterByEntity.SERIES, seriesIds) + readListIds.isNotEmpty() -> FilterBy(FilterByEntity.READLIST, readListIds) + else -> null + } + + return referentialRepository.findAuthorsRoles(SearchContext(principal.user), filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("authors/names") + @Operation(summary = "List authors names", description = "Can be filtered by various criteria") + fun getAuthorsNames( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "role", required = false) role: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "series_id", required = false) seriesIds: Set = emptySet(), + @RequestParam(name = "readlist_id", required = false) readListIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + seriesIds.isNotEmpty() -> FilterBy(FilterByEntity.SERIES, seriesIds) + readListIds.isNotEmpty() -> FilterBy(FilterByEntity.READLIST, readListIds) + else -> null + } + + return referentialRepository.findAuthorsNames(SearchContext(principal.user), search, role, filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("genres") + @Operation(summary = "List genres", description = "Can be filtered by various criteria") + fun getGenres( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findGenres(SearchContext(principal.user), search, filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("sharing-labels") + @Operation(summary = "List sharing labels", description = "Can be filtered by various criteria") + fun getSharingLabels( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findSharingLabels(SearchContext(principal.user), search, filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("languages") + @Operation(summary = "List languages", description = "Can be filtered by various criteria") + fun getLanguages( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findLanguages(SearchContext(principal.user), search, filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("publishers") + @Operation(summary = "List publishers", description = "Can be filtered by various criteria") + fun getPublishers( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findPublishers(SearchContext(principal.user), search, filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("tags") + @Operation(summary = "List tags", description = "Can be filtered by various criteria") + fun getTags( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "search", required = false) search: String?, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "series_id", required = false) seriesIds: Set = emptySet(), + @RequestParam(name = "readlist_id", required = false) readListIds: Set = emptySet(), + @RequestParam(name = "include", required = false) includeTags: FilterTags = FilterTags.BOTH, + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + seriesIds.isNotEmpty() -> FilterBy(FilterByEntity.SERIES, seriesIds) + readListIds.isNotEmpty() -> FilterBy(FilterByEntity.READLIST, readListIds) + else -> null + } + + return referentialRepository.findTags(SearchContext(principal.user), search, filterBy, includeTags, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("series/release-dates") + @Operation(summary = "List series release dates", description = "Can be filtered by various criteria") + fun getSeriesReleaseDates( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findSeriesReleaseDates(SearchContext(principal.user), filterBy, pageRequest) + } + + @PageableWithoutSortAsQueryParam + @GetMapping("age-ratings") + @Operation(summary = "List age ratings", description = "Can be filtered by various criteria") + fun getAgeRatings( + @AuthenticationPrincipal principal: KomgaPrincipal, + @RequestParam(name = "library_id", required = false) libraryIds: Set = emptySet(), + @RequestParam(name = "collection_id", required = false) collectionIds: Set = emptySet(), + @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, + @Parameter(hidden = true) page: Pageable, + ): Page { + val pageRequest = + if (unpaged) + Pageable.unpaged() + else + PageRequest.of( + page.pageNumber, + page.pageSize, + ) + + val filterBy = + when { + libraryIds.isNotEmpty() -> FilterBy(FilterByEntity.LIBRARY, libraryIds) + + collectionIds.isNotEmpty() -> FilterBy(FilterByEntity.COLLECTION, collectionIds) + else -> null + } + + return referentialRepository.findAgeRatings(SearchContext(principal.user), filterBy, pageRequest) } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDaoTest.kt new file mode 100644 index 00000000..4c1d5179 --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/ReferentialDaoTest.kt @@ -0,0 +1,773 @@ +package org.gotson.komga.infrastructure.jooq.main + +import com.ninjasquad.springmockk.MockkBean +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.domain.model.AgeRestriction +import org.gotson.komga.domain.model.AllowExclude +import org.gotson.komga.domain.model.Author +import org.gotson.komga.domain.model.ContentRestrictions +import org.gotson.komga.domain.model.FilterBy +import org.gotson.komga.domain.model.FilterByEntity +import org.gotson.komga.domain.model.FilterTags +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.model.SearchContext +import org.gotson.komga.domain.model.makeBook +import org.gotson.komga.domain.model.makeLibrary +import org.gotson.komga.domain.model.makeSeries +import org.gotson.komga.domain.persistence.BookMetadataRepository +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.SeriesMetadataRepository +import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.domain.service.BookLifecycle +import org.gotson.komga.domain.service.KomgaUserLifecycle +import org.gotson.komga.domain.service.LibraryLifecycle +import org.gotson.komga.domain.service.SeriesLifecycle +import org.gotson.komga.domain.service.SeriesMetadataLifecycle +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationEventPublisher +import org.springframework.data.domain.Pageable +import java.time.LocalDate + +@SpringBootTest +class ReferentialDaoTest( + @Autowired private val referentialDao: ReferentialDao, + @Autowired private val userRepository: KomgaUserRepository, + @Autowired private val bookRepository: BookRepository, + @Autowired private val seriesRepository: SeriesRepository, + @Autowired private val seriesMetadataLifecycle: SeriesMetadataLifecycle, + @Autowired private val bookMetadataRepository: BookMetadataRepository, + @Autowired private val bookLifecycle: BookLifecycle, + @Autowired private val userLifecycle: KomgaUserLifecycle, + @Autowired private val seriesLifecycle: SeriesLifecycle, + @Autowired private val libraryRepository: LibraryRepository, + @Autowired private val libraryLifecycle: LibraryLifecycle, + @Autowired private val seriesMetadataRepository: SeriesMetadataRepository, +) { + private val library1 = makeLibrary() + private val library2 = makeLibrary() + private val series1 = makeSeries("Series 1").copy(libraryId = library1.id) + private val series2 = makeSeries("Series 2").copy(libraryId = library2.id) + private val seriesEmpty = makeSeries("Series Empty").copy(libraryId = library2.id) + private val seriesShared = makeSeries("Series Shared").copy(libraryId = library1.id) + private val seriesAge10 = makeSeries("Series Age 10").copy(libraryId = library1.id) + private val userAll = KomgaUser("user1@example.org", "p") + private val userLib1 = KomgaUser("user2@example.org", "p", sharedLibrariesIds = setOf(library1.id), sharedAllLibraries = false) + private val userLabelAllow = KomgaUser("user3@example.org", "p", restrictions = ContentRestrictions(labelsAllow = setOf("item_shared"))) + private val userAge10 = KomgaUser("user4@example.org", "p", restrictions = ContentRestrictions(ageRestriction = AgeRestriction(10, AllowExclude.ALLOW_ONLY))) + + @MockkBean + private lateinit var mockEventPublisher: ApplicationEventPublisher + + @BeforeAll + fun setup() { + every { mockEventPublisher.publishEvent(any()) } just Runs + libraryRepository.insert(library1) + libraryRepository.insert(library2) + seriesLifecycle.createSeries(series1) + seriesLifecycle.createSeries(series2) + seriesLifecycle.createSeries(seriesEmpty) + seriesLifecycle.createSeries(seriesShared) + seriesLifecycle.createSeries(seriesAge10) + userRepository.insert(userAll) + userRepository.insert(userLib1) + userRepository.insert(userLabelAllow) + userRepository.insert(userAge10) + + // setup restrictions context + seriesMetadataRepository.findById(seriesShared.id).let { + seriesMetadataRepository.update(it.copy(sharingLabels = setOf("item_shared"))) + } + + seriesMetadataRepository.findById(seriesAge10.id).let { + seriesMetadataRepository.update(it.copy(ageRating = 10)) + } + + // prepare metadata + makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book -> + seriesLifecycle.addBooks(series1, listOf(book)) + bookMetadataRepository.findById(book.id).let { + bookMetadataRepository.update(it.copy(authors = listOf(Author("item1", "writer")), releaseDate = LocalDate.of(2001, 1, 1), tags = setOf("bt1"))) + } + seriesMetadataRepository.findById(series1.id).let { + seriesMetadataRepository.update(it.copy(genres = setOf("item1"), sharingLabels = setOf("item1"), language = "fr", publisher = "item1", ageRating = 18, tags = setOf("st1"))) + } + } + seriesMetadataLifecycle.aggregateMetadata(series1) + + makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book -> + seriesLifecycle.addBooks(series2, listOf(book)) + bookMetadataRepository.findById(book.id).let { + bookMetadataRepository.update(it.copy(authors = listOf(Author("item2", "inker")), releaseDate = LocalDate.of(2002, 1, 1), tags = setOf("bt2"))) + } + seriesMetadataRepository.findById(series2.id).let { + seriesMetadataRepository.update(it.copy(genres = setOf("item2"), sharingLabels = setOf("item2"), language = "en", publisher = "item2", ageRating = 19, tags = setOf("st2"))) + } + } + seriesMetadataLifecycle.aggregateMetadata(series2) + + makeBook("Empty", libraryId = library2.id, seriesId = seriesEmpty.id).let { book -> + seriesLifecycle.addBooks(seriesEmpty, listOf(book)) + } + seriesMetadataLifecycle.aggregateMetadata(seriesEmpty) + + makeBook("shared", libraryId = library1.id, seriesId = seriesShared.id).let { book -> + seriesLifecycle.addBooks(seriesShared, listOf(book)) + bookMetadataRepository.findById(book.id).let { + bookMetadataRepository.update(it.copy(authors = listOf(Author("item_shared", "penciller")), releaseDate = LocalDate.of(2003, 1, 1), tags = setOf("bt_shared"))) + } + seriesMetadataRepository.findById(seriesShared.id).let { + seriesMetadataRepository.update(it.copy(genres = setOf("item_shared"), language = "ja", publisher = "item_shared", tags = setOf("st_shared"))) + } + } + seriesMetadataLifecycle.aggregateMetadata(seriesShared) + + makeBook("10", libraryId = library1.id, seriesId = seriesAge10.id).let { book -> + seriesLifecycle.addBooks(seriesAge10, listOf(book)) + bookMetadataRepository.findById(book.id).let { + bookMetadataRepository.update(it.copy(authors = listOf(Author("item_10", "cover")), releaseDate = LocalDate.of(2004, 1, 1), tags = setOf("bt_10"))) + } + seriesMetadataRepository.findById(seriesAge10.id).let { + seriesMetadataRepository.update(it.copy(genres = setOf("item_10"), sharingLabels = setOf("item_10"), language = "sp", publisher = "item_10", tags = setOf("st_10"))) + } + } + seriesMetadataLifecycle.aggregateMetadata(seriesAge10) + } + + @BeforeEach + fun resetMocks() { + every { mockEventPublisher.publishEvent(any()) } just Runs + } + + @AfterEach + fun deleteBooks() { + bookLifecycle.deleteMany(bookRepository.findAll()) + assertThat(bookRepository.count()).isEqualTo(0) + } + + @AfterAll + fun tearDown() { + every { mockEventPublisher.publishEvent(any()) } just Runs + userRepository.findAll().forEach { + userLifecycle.deleteUser(it) + } + libraryRepository.findAll().forEach { + libraryLifecycle.deleteLibrary(it) + } + } + + @Nested + inner class Author { + @Test + fun `given search when getting authors then matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthors(context, "shared", null, null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given role when getting authors then matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthors(context, null, "writer", null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item1") + } + + @Test + fun `given filter by library when getting authors then only matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthors(context, null, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item2") + } + + @Test + fun `given user without restrictions when getting authors then all authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthors(context, null, null, null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item1", "item2", "item_shared", "item_10") + } + + @Test + fun `given user with restricted library access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findAuthors(context, null, null, null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item1", "item_shared", "item_10") + } + + @Test + fun `given user with restricted label access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findAuthors(context, null, null, null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given user with restricted age access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findAuthors(context, null, null, null, Pageable.unpaged()).content + + assertThat(items.map { it.name }).containsExactlyInAnyOrder("item_10") + } + } + + @Nested + inner class AuthorName { + @Test + fun `given search when getting authors then matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsNames(context, "shared", null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given role when getting authors then matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsNames(context, null, "writer", null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1") + } + + @Test + fun `given filter by library when getting authors then only matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsNames(context, null, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item2") + } + + @Test + fun `given user without restrictions when getting authors then all authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsNames(context, null, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item2", "item_shared", "item_10") + } + + @Test + fun `given user with restricted library access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findAuthorsNames(context, null, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item_shared", "item_10") + } + + @Test + fun `given user with restricted label access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findAuthorsNames(context, null, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given user with restricted age access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findAuthorsNames(context, null, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_10") + } + } + + @Nested + inner class AuthorRole { + @Test + fun `given filter by library when getting authors then only matching authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsRoles(context, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("inker") + } + + @Test + fun `given user without restrictions when getting authors then all authors are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAuthorsRoles(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("writer", "inker", "penciller", "cover") + } + + @Test + fun `given user with restricted library access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findAuthorsRoles(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("writer", "penciller", "cover") + } + + @Test + fun `given user with restricted label access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findAuthorsRoles(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("penciller") + } + + @Test + fun `given user with restricted age access when getting authors then only allowed authors are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findAuthorsRoles(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("cover") + } + } + + @Nested + inner class Genre { + @Test + fun `given search when getting genres then all genres are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findGenres(context, "shared", null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given filter by library when getting genres then only matching genres are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findGenres(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item2") + } + + @Test + fun `given user without restrictions when getting genres then all genres are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findGenres(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item2", "item_shared", "item_10") + } + + @Test + fun `given user with restricted library access when getting genres then only allowed genres are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findGenres(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item_shared", "item_10") + } + + @Test + fun `given user with restricted label access when getting genres then only allowed genres are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findGenres(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given user with restricted age access when getting genres then only allowed genres are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findGenres(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_10") + } + } + + @Nested + inner class BookTag { + @Test + fun `given search when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, "shared", null, FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt_shared") + } + + @Test + fun `given filter by library when getting tags then only matching tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt2") + } + + @Test + fun `given user without restrictions when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, null, FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt1", "bt2", "bt_shared", "bt_10") + } + + @Test + fun `given user with restricted library access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findTags(context, null, null, FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt1", "bt_shared", "bt_10") + } + + @Test + fun `given user with restricted label access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findTags(context, null, null, FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt_shared") + } + + @Test + fun `given user with restricted age access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findTags(context, null, null, FilterTags.BOOK, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("bt_10") + } + } + + @Nested + inner class SeriesTag { + @Test + fun `given search when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, "shared", null, FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_shared") + } + + @Test + fun `given filter by library when getting tags then only matching tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st2") + } + + @Test + fun `given user without restrictions when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, null, FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st1", "st2", "st_shared", "st_10") + } + + @Test + fun `given user with restricted library access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findTags(context, null, null, FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st1", "st_shared", "st_10") + } + + @Test + fun `given user with restricted label access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findTags(context, null, null, FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_shared") + } + + @Test + fun `given user with restricted age access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findTags(context, null, null, FilterTags.SERIES, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_10") + } + } + + @Nested + inner class BothTag { + @Test + fun `given search when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, "shared", null, FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_shared", "bt_shared") + } + + @Test + fun `given filter by library when getting tags then only matching tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st2", "bt2") + } + + @Test + fun `given user without restrictions when getting tags then all tags are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findTags(context, null, null, FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st1", "st2", "st_shared", "st_10", "bt1", "bt2", "bt_shared", "bt_10") + } + + @Test + fun `given user with restricted library access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findTags(context, null, null, FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st1", "st_shared", "st_10", "bt1", "bt_shared", "bt_10") + } + + @Test + fun `given user with restricted label access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findTags(context, null, null, FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_shared", "bt_shared") + } + + @Test + fun `given user with restricted age access when getting tags then only allowed tags are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findTags(context, null, null, FilterTags.BOTH, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("st_10", "bt_10") + } + } + + @Nested + inner class SharingLabel { + @Test + fun `given search when getting sharing labels then all sharing labels are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findSharingLabels(context, "shared", null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given filter by library when getting sharing labels then only matching sharing labels are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findSharingLabels(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item2") + } + + @Test + fun `given user without restrictions when getting sharing labels then all sharing labels are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findSharingLabels(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item2", "item_shared", "item_10") + } + + @Test + fun `given user with restricted library access when getting sharing labels then only allowed sharing labels are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findSharingLabels(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item_shared", "item_10") + } + + @Test + fun `given user with restricted label access when getting sharing labels then only allowed sharing labels are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findSharingLabels(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given user with restricted age access when getting sharing labels then only allowed sharing labels are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findSharingLabels(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_10") + } + } + + @Nested + inner class Publisher { + @Test + fun `given search when getting publishers then all publishers are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findPublishers(context, "shared", null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given filter by library when getting publishers then only matching publishers are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findPublishers(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item2") + } + + @Test + fun `given user without restrictions when getting publishers then all publishers are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findPublishers(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item2", "item_shared", "item_10") + } + + @Test + fun `given user with restricted library access when getting publishers then only allowed publishers are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findPublishers(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item1", "item_shared", "item_10") + } + + @Test + fun `given user with restricted label access when getting publishers then only allowed publishers are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findPublishers(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_shared") + } + + @Test + fun `given user with restricted age access when getting publishers then only allowed publishers are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findPublishers(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("item_10") + } + } + + @Nested + inner class Language { + @Test + fun `given search when getting languages then all languages are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findLanguages(context, "j", null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("ja") + } + + @Test + fun `given filter by library when getting languages then only matching languages are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findLanguages(context, null, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("en") + } + + @Test + fun `given user without restrictions when getting languages then all languages are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findLanguages(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("fr", "en", "ja", "sp") + } + + @Test + fun `given user with restricted library access when getting languages then only allowed languages are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findLanguages(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("fr", "ja", "sp") + } + + @Test + fun `given user with restricted label access when getting languages then only allowed languages are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findLanguages(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("ja") + } + + @Test + fun `given user with restricted age access when getting languages then only allowed languages are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findLanguages(context, null, null, Pageable.unpaged()).content + + assertThat(items).containsExactlyInAnyOrder("sp") + } + } + + @Nested + inner class AgeRating { + @Test + fun `given filter by library when getting age ratings then only matching age ratings are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAgeRatings(context, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactly(19) + } + + @Test + fun `given user without restrictions when getting age ratings then all age ratings are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findAgeRatings(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly(10, 18, 19) + } + + @Test + fun `given user with restricted library access when getting age ratings then only allowed age ratings are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findAgeRatings(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly(10, 18) + } + + @Test + fun `given user with restricted label access when getting age ratings then only allowed age ratings are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findAgeRatings(context, null, Pageable.unpaged()).content + + assertThat(items).isEmpty() + } + + @Test + fun `given user with restricted age access when getting age ratings then only allowed age ratings are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findAgeRatings(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly(10) + } + } + + @Nested + inner class SeriesReleaseDate { + @Test + fun `given filter by library when getting series release dates then only matching series release dates are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findSeriesReleaseDates(context, FilterBy(FilterByEntity.LIBRARY, setOf(library2.id)), Pageable.unpaged()).content + + assertThat(items).containsExactly("2002") + } + + @Test + fun `given user without restrictions when getting series release dates then all series release dates are returned`() { + val context = SearchContext(userAll) + val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly("2004", "2003", "2002", "2001") + } + + @Test + fun `given user with restricted library access when getting series release dates then only allowed series release dates are returned`() { + val context = SearchContext(userLib1) + val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly("2004", "2003", "2001") + } + + @Test + fun `given user with restricted label access when getting series release dates then only allowed series release dates are returned`() { + val context = SearchContext(userLabelAllow) + val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly("2003") + } + + @Test + fun `given user with restricted age access when getting series release dates then only allowed series release dates are returned`() { + val context = SearchContext(userAge10) + val items = referentialDao.findSeriesReleaseDates(context, null, Pageable.unpaged()).content + + assertThat(items).containsExactly("2004") + } + } +}