From 733b9c99198ed201a5388fda44f7d54b90e6e562 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Wed, 17 Jun 2026 14:47:28 -0400 Subject: [PATCH] Proper extension store queries --- .../dataLoaders/ExtensionStoreDataLoader.kt | 20 ++ .../graphql/queries/ExtensionStoreQuery.kt | 184 ++++++++++++++++-- .../TachideskDataLoaderRegistryFactory.kt | 2 + .../graphql/types/ExtensionStoreType.kt | 46 +++++ 4 files changed, 236 insertions(+), 16 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionStoreDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionStoreDataLoader.kt index eaa485685..bba4788b6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionStoreDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionStoreDataLoader.kt @@ -16,6 +16,26 @@ import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.server.JavalinSetup.future +class ExtensionStoreDataLoader : KotlinDataLoader { + override val dataLoaderName = "ExtensionStoreDataLoader" + + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = + DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(Slf4jSqlDebugLogger) + val manga = + ExtensionStoreTable + .selectAll() + .where { ExtensionStoreTable.indexUrl inList ids } + .map { ExtensionStoreType(it) } + .associateBy { it.indexUrl } + ids.map { manga[it] } + } + } + } +} + class ExtensionStoreForExtension : KotlinDataLoader { override val dataLoaderName = "ExtensionStoreForExtension" diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionStoreQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionStoreQuery.kt index effb26142..ed6ecec9f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionStoreQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionStoreQuery.kt @@ -7,33 +7,185 @@ package suwayomi.tachidesk.graphql.queries * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import org.jetbrains.exposed.v1.core.eq +import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.v1.core.Column +import org.jetbrains.exposed.v1.core.Op +import org.jetbrains.exposed.v1.core.SortOrder import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.transactions.transaction import suwayomi.tachidesk.graphql.directives.RequireAuth +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString +import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Order +import suwayomi.tachidesk.graphql.server.primitives.OrderBy +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults +import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter +import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique +import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique +import suwayomi.tachidesk.graphql.server.primitives.maybeSwap +import suwayomi.tachidesk.graphql.types.ExtensionStoreNodeList import suwayomi.tachidesk.graphql.types.ExtensionStoreType import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable import java.util.concurrent.CompletableFuture class ExtensionStoreQuery { @RequireAuth - fun extensionStore(indexUrl: String): CompletableFuture = - CompletableFuture.supplyAsync { - transaction { - ExtensionStoreTable - .selectAll() - .where { ExtensionStoreTable.indexUrl eq indexUrl } - .firstOrNull() - ?.let { ExtensionStoreType(it) } + fun extensionStore( + dataFetchingEnvironment: DataFetchingEnvironment, + indexUrl: String, + ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("ExtensionStoreDataLoader", indexUrl) + + enum class ExtensionStoreOrderBy( + override val column: Column<*>, + ) : OrderBy { + NAME(ExtensionStoreTable.name), + INDEX_URL(ExtensionStoreTable.indexUrl), + ; + + override fun greater(cursor: Cursor): Op = + when (this) { + NAME -> greaterNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString) + INDEX_URL -> greaterNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString) } + + override fun less(cursor: Cursor): Op = + when (this) { + NAME -> lessNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString) + INDEX_URL -> lessNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString) + } + + override fun asCursor(type: ExtensionStoreType): Cursor { + val value = + when (this) { + INDEX_URL -> type.indexUrl + NAME -> type.indexUrl + "-" + type.name + } + return Cursor(value) } + } + + data class ExtensionStoreOrder( + override val by: ExtensionStoreOrderBy, + override val byType: SortOrder? = null, + ) : Order + + data class ExtensionStoreCondition( + val id: Int? = null, + val indexUrl: String? = null, + val name: String? = null, + ) : HasGetOp { + override fun getOp(): Op? { + val opAnd = OpAnd() + opAnd.eq(id, ExtensionStoreTable.id) + opAnd.eq(indexUrl, ExtensionStoreTable.indexUrl) + opAnd.eq(name, ExtensionStoreTable.name) + + return opAnd.op + } + } + + data class ExtensionStoreFilter( + val indexUrl: StringFilter? = null, + val name: StringFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: ExtensionStoreFilter? = null, + ) : Filter { + override fun getOpList(): List> = + listOfNotNull( + andFilterWithCompareString(ExtensionStoreTable.indexUrl, indexUrl), + andFilterWithCompareString(ExtensionStoreTable.name, name), + ) + } @RequireAuth - fun extensionStores(): List = - transaction { - ExtensionStoreTable - .selectAll() - .toList() - .map { ExtensionStoreType(it) } - } + fun extensionStores( + condition: ExtensionStoreCondition? = null, + filter: ExtensionStoreFilter? = null, + order: List? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null, + ): ExtensionStoreNodeList { + val queryResults = + transaction { + val res = ExtensionStoreTable.selectAll() + + res.applyOps(condition, filter) + + if (order != null || (last != null || before != null)) { + val baseSort = listOf(ExtensionStoreOrder(ExtensionStoreOrderBy.INDEX_URL, SortOrder.ASC)) + val actualSort = (order.orEmpty() + baseSort) + actualSort.forEach { (orderBy, orderByType) -> + val orderByColumn = orderBy.column + val orderType = orderByType.maybeSwap(last ?: before) + + res.orderBy(orderByColumn to orderType) + } + } + + val total = res.count() + val firstResult = res.firstOrNull()?.get(ExtensionStoreTable.indexUrl) + val lastResult = res.lastOrNull()?.get(ExtensionStoreTable.indexUrl) + + res.applyBeforeAfter( + before = before, + after = after, + orderBy = order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL, + orderByType = order?.firstOrNull()?.byType, + ) + + if (first != null) { + res.limit(first).offset(offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) + } + + val getAsCursor: (ExtensionStoreType) -> Cursor = (order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL)::asCursor + + val resultsAsType = queryResults.results.map { ExtensionStoreType(it) } + + return ExtensionStoreNodeList( + resultsAsType, + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOfNotNull( + resultsAsType.firstOrNull()?.let { + ExtensionStoreNodeList.ExtensionStoreEdge( + getAsCursor(it), + it, + ) + }, + resultsAsType.lastOrNull()?.let { + ExtensionStoreNodeList.ExtensionStoreEdge( + getAsCursor(it), + it, + ) + }, + ) + }, + pageInfo = + PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.indexUrl, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.indexUrl, + startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) }, + endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }, + ), + totalCount = queryResults.total.toInt(), + ) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt index 60dbfc151..07f1d7d24 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaData import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader @@ -81,6 +82,7 @@ class TachideskDataLoaderRegistryFactory { ExtensionDataLoader(), ExtensionForSourceDataLoader(), ExtensionForExtensionStore(), + ExtensionStoreDataLoader(), ExtensionStoreForExtension(), TrackerDataLoader(), TrackerStatusesDataLoader(), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionStoreType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionStoreType.kt index 89ec32600..2d1e1c93f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionStoreType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionStoreType.kt @@ -10,7 +10,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.v1.core.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edge import suwayomi.tachidesk.graphql.server.primitives.Node +import suwayomi.tachidesk.graphql.server.primitives.NodeList +import suwayomi.tachidesk.graphql.server.primitives.PageInfo import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable import java.util.concurrent.CompletableFuture @@ -36,3 +40,45 @@ class ExtensionStoreType( fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("ExtensionForExtensionStore", indexUrl) } + +data class ExtensionStoreNodeList( + override val nodes: List, + override val edges: List, + override val pageInfo: PageInfo, + override val totalCount: Int, +) : NodeList() { + data class ExtensionStoreEdge( + override val cursor: Cursor, + override val node: ExtensionStoreType, + ) : Edge() + + companion object { + fun List.toNodeList(): ExtensionStoreNodeList = + ExtensionStoreNodeList( + nodes = this, + edges = getEdges(), + pageInfo = + PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()), + ), + totalCount = size, + ) + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + ExtensionStoreEdge( + cursor = Cursor("0"), + node = first(), + ), + ExtensionStoreEdge( + cursor = Cursor(lastIndex.toString()), + node = last(), + ), + ) + } + } +}