From 21719f4408d73a22cae2f197b078b3af1f08cbd8 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Thu, 10 Nov 2022 22:42:12 +0100 Subject: [PATCH 01/49] Add basic graphql implementation with manga and chapters loading with data loaders --- server/build.gradle.kts | 4 ++ .../suwayomi/tachidesk/graphql/GraphQL.kt | 17 +++++ .../graphql/controller/GraphQLController.kt | 41 +++++++++++ .../graphql/dataLoaders/ChapterDataLoader.kt | 47 +++++++++++++ .../graphql/dataLoaders/MangaDataLoader.kt | 32 +++++++++ .../TachideskDataLoaderRegistryFactory.kt | 16 +++++ .../impl/JavalinGraphQLRequestParser.kt | 30 ++++++++ .../impl/TachideskGraphQLContextFactory.kt | 31 ++++++++ .../graphql/impl/TachideskGraphQLSchema.kt | 45 ++++++++++++ .../graphql/impl/TachideskGraphQLServer.kt | 29 ++++++++ .../tachidesk/graphql/queries/ChapterQuery.kt | 53 ++++++++++++++ .../tachidesk/graphql/queries/MangaQuery.kt | 56 +++++++++++++++ .../tachidesk/graphql/types/ChapterType.kt | 62 ++++++++++++++++ .../tachidesk/graphql/types/MangaType.kt | 70 +++++++++++++++++++ .../suwayomi/tachidesk/server/JavalinSetup.kt | 2 + 15 files changed, 535 insertions(+) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 57f3ffbb..cd386dee 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -64,6 +64,10 @@ dependencies { // implementation(fileTree("lib/")) implementation(kotlin("script-runtime")) + implementation("com.expediagroup", "graphql-kotlin-server", "6.3.0") + implementation("com.expediagroup", "graphql-kotlin-schema-generator", "6.3.0") + implementation("com.graphql-java", "graphql-java-extended-scalars", "19.0") + testImplementation(libs.mockk) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt new file mode 100644 index 00000000..6086ec00 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -0,0 +1,17 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql + +import io.javalin.apibuilder.ApiBuilder.post +import suwayomi.tachidesk.graphql.controller.GraphQLController + +object GraphQL { + fun defineEndpoints() { + post("graphql", GraphQLController.execute) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt new file mode 100644 index 00000000..711e9cb5 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.controller + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.javalin.http.HttpCode +import suwayomi.tachidesk.graphql.impl.getGraphQLServer +import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.withOperation + +object GraphQLController { + private val mapper = jacksonObjectMapper() + private val tachideskGraphQLServer = getGraphQLServer(mapper) + + /** execute graphql query */ + val execute = handler( + documentWith = { + withOperation { + summary("GraphQL endpoint") + description("Endpoint for GraphQL endpoints") + } + }, + + behaviorOf = { ctx -> + ctx.future( + future { + tachideskGraphQLServer.execute(ctx) + } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt new file mode 100644 index 00000000..c3567893 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterType +import suwayomi.tachidesk.manga.model.table.ChapterTable +import java.util.concurrent.CompletableFuture + +class ChapterDataLoader : KotlinDataLoader { + override val dataLoaderName = "ChapterDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + ChapterTable.select { ChapterTable.id inList ids } + .map { ChapterType(it) } + } + } + } +} + +class ChaptersForMangaDataLoader : KotlinDataLoader> { + override val dataLoaderName = "ChaptersForMangaDataLoader" + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } + .map { ChapterType(it) } + .groupBy { it.mangaId } + ids.map { chaptersByMangaId[it] ?: emptyList() } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt new file mode 100644 index 00000000..d4804380 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.manga.model.table.MangaTable +import java.util.concurrent.CompletableFuture + +class MangaDataLoader : KotlinDataLoader { + override val dataLoaderName = "MangaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + MangaTable.select { MangaTable.id inList ids } + .map { MangaType(it) } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt new file mode 100644 index 00000000..27ed965a --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt @@ -0,0 +1,16 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory + +val tachideskDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory( + MangaDataLoader(), + ChapterDataLoader(), + ChaptersForMangaDataLoader() +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt new file mode 100644 index 00000000..ad9b976f --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.impl + +import com.expediagroup.graphql.server.execution.GraphQLRequestParser +import com.expediagroup.graphql.server.types.GraphQLServerRequest +import com.fasterxml.jackson.databind.ObjectMapper +import io.javalin.http.Context +import java.io.IOException + +/** + * Custom logic for how Javalin parses the incoming [Context] into the [GraphQLServerRequest] + */ +class JavalinGraphQLRequestParser( + private val mapper: ObjectMapper +) : GraphQLRequestParser { + + @Suppress("BlockingMethodInNonBlockingContext") + override suspend fun parseRequest(context: Context): GraphQLServerRequest = try { + val rawRequest = context.body() + mapper.readValue(rawRequest, GraphQLServerRequest::class.java) + } catch (e: IOException) { + throw IOException("Unable to parse GraphQL payload.") + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt new file mode 100644 index 00000000..970c6c8f --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.impl + +import com.expediagroup.graphql.generator.execution.GraphQLContext +import com.expediagroup.graphql.server.execution.GraphQLContextFactory +import io.javalin.http.Context + +/** + * Custom logic for how Tachidesk should create its context given the [Context] + */ +class TachideskGraphQLContextFactory : GraphQLContextFactory { + override suspend fun generateContextMap(request: Context): Map<*, Any> = + mutableMapOf( +// "user" to User( +// email = "fake@site.com", +// firstName = "Someone", +// lastName = "You Don't know", +// universityId = 4 +// ) + ).also { map -> +// request.headers["my-custom-header"]?.let { customHeader -> +// map["customHeader"] = customHeader +// } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt new file mode 100644 index 00000000..dc9ed9c5 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.impl + +import com.expediagroup.graphql.generator.SchemaGeneratorConfig +import com.expediagroup.graphql.generator.TopLevelObject +import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks +import com.expediagroup.graphql.generator.scalars.IDValueUnboxer +import com.expediagroup.graphql.generator.toSchema +import graphql.GraphQL +import graphql.scalars.ExtendedScalars +import graphql.schema.GraphQLType +import suwayomi.tachidesk.graphql.queries.ChapterQuery +import suwayomi.tachidesk.graphql.queries.MangaQuery +import kotlin.reflect.KClass +import kotlin.reflect.KType + +class CustomSchemaGeneratorHooks : SchemaGeneratorHooks { + override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { + Long::class -> ExtendedScalars.GraphQLLong + else -> null + } +} + +val schema = toSchema( + config = SchemaGeneratorConfig( + supportedPackages = listOf("suwayomi.tachidesk.graphql"), + introspectionEnabled = true, + hooks = CustomSchemaGeneratorHooks() + ), + queries = listOf( + TopLevelObject(MangaQuery()), + TopLevelObject(ChapterQuery()) + ), + mutations = listOf() +) + +fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) + .valueUnboxer(IDValueUnboxer()) + .build() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt new file mode 100644 index 00000000..dbe2c6c7 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.impl + +import com.expediagroup.graphql.server.execution.GraphQLRequestHandler +import com.expediagroup.graphql.server.execution.GraphQLServer +import com.fasterxml.jackson.databind.ObjectMapper +import io.javalin.http.Context +import suwayomi.tachidesk.graphql.dataLoaders.tachideskDataLoaderRegistryFactory + +class TachideskGraphQLServer( + requestParser: JavalinGraphQLRequestParser, + contextFactory: TachideskGraphQLContextFactory, + requestHandler: GraphQLRequestHandler +) : GraphQLServer(requestParser, contextFactory, requestHandler) + +fun getGraphQLServer(mapper: ObjectMapper): TachideskGraphQLServer { + val requestParser = JavalinGraphQLRequestParser(mapper) + val contextFactory = TachideskGraphQLContextFactory() + val graphQL = getGraphQLObject() + val requestHandler = GraphQLRequestHandler(graphQL, tachideskDataLoaderRegistryFactory) + + return TachideskGraphQLServer(requestParser, contextFactory, requestHandler) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt new file mode 100644 index 00000000..3dbaa076 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterType +import suwayomi.tachidesk.manga.model.table.ChapterTable +import java.util.concurrent.CompletableFuture + +class ChapterQuery { + fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) + } + + data class ChapterQueryInput( + val ids: List? = null, + val mangaIds: List? = null, + val page: Int? = null, + val count: Int? = null + ) + + fun chapters(input: ChapterQueryInput? = null): List { + val results = transaction { + var res = ChapterTable.selectAll() + + if (input != null) { + if (input.mangaIds != null) { + res = res.andWhere { ChapterTable.manga inList input.mangaIds } + } + if (input.ids != null) { + res = res.andWhere { ChapterTable.id inList input.ids } + } + if (input.count != null) { + val offset = if (input.page == null) 0 else (input.page * input.count).toLong() + res = res.limit(input.count, offset) + } + } + + res.toList() + } + + return results.map { ChapterType(it) } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt new file mode 100644 index 00000000..9f19c65e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.manga.model.table.CategoryMangaTable +import suwayomi.tachidesk.manga.model.table.MangaTable +import java.util.concurrent.CompletableFuture + +class MangaQuery { + fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) + } + + data class MangaQueryInput( + val ids: List? = null, + val categoryIds: List? = null, + val page: Int? = null, + val count: Int? = null + ) + + fun mangas(input: MangaQueryInput? = null): List { + val results = transaction { + var res = MangaTable.selectAll() + + if (input != null) { + if (input.categoryIds != null) { + res = MangaTable.innerJoin(CategoryMangaTable) + .select { CategoryMangaTable.category inList input.categoryIds } + } + if (input.ids != null) { + res.andWhere { MangaTable.id inList input.ids } + } + if (input.count != null) { + val offset = if (input.page == null) 0 else (input.page * input.count).toLong() + res.limit(input.count, offset) + } + } + + res.toList() + } + + return results.map { MangaType(it) } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt new file mode 100644 index 00000000..f8536ad9 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.table.ChapterTable +import java.util.concurrent.CompletableFuture + +class ChapterType( + val id: Int, + val url: String, + val name: String, + val uploadDate: Long, + val chapterNumber: Float, + val scanlator: String?, + val mangaId: Int, + val isRead: Boolean, + val isBookmarked: Boolean, + val lastPageRead: Int, + val lastReadAt: Long, + val sourceOrder: Int, + val fetchedAt: Long, + val isDownloaded: Boolean, + val pageCount: Int +// val chapterCount: Int?, +// val meta: Map = emptyMap() +) { + constructor(row: ResultRow) : this( + row[ChapterTable.id].value, + row[ChapterTable.url], + row[ChapterTable.name], + row[ChapterTable.date_upload], + row[ChapterTable.chapter_number], + row[ChapterTable.scanlator], + row[ChapterTable.manga].value, + row[ChapterTable.isRead], + row[ChapterTable.isBookmarked], + row[ChapterTable.lastPageRead], + row[ChapterTable.lastReadAt], + row[ChapterTable.sourceOrder], + row[ChapterTable.fetchedAt], + row[ChapterTable.isDownloaded], + row[ChapterTable.pageCount] +// transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() }, +// Chapter.getChapterMetaMap(chapterEntry[id]) + ) + + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", mangaId) + } + +// fun chapters(): List { +// return listOf("Foo", "Bar", "Baz") +// } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt new file mode 100644 index 00000000..50323872 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.dataclass.toGenreList +import suwayomi.tachidesk.manga.model.table.MangaStatus +import suwayomi.tachidesk.manga.model.table.MangaTable +import java.time.Instant +import java.util.concurrent.CompletableFuture + +class MangaType( + val id: Int, + val sourceId: String, + val url: String, + val title: String, + val thumbnailUrl: String?, + val initialized: Boolean, + val artist: String?, + val author: String?, + val description: String?, + val genre: List, + val status: String, + val inLibrary: Boolean, + val inLibraryAt: Long, + val realUrl: String?, + var lastFetchedAt: Long?, + var chaptersLastFetchedAt: Long? +) { + constructor(row: ResultRow) : this( + row[MangaTable.id].value, + row[MangaTable.sourceReference].toString(), + row[MangaTable.url], + row[MangaTable.title], + row[MangaTable.thumbnail_url], + row[MangaTable.initialized], + row[MangaTable.artist], + row[MangaTable.author], + row[MangaTable.description], + row[MangaTable.genre].toGenreList(), + MangaStatus.valueOf(row[MangaTable.status]).name, + row[MangaTable.inLibrary], + row[MangaTable.inLibraryAt], + row[MangaTable.realUrl], + row[MangaTable.lastFetchedAt], + row[MangaTable.chaptersLastFetchedAt] + ) + + fun chapters(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("ChaptersForMangaDataLoader", id) + } + + fun age(): Long? { + if (lastFetchedAt == null) return null + return Instant.now().epochSecond.minus(lastFetchedAt!!) + } + + fun chaptersAge(): Long? { + if (chaptersLastFetchedAt == null) return null + + return Instant.now().epochSecond.minus(chaptersLastFetchedAt!!) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 7ccce731..cd7b7bb0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -24,6 +24,7 @@ import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance import suwayomi.tachidesk.global.GlobalAPI +import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.setupWebInterface @@ -109,6 +110,7 @@ object JavalinSetup { GlobalAPI.defineEndpoints() MangaAPI.defineEndpoints() } + GraphQL.defineEndpoints() } } From 6054c489c6feda3e2d585ad1de97d51dba0fc126 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Thu, 10 Nov 2022 23:03:18 +0100 Subject: [PATCH 02/49] Add graphql playground --- .../suwayomi/tachidesk/graphql/GraphQL.kt | 5 ++ server/src/main/resources/graphql.html | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 server/src/main/resources/graphql.html diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt index 6086ec00..8bc2edab 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -7,11 +7,16 @@ package suwayomi.tachidesk.graphql +import io.javalin.apibuilder.ApiBuilder.get import io.javalin.apibuilder.ApiBuilder.post import suwayomi.tachidesk.graphql.controller.GraphQLController object GraphQL { fun defineEndpoints() { post("graphql", GraphQLController.execute) + get("graphql") { ctx -> + val html = javaClass.getResource("/graphql.html")?.readText() + ctx.html(html ?: "Could not load playground") + } } } diff --git a/server/src/main/resources/graphql.html b/server/src/main/resources/graphql.html new file mode 100644 index 00000000..23825856 --- /dev/null +++ b/server/src/main/resources/graphql.html @@ -0,0 +1,58 @@ + + + + + + + GraphQL Playground + + + + + + +
+ + +
Loading + GraphQL Playground +
+
+ + + + \ No newline at end of file From 4fb689d9e4ff54143a8685d77513cbe2265c6428 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Thu, 10 Nov 2022 23:47:07 +0100 Subject: [PATCH 03/49] Add chapter and manga meta field --- .../graphql/dataLoaders/MetaDataLoader.kt | 45 +++++++++++++++++++ .../TachideskDataLoaderRegistryFactory.kt | 4 +- .../tachidesk/graphql/types/ChapterType.kt | 8 ++-- .../tachidesk/graphql/types/MangaType.kt | 4 ++ .../tachidesk/graphql/types/MetaType.kt | 23 ++++++++++ 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt new file mode 100644 index 00000000..1ac2b6ac --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -0,0 +1,45 @@ +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterMetaItem +import suwayomi.tachidesk.graphql.types.MangaMetaItem +import suwayomi.tachidesk.graphql.types.MetaType +import suwayomi.tachidesk.manga.model.table.ChapterMetaTable +import suwayomi.tachidesk.manga.model.table.MangaMetaTable +import java.util.concurrent.CompletableFuture + +class ChapterMetaDataLoader : KotlinDataLoader { + override val dataLoaderName = "ChapterMetaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val metasByChapterId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } + .map { ChapterMetaItem(it) } + .groupBy { it.ref } + ids.map { metasByChapterId[it] ?: emptyList() } + } + } + } +} + +class MangaMetaDataLoader : KotlinDataLoader { + override val dataLoaderName = "MangaMetaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val metasByChapterId = MangaMetaTable.select { MangaMetaTable.ref inList ids } + .map { MangaMetaItem(it) } + .groupBy { it.ref } + ids.map { metasByChapterId[it] ?: emptyList() } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt index 27ed965a..a0f5cf38 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt @@ -12,5 +12,7 @@ import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory val tachideskDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory( MangaDataLoader(), ChapterDataLoader(), - ChaptersForMangaDataLoader() + ChaptersForMangaDataLoader(), + ChapterMetaDataLoader(), + MangaMetaDataLoader() ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index f8536ad9..03a1102c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -30,7 +30,6 @@ class ChapterType( val isDownloaded: Boolean, val pageCount: Int // val chapterCount: Int?, -// val meta: Map = emptyMap() ) { constructor(row: ResultRow) : this( row[ChapterTable.id].value, @@ -49,14 +48,13 @@ class ChapterType( row[ChapterTable.isDownloaded], row[ChapterTable.pageCount] // transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() }, -// Chapter.getChapterMetaMap(chapterEntry[id]) ) fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", mangaId) } -// fun chapters(): List { -// return listOf("Foo", "Bar", "Baz") -// } + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ChapterMetaDataLoader", id) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 50323872..b00dadfa 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -67,4 +67,8 @@ class MangaType( return Instant.now().epochSecond.minus(chaptersLastFetchedAt!!) } + + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaMetaDataLoader", id) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt new file mode 100644 index 00000000..d9826d72 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -0,0 +1,23 @@ +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.table.ChapterMetaTable +import suwayomi.tachidesk.manga.model.table.MangaMetaTable + +typealias MetaType = List + +open class MetaItem( + val key: String, + val value: String, + @GraphQLIgnore + val ref: Int +) + +class ChapterMetaItem( + private val row: ResultRow +) : MetaItem(row[ChapterMetaTable.key], row[ChapterMetaTable.value], row[ChapterMetaTable.ref].value) + +class MangaMetaItem( + private val row: ResultRow +) : MetaItem(row[MangaMetaTable.key], row[MangaMetaTable.value], row[MangaMetaTable.ref].value) From 623172af6d2d26ecb67b9ea2ea70a129d6ff1d35 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Fri, 11 Nov 2022 00:53:43 +0100 Subject: [PATCH 04/49] Add mutation for updating chapters --- .../graphql/impl/TachideskGraphQLSchema.kt | 5 +- .../graphql/mutations/ChapterMutation.kt | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt index dc9ed9c5..e4d7763d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt @@ -15,6 +15,7 @@ import com.expediagroup.graphql.generator.toSchema import graphql.GraphQL import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLType +import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import kotlin.reflect.KClass @@ -37,7 +38,9 @@ val schema = toSchema( TopLevelObject(MangaQuery()), TopLevelObject(ChapterQuery()) ), - mutations = listOf() + mutations = listOf( + TopLevelObject(ChapterMutation()) + ) ) fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt new file mode 100644 index 00000000..c84ec775 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -0,0 +1,73 @@ +package suwayomi.tachidesk.graphql.mutations + +import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterType +import suwayomi.tachidesk.manga.model.table.ChapterMetaTable +import suwayomi.tachidesk.manga.model.table.ChapterTable +import java.time.Instant +import java.util.concurrent.CompletableFuture + +class ChapterMutation { + data class MetaTypeInput( + val key: String, + val value: String? + ) + + data class ChapterAttributesInput( + val isBookmarked: Boolean? = null, + val isRead: Boolean? = null, + val lastPageRead: Int? = null, + val meta: List? = null + ) + + data class UpdateChapterInput( + val ids: List, + val attributes: ChapterAttributesInput + ) + + fun updateChapters(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput): CompletableFuture> { + val (ids, attributes) = input + + transaction { + if (attributes.isRead != null || attributes.isBookmarked != null || attributes.lastPageRead != null) { + val now = Instant.now().epochSecond + ChapterTable.update({ ChapterTable.id inList ids }) { update -> + attributes.isRead?.also { + update[isRead] = it + } + attributes.isBookmarked?.also { + update[isBookmarked] = it + } + attributes.lastPageRead?.also { + update[lastPageRead] = it + update[lastReadAt] = now + } + } + } + + if (attributes.meta != null) { + attributes.meta.forEach { metaItem -> + // Delete any existing values + // Even when updating, it is easier to just delete all and create new + ChapterMetaTable.deleteWhere { + (key eq metaItem.key) and (ref inList ids) + } + if (metaItem.value != null) { + ChapterMetaTable.batchInsert(ids) { chapterId -> + this[ChapterMetaTable.ref] = chapterId + this[ChapterMetaTable.key] = metaItem.key + this[ChapterMetaTable.value] = metaItem.value + } + } + } + } + } + + return dataFetchingEnvironment.getValuesFromDataLoader("ChapterDataLoader", ids) + } +} From bf7f1a04b33f5d35080a01e1e8912f254b48345f Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Fri, 11 Nov 2022 01:27:11 +0100 Subject: [PATCH 05/49] Add categories to graphql --- .../graphql/dataLoaders/CategoryDataLoader.kt | 49 +++++++++++++++++++ .../graphql/dataLoaders/MangaDataLoader.kt | 18 +++++++ .../graphql/dataLoaders/MetaDataLoader.kt | 24 +++++++-- .../TachideskDataLoaderRegistryFactory.kt | 5 +- .../graphql/impl/TachideskGraphQLSchema.kt | 4 +- .../graphql/queries/CategoryQuery.kt | 30 ++++++++++++ .../tachidesk/graphql/types/CategoryType.kt | 36 ++++++++++++++ .../tachidesk/graphql/types/MangaType.kt | 4 ++ .../tachidesk/graphql/types/MetaType.kt | 5 ++ 9 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt new file mode 100644 index 00000000..61146aad --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.CategoryType +import suwayomi.tachidesk.manga.model.table.CategoryMangaTable +import suwayomi.tachidesk.manga.model.table.CategoryTable +import java.util.concurrent.CompletableFuture + +class CategoryDataLoader : KotlinDataLoader { + override val dataLoaderName = "CategoryDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + CategoryTable.select { CategoryTable.id inList ids } + .map { CategoryType(it) } + } + } + } +} + +class CategoriesForMangaDataLoader : KotlinDataLoader> { + override val dataLoaderName = "CategoriesForMangaDataLoader" + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga inList ids } + .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } + .groupBy { it.first } + .mapValues { it.value.map { pair -> pair.second } } + ids.map { itemsByRef[it] ?: emptyList() } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index d4804380..b3e9fb8d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -10,11 +10,13 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable import java.util.concurrent.CompletableFuture @@ -30,3 +32,19 @@ class MangaDataLoader : KotlinDataLoader { } } } + +class MangaForCategoryDataLoader : KotlinDataLoader> { + override val dataLoaderName = "MangaForCategoryDataLoader" + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val itemsByRef = CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category inList ids } + .map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) } + .groupBy { it.first } + .mapValues { it.value.map { pair -> pair.second } } + ids.map { itemsByRef[it] ?: emptyList() } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 1ac2b6ac..57ea723f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -7,6 +7,7 @@ import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.CategoryMetaItem import suwayomi.tachidesk.graphql.types.ChapterMetaItem import suwayomi.tachidesk.graphql.types.MangaMetaItem import suwayomi.tachidesk.graphql.types.MetaType @@ -20,10 +21,10 @@ class ChapterMetaDataLoader : KotlinDataLoader { CompletableFuture.supplyAsync { transaction { addLogger(StdOutSqlLogger) - val metasByChapterId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } + val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } .map { ChapterMetaItem(it) } .groupBy { it.ref } - ids.map { metasByChapterId[it] ?: emptyList() } + ids.map { metasByRefId[it] ?: emptyList() } } } } @@ -35,10 +36,25 @@ class MangaMetaDataLoader : KotlinDataLoader { CompletableFuture.supplyAsync { transaction { addLogger(StdOutSqlLogger) - val metasByChapterId = MangaMetaTable.select { MangaMetaTable.ref inList ids } + val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } .map { MangaMetaItem(it) } .groupBy { it.ref } - ids.map { metasByChapterId[it] ?: emptyList() } + ids.map { metasByRefId[it] ?: emptyList() } + } + } + } +} + +class CategoryMetaDataLoader : KotlinDataLoader { + override val dataLoaderName = "CategoryMetaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { + transaction { + addLogger(StdOutSqlLogger) + val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } + .map { CategoryMetaItem(it) } + .groupBy { it.ref } + ids.map { metasByRefId[it] ?: emptyList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt index a0f5cf38..a035f03d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt @@ -14,5 +14,8 @@ val tachideskDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory( ChapterDataLoader(), ChaptersForMangaDataLoader(), ChapterMetaDataLoader(), - MangaMetaDataLoader() + MangaMetaDataLoader(), + MangaForCategoryDataLoader(), + CategoryMetaDataLoader(), + CategoriesForMangaDataLoader() ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt index e4d7763d..257426c2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt @@ -16,6 +16,7 @@ import graphql.GraphQL import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation +import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import kotlin.reflect.KClass @@ -36,7 +37,8 @@ val schema = toSchema( ), queries = listOf( TopLevelObject(MangaQuery()), - TopLevelObject(ChapterQuery()) + TopLevelObject(ChapterQuery()), + TopLevelObject(CategoryQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt new file mode 100644 index 00000000..15f73722 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.CategoryType +import suwayomi.tachidesk.manga.model.table.CategoryTable +import java.util.concurrent.CompletableFuture + +class CategoryQuery { + fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) + } + + fun categories(): List { + val results = transaction { + CategoryTable.selectAll().toList() + } + + return results.map { CategoryType(it) } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt new file mode 100644 index 00000000..ada7129e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.table.CategoryTable +import java.util.concurrent.CompletableFuture + +class CategoryType( + val id: Int, + val order: Int, + val name: String, + val default: Boolean +) { + constructor(row: ResultRow) : this( + row[CategoryTable.id].value, + row[CategoryTable.order], + row[CategoryTable.name], + row[CategoryTable.isDefault] + ) + + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("MangaForCategoryDataLoader", id) + } + + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("CategoryMetaDataLoader", id) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index b00dadfa..40cd2222 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -71,4 +71,8 @@ class MangaType( fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("MangaMetaDataLoader", id) } + + fun categories(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("CategoriesForMangaDataLoader", id) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index d9826d72..a8c19240 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -2,6 +2,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable @@ -21,3 +22,7 @@ class ChapterMetaItem( class MangaMetaItem( private val row: ResultRow ) : MetaItem(row[MangaMetaTable.key], row[MangaMetaTable.value], row[MangaMetaTable.ref].value) + +class CategoryMetaItem( + private val row: ResultRow +) : MetaItem(row[CategoryMetaTable.key], row[CategoryMetaTable.value], row[CategoryMetaTable.ref].value) From 0c555e88d379b14161727237a09f4840a46bf1a2 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Fri, 11 Nov 2022 11:31:38 +0100 Subject: [PATCH 06/49] Update graphql-playground endpoint --- .../kotlin/suwayomi/tachidesk/graphql/GraphQL.kt | 10 ++++------ .../graphql/controller/GraphQLController.kt | 16 ++++++++++++++++ .../{graphql.html => graphql-playground.html} | 6 ++++-- 3 files changed, 24 insertions(+), 8 deletions(-) rename server/src/main/resources/{graphql.html => graphql-playground.html} (90%) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt index 8bc2edab..82093452 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -7,16 +7,14 @@ package suwayomi.tachidesk.graphql -import io.javalin.apibuilder.ApiBuilder.get -import io.javalin.apibuilder.ApiBuilder.post +import io.javalin.apibuilder.ApiBuilder.* import suwayomi.tachidesk.graphql.controller.GraphQLController object GraphQL { fun defineEndpoints() { post("graphql", GraphQLController.execute) - get("graphql") { ctx -> - val html = javaClass.getResource("/graphql.html")?.readText() - ctx.html(html ?: "Could not load playground") - } + + // graphql playground + get("graphql", GraphQLController::playground) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt index 711e9cb5..615db95c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.graphql.controller import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.javalin.http.Context import io.javalin.http.HttpCode import suwayomi.tachidesk.graphql.impl.getGraphQLServer import suwayomi.tachidesk.server.JavalinSetup.future @@ -38,4 +39,19 @@ object GraphQLController { json(HttpCode.OK) } ) + + fun playground(ctx: Context) { + val playgroundHtml = javaClass.getResource("/graphql-playground.html") + + val body = playgroundHtml.openStream().bufferedReader().use { reader -> + val graphQLEndpoint = "graphql" + val subscriptionsEndpoint = "graphql" + + reader.readText() + .replace("\${graphQLEndpoint}", graphQLEndpoint) + .replace("\${subscriptionsEndpoint}", subscriptionsEndpoint) + } + + ctx.html(body ?: "Could not load playground") + } } diff --git a/server/src/main/resources/graphql.html b/server/src/main/resources/graphql-playground.html similarity index 90% rename from server/src/main/resources/graphql.html rename to server/src/main/resources/graphql-playground.html index 23825856..894ff66f 100644 --- a/server/src/main/resources/graphql.html +++ b/server/src/main/resources/graphql-playground.html @@ -50,9 +50,11 @@ - \ No newline at end of file + From e2fa0032391191a2ed8536213f989bf9acc5824e Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Fri, 11 Nov 2022 16:12:24 +0100 Subject: [PATCH 07/49] Rewrite graphql controller execute as function without docs --- .../suwayomi/tachidesk/graphql/GraphQL.kt | 2 +- .../graphql/controller/GraphQLController.kt | 27 +++++-------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt index 82093452..46600e72 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -12,7 +12,7 @@ import suwayomi.tachidesk.graphql.controller.GraphQLController object GraphQL { fun defineEndpoints() { - post("graphql", GraphQLController.execute) + post("graphql", GraphQLController::execute) // graphql playground get("graphql", GraphQLController::playground) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt index 615db95c..5dd3812a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt @@ -9,36 +9,21 @@ package suwayomi.tachidesk.graphql.controller import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.javalin.http.Context -import io.javalin.http.HttpCode import suwayomi.tachidesk.graphql.impl.getGraphQLServer import suwayomi.tachidesk.server.JavalinSetup.future -import suwayomi.tachidesk.server.util.handler -import suwayomi.tachidesk.server.util.withOperation object GraphQLController { private val mapper = jacksonObjectMapper() private val tachideskGraphQLServer = getGraphQLServer(mapper) /** execute graphql query */ - val execute = handler( - documentWith = { - withOperation { - summary("GraphQL endpoint") - description("Endpoint for GraphQL endpoints") + fun execute(ctx: Context) { + ctx.future( + future { + tachideskGraphQLServer.execute(ctx) } - }, - - behaviorOf = { ctx -> - ctx.future( - future { - tachideskGraphQLServer.execute(ctx) - } - ) - }, - withResults = { - json(HttpCode.OK) - } - ) + ) + } fun playground(ctx: Context) { val playgroundHtml = javaClass.getResource("/graphql-playground.html") From 847a5fe71b088cf1a355653471ccd82a0ff12168 Mon Sep 17 00:00:00 2001 From: Valter Martinek Date: Fri, 11 Nov 2022 22:56:14 +0100 Subject: [PATCH 08/49] Subscriptions! --- server/build.gradle.kts | 1 + .../suwayomi/tachidesk/graphql/GraphQL.kt | 1 + .../graphql/controller/GraphQLController.kt | 18 +- .../TachideskDataLoaderRegistryFactory.kt | 21 -- .../graphql/impl/TachideskGraphQLServer.kt | 29 --- .../JavalinGraphQLRequestParser.kt | 13 +- .../TachideskDataLoaderRegistryFactory.kt | 28 +++ .../TachideskGraphQLContextFactory.kt | 20 +- .../TachideskGraphQLSchema.kt | 12 +- .../graphql/server/TachideskGraphQLServer.kt | 57 +++++ .../ApolloSubscriptionProtocolHandler.kt | 194 ++++++++++++++++++ .../ApolloSubscriptionSessionState.kt | 125 +++++++++++ .../subscriptions/FluxSubscriptionSource.kt | 20 ++ .../GraphQLSubscriptionHandler.kt | 43 ++++ .../SubscriptionOperationMessage.kt | 39 ++++ .../subscriptions/DownloadSubscription.kt | 24 +++ .../tachidesk/graphql/types/ChapterType.kt | 19 ++ .../tachidesk/graphql/types/DownloadType.kt | 46 +++++ .../tachidesk/graphql/types/MangaType.kt | 20 ++ .../manga/impl/download/DownloadManager.kt | 5 + 20 files changed, 658 insertions(+), 77 deletions(-) delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt rename server/src/main/kotlin/suwayomi/tachidesk/graphql/{impl => server}/JavalinGraphQLRequestParser.kt (63%) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt rename server/src/main/kotlin/suwayomi/tachidesk/graphql/{impl => server}/TachideskGraphQLContextFactory.kt (69%) rename server/src/main/kotlin/suwayomi/tachidesk/graphql/{impl => server}/TachideskGraphQLSchema.kt (86%) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/SubscriptionOperationMessage.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt diff --git a/server/build.gradle.kts b/server/build.gradle.kts index cd386dee..cbca5f9f 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation("com.expediagroup", "graphql-kotlin-server", "6.3.0") implementation("com.expediagroup", "graphql-kotlin-schema-generator", "6.3.0") implementation("com.graphql-java", "graphql-java-extended-scalars", "19.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.0-RC-native-mt") testImplementation(libs.mockk) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt index 46600e72..8e0225e3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -13,6 +13,7 @@ import suwayomi.tachidesk.graphql.controller.GraphQLController object GraphQL { fun defineEndpoints() { post("graphql", GraphQLController::execute) + ws("graphql", GraphQLController::webSocket) // graphql playground get("graphql", GraphQLController::playground) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt index 5dd3812a..433b64d6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt @@ -7,20 +7,19 @@ package suwayomi.tachidesk.graphql.controller -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.javalin.http.Context -import suwayomi.tachidesk.graphql.impl.getGraphQLServer +import io.javalin.websocket.WsConfig +import suwayomi.tachidesk.graphql.server.TachideskGraphQLServer import suwayomi.tachidesk.server.JavalinSetup.future object GraphQLController { - private val mapper = jacksonObjectMapper() - private val tachideskGraphQLServer = getGraphQLServer(mapper) + private val server = TachideskGraphQLServer.create() /** execute graphql query */ fun execute(ctx: Context) { ctx.future( future { - tachideskGraphQLServer.execute(ctx) + server.execute(ctx) } ) } @@ -39,4 +38,13 @@ object GraphQLController { ctx.html(body ?: "Could not load playground") } + + fun webSocket(ws: WsConfig) { + ws.onMessage { ctx -> + server.handleSubscriptionMessage(ctx) + } + ws.onClose { ctx -> + server.handleSubscriptionDisconnect(ctx) + } + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt deleted file mode 100644 index a035f03d..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/TachideskDataLoaderRegistryFactory.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -package suwayomi.tachidesk.graphql.dataLoaders - -import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory - -val tachideskDataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory( - MangaDataLoader(), - ChapterDataLoader(), - ChaptersForMangaDataLoader(), - ChapterMetaDataLoader(), - MangaMetaDataLoader(), - MangaForCategoryDataLoader(), - CategoryMetaDataLoader(), - CategoriesForMangaDataLoader() -) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt deleted file mode 100644 index dbe2c6c7..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLServer.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -package suwayomi.tachidesk.graphql.impl - -import com.expediagroup.graphql.server.execution.GraphQLRequestHandler -import com.expediagroup.graphql.server.execution.GraphQLServer -import com.fasterxml.jackson.databind.ObjectMapper -import io.javalin.http.Context -import suwayomi.tachidesk.graphql.dataLoaders.tachideskDataLoaderRegistryFactory - -class TachideskGraphQLServer( - requestParser: JavalinGraphQLRequestParser, - contextFactory: TachideskGraphQLContextFactory, - requestHandler: GraphQLRequestHandler -) : GraphQLServer(requestParser, contextFactory, requestHandler) - -fun getGraphQLServer(mapper: ObjectMapper): TachideskGraphQLServer { - val requestParser = JavalinGraphQLRequestParser(mapper) - val contextFactory = TachideskGraphQLContextFactory() - val graphQL = getGraphQLObject() - val requestHandler = GraphQLRequestHandler(graphQL, tachideskDataLoaderRegistryFactory) - - return TachideskGraphQLServer(requestParser, contextFactory, requestHandler) -} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt similarity index 63% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt index ad9b976f..7dc243b4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/JavalinGraphQLRequestParser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt @@ -5,25 +5,18 @@ * 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/. */ -package suwayomi.tachidesk.graphql.impl +package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.server.execution.GraphQLRequestParser import com.expediagroup.graphql.server.types.GraphQLServerRequest -import com.fasterxml.jackson.databind.ObjectMapper import io.javalin.http.Context import java.io.IOException -/** - * Custom logic for how Javalin parses the incoming [Context] into the [GraphQLServerRequest] - */ -class JavalinGraphQLRequestParser( - private val mapper: ObjectMapper -) : GraphQLRequestParser { +class JavalinGraphQLRequestParser : GraphQLRequestParser { @Suppress("BlockingMethodInNonBlockingContext") override suspend fun parseRequest(context: Context): GraphQLServerRequest = try { - val rawRequest = context.body() - mapper.readValue(rawRequest, GraphQLServerRequest::class.java) + context.bodyAsClass(GraphQLServerRequest::class.java) } catch (e: IOException) { throw IOException("Unable to parse GraphQL payload.") } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt new file mode 100644 index 00000000..1186a1ce --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server + +import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory +import suwayomi.tachidesk.graphql.dataLoaders.* + +class TachideskDataLoaderRegistryFactory { + companion object { + fun create(): KotlinDataLoaderRegistryFactory { + return KotlinDataLoaderRegistryFactory( + MangaDataLoader(), + ChapterDataLoader(), + ChaptersForMangaDataLoader(), + ChapterMetaDataLoader(), + MangaMetaDataLoader(), + MangaForCategoryDataLoader(), + CategoryMetaDataLoader(), + CategoriesForMangaDataLoader() + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt similarity index 69% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt index 970c6c8f..34147ae8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLContextFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt @@ -5,27 +5,37 @@ * 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/. */ -package suwayomi.tachidesk.graphql.impl +package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.generator.execution.GraphQLContext import com.expediagroup.graphql.server.execution.GraphQLContextFactory import io.javalin.http.Context +import io.javalin.websocket.WsContext /** * Custom logic for how Tachidesk should create its context given the [Context] */ class TachideskGraphQLContextFactory : GraphQLContextFactory { - override suspend fun generateContextMap(request: Context): Map<*, Any> = - mutableMapOf( + override suspend fun generateContextMap(request: Context): Map<*, Any> = emptyMap() +// mutableMapOf( // "user" to User( // email = "fake@site.com", // firstName = "Someone", // lastName = "You Don't know", // universityId = 4 // ) - ).also { map -> +// ).also { map -> // request.headers["my-custom-header"]?.let { customHeader -> // map["customHeader"] = customHeader // } - } +// } + + fun generateContextMap(request: WsContext): Map<*, Any> = emptyMap() } + +/** + * Create a [GraphQLContext] from [this] map + * @return a new [GraphQLContext] + */ +fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = + graphql.GraphQLContext.of(this) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt similarity index 86% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 257426c2..e5de168b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/impl/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -5,20 +5,19 @@ * 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/. */ -package suwayomi.tachidesk.graphql.impl +package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks -import com.expediagroup.graphql.generator.scalars.IDValueUnboxer import com.expediagroup.graphql.generator.toSchema -import graphql.GraphQL import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.MangaQuery +import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass import kotlin.reflect.KType @@ -42,9 +41,8 @@ val schema = toSchema( ), mutations = listOf( TopLevelObject(ChapterMutation()) + ), + subscriptions = listOf( + TopLevelObject(DownloadSubscription()) ) ) - -fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) - .valueUnboxer(IDValueUnboxer()) - .build() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt new file mode 100644 index 00000000..65f6a170 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server + +import com.expediagroup.graphql.generator.execution.FlowSubscriptionExecutionStrategy +import com.expediagroup.graphql.server.execution.GraphQLRequestHandler +import com.expediagroup.graphql.server.execution.GraphQLServer +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import graphql.GraphQL +import io.javalin.http.Context +import io.javalin.websocket.WsCloseContext +import io.javalin.websocket.WsMessageContext +import suwayomi.tachidesk.graphql.server.subscriptions.ApolloSubscriptionProtocolHandler +import suwayomi.tachidesk.graphql.server.subscriptions.GraphQLSubscriptionHandler + +class TachideskGraphQLServer( + requestParser: JavalinGraphQLRequestParser, + contextFactory: TachideskGraphQLContextFactory, + requestHandler: GraphQLRequestHandler, + subscriptionHandler: GraphQLSubscriptionHandler +) : GraphQLServer(requestParser, contextFactory, requestHandler) { + private val objectMapper = jacksonObjectMapper() + private val subscriptionProtocolHandler = ApolloSubscriptionProtocolHandler(contextFactory, subscriptionHandler, objectMapper) + + fun handleSubscriptionMessage(context: WsMessageContext) { + subscriptionProtocolHandler.handleMessage(context) + .map { objectMapper.writeValueAsString(it) } + .map { context.send(it) } + .subscribe() + } + + fun handleSubscriptionDisconnect(context: WsCloseContext) { + subscriptionProtocolHandler.handleDisconnect(context) + } + + companion object { + private fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) + .subscriptionExecutionStrategy(FlowSubscriptionExecutionStrategy()) + .build() + + fun create(): TachideskGraphQLServer { + val graphQL = getGraphQLObject() + + val requestParser = JavalinGraphQLRequestParser() + val contextFactory = TachideskGraphQLContextFactory() + val requestHandler = GraphQLRequestHandler(graphQL, TachideskDataLoaderRegistryFactory.create()) + val subscriptionHandler = GraphQLSubscriptionHandler(graphQL, TachideskDataLoaderRegistryFactory.create()) + + return TachideskGraphQLServer(requestParser, contextFactory, requestHandler, subscriptionHandler) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt new file mode 100644 index 00000000..53df508e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server.subscriptions + +import com.expediagroup.graphql.server.types.GraphQLRequest +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.convertValue +import com.fasterxml.jackson.module.kotlin.readValue +import io.javalin.websocket.WsContext +import io.javalin.websocket.WsMessageContext +import kotlinx.coroutines.reactor.asFlux +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.publisher.toFlux +import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.* +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.* +import suwayomi.tachidesk.graphql.server.toGraphQLContext +import java.time.Duration + +/** + * Implementation of the `graphql-ws` protocol defined by Apollo + * https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md + * ported for Javalin + */ +class ApolloSubscriptionProtocolHandler( + private val contextFactory: TachideskGraphQLContextFactory, + private val subscriptionHandler: GraphQLSubscriptionHandler, + private val objectMapper: ObjectMapper +) { + private val sessionState = ApolloSubscriptionSessionState() + private val logger = LoggerFactory.getLogger(ApolloSubscriptionProtocolHandler::class.java) + private val keepAliveMessage = SubscriptionOperationMessage(type = GQL_CONNECTION_KEEP_ALIVE.type) + private val basicConnectionErrorMessage = SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type) + private val acknowledgeMessage = SubscriptionOperationMessage(GQL_CONNECTION_ACK.type) + + @Suppress("Detekt.TooGenericExceptionCaught") + fun handleMessage(context: WsMessageContext): Flux { + val operationMessage = convertToMessageOrNull(context.message()) ?: return Flux.just(basicConnectionErrorMessage) + logger.debug("GraphQL subscription client message, sessionId=${context.sessionId} operationMessage=$operationMessage") + + return try { + when (operationMessage.type) { + GQL_CONNECTION_INIT.type -> onInit(operationMessage, context) + GQL_START.type -> startSubscription(operationMessage, context) + GQL_STOP.type -> onStop(operationMessage, context) + GQL_CONNECTION_TERMINATE.type -> onDisconnect(context) + else -> onUnknownOperation(operationMessage, context) + } + } catch (exception: Exception) { + onException(exception) + } + } + + fun handleDisconnect(context: WsContext) { + onDisconnect(context) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun convertToMessageOrNull(payload: String): SubscriptionOperationMessage? { + return try { + objectMapper.readValue(payload) + } catch (exception: Exception) { + logger.error("Error parsing the subscription message", exception) + null + } + } + + /** + * If the keep alive configuration is set, send a message back to client at every interval until the session is terminated. + * Otherwise just return empty flux to append to the acknowledge message. + */ + private fun getKeepAliveFlux(context: WsContext): Flux { + val keepAliveInterval: Long? = 2000 + if (keepAliveInterval != null) { + return Flux.interval(Duration.ofMillis(keepAliveInterval)) + .map { keepAliveMessage } + .doOnSubscribe { sessionState.saveKeepAliveSubscription(context, it) } + } + + return Flux.empty() + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun startSubscription( + operationMessage: SubscriptionOperationMessage, + context: WsContext + ): Flux { + val graphQLContext = sessionState.getGraphQLContext(context) + + if (operationMessage.id == null) { + logger.error("GraphQL subscription operation id is required") + return Flux.just(basicConnectionErrorMessage) + } + + if (sessionState.doesOperationExist(context, operationMessage)) { + logger.info("Already subscribed to operation ${operationMessage.id} for session ${context.sessionId}") + return Flux.empty() + } + + val payload = operationMessage.payload + + if (payload == null) { + logger.error("GraphQL subscription payload was null instead of a GraphQLRequest object") + sessionState.stopOperation(context, operationMessage) + return Flux.just(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) + } + + try { + val request = objectMapper.convertValue(payload) + return subscriptionHandler.executeSubscription(request, graphQLContext) + .asFlux() + .map { + if (it.errors?.isNotEmpty() == true) { + SubscriptionOperationMessage(type = GQL_ERROR.type, id = operationMessage.id, payload = it) + } else { + SubscriptionOperationMessage(type = GQL_DATA.type, id = operationMessage.id, payload = it) + } + } + .concatWith(onComplete(operationMessage, context).toFlux()) + .doOnSubscribe { sessionState.saveOperation(context, operationMessage, it) } + } catch (exception: Exception) { + logger.error("Error running graphql subscription", exception) + // Do not terminate the session, just stop the operation messages + sessionState.stopOperation(context, operationMessage) + return Flux.just(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) + } + } + + private fun onInit(operationMessage: SubscriptionOperationMessage, context: WsContext): Flux { + saveContext(operationMessage, context) + val acknowledgeMessage = Mono.just(acknowledgeMessage) + val keepAliveFlux = getKeepAliveFlux(context) + return acknowledgeMessage.concatWith(keepAliveFlux) + .onErrorReturn(getConnectionErrorMessage(operationMessage)) + } + + /** + * Generate the context and save it for all future messages. + */ + private fun saveContext(operationMessage: SubscriptionOperationMessage, context: WsContext) { + runBlocking { + val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext() + sessionState.saveContext(context, graphQLContext) + } + } + + /** + * Called with the publisher has completed on its own. + */ + private fun onComplete( + operationMessage: SubscriptionOperationMessage, + context: WsContext + ): Mono { + return sessionState.completeOperation(context, operationMessage) + } + + /** + * Called with the client has called stop manually, or on error, and we need to cancel the publisher + */ + private fun onStop( + operationMessage: SubscriptionOperationMessage, + context: WsContext + ): Flux { + return sessionState.stopOperation(context, operationMessage).toFlux() + } + + private fun onDisconnect(context: WsContext): Flux { + sessionState.terminateSession(context) + return Flux.empty() + } + + private fun onUnknownOperation(operationMessage: SubscriptionOperationMessage, context: WsContext): Flux { + logger.error("Unknown subscription operation $operationMessage") + sessionState.stopOperation(context, operationMessage) + return Flux.just(getConnectionErrorMessage(operationMessage)) + } + + private fun onException(exception: Exception): Flux { + logger.error("Error parsing the subscription message", exception) + return Flux.just(basicConnectionErrorMessage) + } + + private fun getConnectionErrorMessage(operationMessage: SubscriptionOperationMessage): SubscriptionOperationMessage { + return SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt new file mode 100644 index 00000000..7e2358ed --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server.subscriptions + +import graphql.GraphQLContext +import io.javalin.websocket.WsContext +import org.reactivestreams.Subscription +import reactor.core.publisher.Mono +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_COMPLETE +import suwayomi.tachidesk.graphql.server.toGraphQLContext +import java.util.concurrent.ConcurrentHashMap + +internal class ApolloSubscriptionSessionState { + + // Sessions are saved by web socket session id + internal val activeKeepAliveSessions = ConcurrentHashMap() + + // Operations are saved by web socket session id, then operation id + internal val activeOperations = ConcurrentHashMap>() + + // The graphQL context is saved by web socket session id + private val cachedGraphQLContext = ConcurrentHashMap() + + /** + * Save the context created from the factory and possibly updated in the onConnect hook. + * This allows us to include some initial state to be used when handling all the messages. + * This will be removed in [terminateSession]. + */ + fun saveContext(context: WsContext, graphQLContext: GraphQLContext) { + cachedGraphQLContext[context.sessionId] = graphQLContext + } + + /** + * Return the graphQL context for this session. + */ + fun getGraphQLContext(context: WsContext): GraphQLContext = cachedGraphQLContext[context.sessionId] ?: emptyMap().toGraphQLContext() + + /** + * Save the session that is sending keep alive messages. + * This will override values without cancelling the subscription, so it is the responsibility of the consumer to cancel. + * These messages will be stopped on [terminateSession]. + */ + fun saveKeepAliveSubscription(context: WsContext, subscription: Subscription) { + activeKeepAliveSessions[context.sessionId] = subscription + } + + /** + * Save the operation that is sending data to the client. + * This will override values without cancelling the subscription so it is the responsibility of the consumer to cancel. + * These messages will be stopped on [stopOperation]. + */ + fun saveOperation(context: WsContext, operationMessage: SubscriptionOperationMessage, subscription: Subscription) { + val id = operationMessage.id + if (id != null) { + val operationsForSession: ConcurrentHashMap = activeOperations.getOrPut(context.sessionId) { ConcurrentHashMap() } + operationsForSession[id] = subscription + } + } + + /** + * Send the [GQL_COMPLETE] message. + * This can happen when the publisher finishes or if the client manually sends the stop message. + */ + fun completeOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Mono { + return getCompleteMessage(operationMessage) + .doFinally { removeActiveOperation(context, operationMessage.id, cancelSubscription = false) } + } + + /** + * Stop the subscription sending data and send the [GQL_COMPLETE] message. + * Does NOT terminate the session. + */ + fun stopOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Mono { + return getCompleteMessage(operationMessage) + .doFinally { removeActiveOperation(context, operationMessage.id, cancelSubscription = true) } + } + + private fun getCompleteMessage(operationMessage: SubscriptionOperationMessage): Mono { + val id = operationMessage.id + if (id != null) { + return Mono.just(SubscriptionOperationMessage(type = GQL_COMPLETE.type, id = id)) + } + return Mono.empty() + } + + /** + * Remove active running subscription from the cache and cancel if needed + */ + private fun removeActiveOperation(context: WsContext, id: String?, cancelSubscription: Boolean) { + val operationsForSession = activeOperations[context.sessionId] + val subscription = operationsForSession?.get(id) + if (subscription != null) { + if (cancelSubscription) { + subscription.cancel() + } + operationsForSession.remove(id) + if (operationsForSession.isEmpty()) { + activeOperations.remove(context.sessionId) + } + } + } + + /** + * Terminate the session, cancelling the keep alive messages and all operations active for this session. + */ + fun terminateSession(context: WsContext) { + activeOperations[context.sessionId]?.forEach { (_, subscription) -> subscription.cancel() } + activeOperations.remove(context.sessionId) + cachedGraphQLContext.remove(context.sessionId) + activeKeepAliveSessions[context.sessionId]?.cancel() + activeKeepAliveSessions.remove(context.sessionId) + context.closeSession() + } + + /** + * Looks up the operation for the client, to check if it already exists + */ + fun doesOperationExist(context: WsContext, operationMessage: SubscriptionOperationMessage): Boolean = + activeOperations[context.sessionId]?.containsKey(operationMessage.id) ?: false +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt new file mode 100644 index 00000000..ab4c1675 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server.subscriptions + +import reactor.core.publisher.Flux +import reactor.core.publisher.FluxSink + +class FluxSubscriptionSource() { + private var sink: FluxSink? = null + val emitter: Flux = Flux.create { emitter -> sink = emitter } + + fun publish(value: T) { + sink?.next(value) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt new file mode 100644 index 00000000..a402e642 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server.subscriptions + +import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory +import com.expediagroup.graphql.server.extensions.toExecutionInput +import com.expediagroup.graphql.server.extensions.toGraphQLError +import com.expediagroup.graphql.server.extensions.toGraphQLKotlinType +import com.expediagroup.graphql.server.extensions.toGraphQLResponse +import com.expediagroup.graphql.server.types.GraphQLRequest +import com.expediagroup.graphql.server.types.GraphQLResponse +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.GraphQLContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +open class GraphQLSubscriptionHandler( + private val graphQL: GraphQL, + private val dataLoaderRegistryFactory: KotlinDataLoaderRegistryFactory? = null +) { + open fun executeSubscription( + graphQLRequest: GraphQLRequest, + graphQLContext: GraphQLContext = GraphQLContext.of(emptyMap()) + ): Flow> { + val dataLoaderRegistry = dataLoaderRegistryFactory?.generate() + val input = graphQLRequest.toExecutionInput(dataLoaderRegistry, graphQLContext) + + val res = graphQL.execute(input) + val data = res.getData>() + val mapped = data.map { result -> result.toGraphQLResponse() } + return mapped.catch { throwable -> + val error = throwable.toGraphQLError() + emit(GraphQLResponse(errors = listOf(error.toGraphQLKotlinType()))) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/SubscriptionOperationMessage.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/SubscriptionOperationMessage.kt new file mode 100644 index 00000000..c118eb7c --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/SubscriptionOperationMessage.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.server.subscriptions + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +/** + * The `graphql-ws` protocol from Apollo Client has some special text messages to signal events. + * Along with the HTTP WebSocket event handling we need to have some extra logic + * + * https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class SubscriptionOperationMessage( + val type: String, + val id: String? = null, + val payload: Any? = null +) { + enum class ClientMessages(val type: String) { + GQL_CONNECTION_INIT("connection_init"), + GQL_START("start"), + GQL_STOP("stop"), + GQL_CONNECTION_TERMINATE("connection_terminate") + } + + enum class ServerMessages(val type: String) { + GQL_CONNECTION_ACK("connection_ack"), + GQL_CONNECTION_ERROR("connection_error"), + GQL_DATA("data"), + GQL_ERROR("error"), + GQL_COMPLETE("complete"), + GQL_CONNECTION_KEEP_ALIVE("ka") + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt new file mode 100644 index 00000000..6f1db4f6 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.subscriptions + +import graphql.schema.DataFetchingEnvironment +import reactor.core.publisher.Flux +import suwayomi.tachidesk.graphql.server.subscriptions.FluxSubscriptionSource +import suwayomi.tachidesk.graphql.types.DownloadType +import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter + +val downloadSubscriptionSource = FluxSubscriptionSource() + +class DownloadSubscription { + fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flux { + return downloadSubscriptionSource.emitter.map { downloadChapter -> + DownloadType(downloadChapter) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 03a1102c..1d8f0e2c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -10,6 +10,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import java.util.concurrent.CompletableFuture @@ -50,6 +51,24 @@ class ChapterType( // transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() }, ) + constructor(dataClass: ChapterDataClass) : this( + dataClass.id, + dataClass.url, + dataClass.name, + dataClass.uploadDate, + dataClass.chapterNumber, + dataClass.scanlator, + dataClass.mangaId, + dataClass.read, + dataClass.bookmarked, + dataClass.lastPageRead, + dataClass.lastReadAt, + dataClass.index, + dataClass.fetchedAt, + dataClass.downloaded, + dataClass.pageCount + ) + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", mangaId) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt new file mode 100644 index 00000000..9e58d813 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter +import suwayomi.tachidesk.manga.impl.download.model.DownloadState +import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass +import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass + +class DownloadType( + val chapterId: Int, + val chapterIndex: Int, + val mangaId: Int, + var state: DownloadState = DownloadState.Queued, + var progress: Float = 0f, + var tries: Int = 0, + @GraphQLIgnore + var mangaDataClass: MangaDataClass, + @GraphQLIgnore + var chapterDataClass: ChapterDataClass +) { + constructor(downloadChapter: DownloadChapter) : this( + downloadChapter.chapter.id, + downloadChapter.chapterIndex, + downloadChapter.mangaId, + downloadChapter.state, + downloadChapter.progress, + downloadChapter.tries, + downloadChapter.manga, + downloadChapter.chapter + ) + + fun manga(): MangaType { + return MangaType(mangaDataClass) + } + + fun chapter(): ChapterType { + return ChapterType(chapterDataClass) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 40cd2222..1c1030ce 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -10,6 +10,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable @@ -53,6 +54,25 @@ class MangaType( row[MangaTable.chaptersLastFetchedAt] ) + constructor(dataClass: MangaDataClass) : this( + dataClass.id, + dataClass.sourceId, + dataClass.url, + dataClass.title, + dataClass.thumbnailUrl, + dataClass.initialized, + dataClass.artist, + dataClass.author, + dataClass.description, + dataClass.genre, + dataClass.status, + dataClass.inLibrary, + dataClass.inLibraryAt, + dataClass.realUrl, + dataClass.lastFetchedAt, + dataClass.chaptersLastFetchedAt + ) + fun chapters(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { return dataFetchingEnvironment.getValueFromDataLoader>("ChaptersForMangaDataLoader", id) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt index 7ddd40d5..fe9a13a5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt @@ -24,6 +24,7 @@ import mu.KotlinLogging import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.subscriptions.downloadSubscriptionSource import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter import suwayomi.tachidesk.manga.impl.download.model.DownloadState.Downloading import suwayomi.tachidesk.manga.impl.download.model.DownloadStatus @@ -100,6 +101,9 @@ object DownloadManager { notifyFlow.emit(Unit) } } + /*if (downloadChapter != null) { TODO GRAPHQL + downloadSubscriptionSource.publish(downloadChapter) + }*/ } private fun getStatus(): DownloadStatus { @@ -234,6 +238,7 @@ object DownloadManager { manga ) downloadQueue.add(downloadChapter) + downloadSubscriptionSource.publish(downloadChapter) logger.debug { "Added chapter ${chapter.id} to download queue (${manga.title} | ${chapter.name})" } return downloadChapter } From bce76bbcf36a21fc1916566a8e23303f7ccb5bca Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 30 Mar 2023 18:28:56 -0400 Subject: [PATCH 09/49] Use Kotlin Coroutines Flow instead of Project reactor --- server/build.gradle.kts | 7 +- .../server/JavalinGraphQLRequestParser.kt | 2 +- .../graphql/server/TachideskGraphQLSchema.kt | 6 +- .../graphql/server/TachideskGraphQLServer.kt | 5 +- .../ApolloSubscriptionProtocolHandler.kt | 76 ++++++++++--------- .../ApolloSubscriptionSessionState.kt | 31 ++++---- ...ionSource.kt => FlowSubscriptionSource.kt} | 12 +-- .../subscriptions/DownloadSubscription.kt | 9 ++- 8 files changed, 81 insertions(+), 67 deletions(-) rename server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/{FluxSubscriptionSource.kt => FlowSubscriptionSource.kt} (56%) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index cbca5f9f..cb5ff96d 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -64,10 +64,9 @@ dependencies { // implementation(fileTree("lib/")) implementation(kotlin("script-runtime")) - implementation("com.expediagroup", "graphql-kotlin-server", "6.3.0") - implementation("com.expediagroup", "graphql-kotlin-schema-generator", "6.3.0") - implementation("com.graphql-java", "graphql-java-extended-scalars", "19.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.5.0-RC-native-mt") + implementation("com.expediagroup:graphql-kotlin-server:6.3.0") + implementation("com.expediagroup:graphql-kotlin-schema-generator:6.3.0") + implementation("com.graphql-java:graphql-java-extended-scalars:20.0") testImplementation(libs.mockk) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt index 7dc243b4..c2497cd2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt @@ -14,7 +14,7 @@ import java.io.IOException class JavalinGraphQLRequestParser : GraphQLRequestParser { - @Suppress("BlockingMethodInNonBlockingContext") + @Suppress("BlockingMethodInNonBlockingContext", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") override suspend fun parseRequest(context: Context): GraphQLServerRequest = try { context.bodyAsClass(GraphQLServerRequest::class.java) } catch (e: IOException) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index e5de168b..c30a1af0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject -import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks +import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks import com.expediagroup.graphql.generator.toSchema import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLType @@ -21,10 +21,10 @@ import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass import kotlin.reflect.KType -class CustomSchemaGeneratorHooks : SchemaGeneratorHooks { +class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { Long::class -> ExtendedScalars.GraphQLLong - else -> null + else -> super.willGenerateGraphQLType(type) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt index 65f6a170..47d3e29e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt @@ -15,6 +15,9 @@ import graphql.GraphQL import io.javalin.http.Context import io.javalin.websocket.WsCloseContext import io.javalin.websocket.WsMessageContext +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import suwayomi.tachidesk.graphql.server.subscriptions.ApolloSubscriptionProtocolHandler import suwayomi.tachidesk.graphql.server.subscriptions.GraphQLSubscriptionHandler @@ -31,7 +34,7 @@ class TachideskGraphQLServer( subscriptionProtocolHandler.handleMessage(context) .map { objectMapper.writeValueAsString(it) } .map { context.send(it) } - .subscribe() + .launchIn(GlobalScope) } fun handleSubscriptionDisconnect(context: WsCloseContext) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt index 53df508e..c4d418ba 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -13,17 +13,24 @@ import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.readValue import io.javalin.websocket.WsContext import io.javalin.websocket.WsMessageContext -import kotlinx.coroutines.reactor.asFlux +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.job import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.publisher.toFlux import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.* import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.* import suwayomi.tachidesk.graphql.server.toGraphQLContext -import java.time.Duration /** * Implementation of the `graphql-ws` protocol defined by Apollo @@ -42,8 +49,8 @@ class ApolloSubscriptionProtocolHandler( private val acknowledgeMessage = SubscriptionOperationMessage(GQL_CONNECTION_ACK.type) @Suppress("Detekt.TooGenericExceptionCaught") - fun handleMessage(context: WsMessageContext): Flux { - val operationMessage = convertToMessageOrNull(context.message()) ?: return Flux.just(basicConnectionErrorMessage) + fun handleMessage(context: WsMessageContext): Flow { + val operationMessage = convertToMessageOrNull(context.message()) ?: return flowOf(basicConnectionErrorMessage) logger.debug("GraphQL subscription client message, sessionId=${context.sessionId} operationMessage=$operationMessage") return try { @@ -77,32 +84,34 @@ class ApolloSubscriptionProtocolHandler( * If the keep alive configuration is set, send a message back to client at every interval until the session is terminated. * Otherwise just return empty flux to append to the acknowledge message. */ - private fun getKeepAliveFlux(context: WsContext): Flux { + @OptIn(FlowPreview::class) + private fun getKeepAliveFlow(context: WsContext): Flow { val keepAliveInterval: Long? = 2000 if (keepAliveInterval != null) { - return Flux.interval(Duration.ofMillis(keepAliveInterval)) - .map { keepAliveMessage } - .doOnSubscribe { sessionState.saveKeepAliveSubscription(context, it) } + return flowOf(keepAliveMessage).sample(keepAliveInterval) + .onStart { + sessionState.saveKeepAliveSubscription(context, currentCoroutineContext().job) + } } - return Flux.empty() + return emptyFlow() } @Suppress("Detekt.TooGenericExceptionCaught") private fun startSubscription( operationMessage: SubscriptionOperationMessage, context: WsContext - ): Flux { + ): Flow { val graphQLContext = sessionState.getGraphQLContext(context) if (operationMessage.id == null) { logger.error("GraphQL subscription operation id is required") - return Flux.just(basicConnectionErrorMessage) + return flowOf(basicConnectionErrorMessage) } if (sessionState.doesOperationExist(context, operationMessage)) { logger.info("Already subscribed to operation ${operationMessage.id} for session ${context.sessionId}") - return Flux.empty() + return emptyFlow() } val payload = operationMessage.payload @@ -110,13 +119,12 @@ class ApolloSubscriptionProtocolHandler( if (payload == null) { logger.error("GraphQL subscription payload was null instead of a GraphQLRequest object") sessionState.stopOperation(context, operationMessage) - return Flux.just(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) + return flowOf(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) } try { val request = objectMapper.convertValue(payload) return subscriptionHandler.executeSubscription(request, graphQLContext) - .asFlux() .map { if (it.errors?.isNotEmpty() == true) { SubscriptionOperationMessage(type = GQL_ERROR.type, id = operationMessage.id, payload = it) @@ -124,22 +132,22 @@ class ApolloSubscriptionProtocolHandler( SubscriptionOperationMessage(type = GQL_DATA.type, id = operationMessage.id, payload = it) } } - .concatWith(onComplete(operationMessage, context).toFlux()) - .doOnSubscribe { sessionState.saveOperation(context, operationMessage, it) } + .onCompletion { if (it == null) emitAll(onComplete(operationMessage, context)) } + .onStart { sessionState.saveOperation(context, operationMessage, currentCoroutineContext().job) } } catch (exception: Exception) { logger.error("Error running graphql subscription", exception) // Do not terminate the session, just stop the operation messages sessionState.stopOperation(context, operationMessage) - return Flux.just(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) + return flowOf(SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type, id = operationMessage.id)) } } - private fun onInit(operationMessage: SubscriptionOperationMessage, context: WsContext): Flux { + private fun onInit(operationMessage: SubscriptionOperationMessage, context: WsContext): Flow { saveContext(operationMessage, context) - val acknowledgeMessage = Mono.just(acknowledgeMessage) - val keepAliveFlux = getKeepAliveFlux(context) - return acknowledgeMessage.concatWith(keepAliveFlux) - .onErrorReturn(getConnectionErrorMessage(operationMessage)) + val acknowledgeMessage = flowOf(acknowledgeMessage) + val keepAliveFlux = getKeepAliveFlow(context) + return acknowledgeMessage.onCompletion { if (it == null) emitAll(keepAliveFlux) } + .catch { emit(getConnectionErrorMessage(operationMessage)) } } /** @@ -158,7 +166,7 @@ class ApolloSubscriptionProtocolHandler( private fun onComplete( operationMessage: SubscriptionOperationMessage, context: WsContext - ): Mono { + ): Flow { return sessionState.completeOperation(context, operationMessage) } @@ -168,24 +176,24 @@ class ApolloSubscriptionProtocolHandler( private fun onStop( operationMessage: SubscriptionOperationMessage, context: WsContext - ): Flux { - return sessionState.stopOperation(context, operationMessage).toFlux() + ): Flow { + return sessionState.stopOperation(context, operationMessage) } - private fun onDisconnect(context: WsContext): Flux { + private fun onDisconnect(context: WsContext): Flow { sessionState.terminateSession(context) - return Flux.empty() + return emptyFlow() } - private fun onUnknownOperation(operationMessage: SubscriptionOperationMessage, context: WsContext): Flux { + private fun onUnknownOperation(operationMessage: SubscriptionOperationMessage, context: WsContext): Flow { logger.error("Unknown subscription operation $operationMessage") sessionState.stopOperation(context, operationMessage) - return Flux.just(getConnectionErrorMessage(operationMessage)) + return flowOf(getConnectionErrorMessage(operationMessage)) } - private fun onException(exception: Exception): Flux { + private fun onException(exception: Exception): Flow { logger.error("Error parsing the subscription message", exception) - return Flux.just(basicConnectionErrorMessage) + return flowOf(basicConnectionErrorMessage) } private fun getConnectionErrorMessage(operationMessage: SubscriptionOperationMessage): SubscriptionOperationMessage { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt index 7e2358ed..f9ec6b0f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt @@ -9,8 +9,11 @@ package suwayomi.tachidesk.graphql.server.subscriptions import graphql.GraphQLContext import io.javalin.websocket.WsContext -import org.reactivestreams.Subscription -import reactor.core.publisher.Mono +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onCompletion import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_COMPLETE import suwayomi.tachidesk.graphql.server.toGraphQLContext import java.util.concurrent.ConcurrentHashMap @@ -18,10 +21,10 @@ import java.util.concurrent.ConcurrentHashMap internal class ApolloSubscriptionSessionState { // Sessions are saved by web socket session id - internal val activeKeepAliveSessions = ConcurrentHashMap() + internal val activeKeepAliveSessions = ConcurrentHashMap() // Operations are saved by web socket session id, then operation id - internal val activeOperations = ConcurrentHashMap>() + internal val activeOperations = ConcurrentHashMap>() // The graphQL context is saved by web socket session id private val cachedGraphQLContext = ConcurrentHashMap() @@ -45,7 +48,7 @@ internal class ApolloSubscriptionSessionState { * This will override values without cancelling the subscription, so it is the responsibility of the consumer to cancel. * These messages will be stopped on [terminateSession]. */ - fun saveKeepAliveSubscription(context: WsContext, subscription: Subscription) { + fun saveKeepAliveSubscription(context: WsContext, subscription: Job) { activeKeepAliveSessions[context.sessionId] = subscription } @@ -54,10 +57,10 @@ internal class ApolloSubscriptionSessionState { * This will override values without cancelling the subscription so it is the responsibility of the consumer to cancel. * These messages will be stopped on [stopOperation]. */ - fun saveOperation(context: WsContext, operationMessage: SubscriptionOperationMessage, subscription: Subscription) { + fun saveOperation(context: WsContext, operationMessage: SubscriptionOperationMessage, subscription: Job) { val id = operationMessage.id if (id != null) { - val operationsForSession: ConcurrentHashMap = activeOperations.getOrPut(context.sessionId) { ConcurrentHashMap() } + val operationsForSession: ConcurrentHashMap = activeOperations.getOrPut(context.sessionId) { ConcurrentHashMap() } operationsForSession[id] = subscription } } @@ -66,26 +69,26 @@ internal class ApolloSubscriptionSessionState { * Send the [GQL_COMPLETE] message. * This can happen when the publisher finishes or if the client manually sends the stop message. */ - fun completeOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Mono { + fun completeOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Flow { return getCompleteMessage(operationMessage) - .doFinally { removeActiveOperation(context, operationMessage.id, cancelSubscription = false) } + .onCompletion { removeActiveOperation(context, operationMessage.id, cancelSubscription = false) } } /** * Stop the subscription sending data and send the [GQL_COMPLETE] message. * Does NOT terminate the session. */ - fun stopOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Mono { + fun stopOperation(context: WsContext, operationMessage: SubscriptionOperationMessage): Flow { return getCompleteMessage(operationMessage) - .doFinally { removeActiveOperation(context, operationMessage.id, cancelSubscription = true) } + .onCompletion { removeActiveOperation(context, operationMessage.id, cancelSubscription = true) } } - private fun getCompleteMessage(operationMessage: SubscriptionOperationMessage): Mono { + private fun getCompleteMessage(operationMessage: SubscriptionOperationMessage): Flow { val id = operationMessage.id if (id != null) { - return Mono.just(SubscriptionOperationMessage(type = GQL_COMPLETE.type, id = id)) + return flowOf(SubscriptionOperationMessage(type = GQL_COMPLETE.type, id = id)) } - return Mono.empty() + return emptyFlow() } /** diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FlowSubscriptionSource.kt similarity index 56% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FlowSubscriptionSource.kt index ab4c1675..16c60863 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FluxSubscriptionSource.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/FlowSubscriptionSource.kt @@ -7,14 +7,14 @@ package suwayomi.tachidesk.graphql.server.subscriptions -import reactor.core.publisher.Flux -import reactor.core.publisher.FluxSink +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow -class FluxSubscriptionSource() { - private var sink: FluxSink? = null - val emitter: Flux = Flux.create { emitter -> sink = emitter } +class FlowSubscriptionSource { + private val mutableSharedFlow = MutableSharedFlow() + val emitter = mutableSharedFlow.asSharedFlow() fun publish(value: T) { - sink?.next(value) + mutableSharedFlow.tryEmit(value) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt index 6f1db4f6..deb9bacc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt @@ -8,15 +8,16 @@ package suwayomi.tachidesk.graphql.subscriptions import graphql.schema.DataFetchingEnvironment -import reactor.core.publisher.Flux -import suwayomi.tachidesk.graphql.server.subscriptions.FluxSubscriptionSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import suwayomi.tachidesk.graphql.server.subscriptions.FlowSubscriptionSource import suwayomi.tachidesk.graphql.types.DownloadType import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter -val downloadSubscriptionSource = FluxSubscriptionSource() +val downloadSubscriptionSource = FlowSubscriptionSource() class DownloadSubscription { - fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flux { + fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow { return downloadSubscriptionSource.emitter.map { downloadChapter -> DownloadType(downloadChapter) } From d4599c3331cab595b810b870a48cf94a91a14751 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 19:09:32 -0400 Subject: [PATCH 10/49] Use Graphiql with the Explorer plugin for the query builder --- .../graphql/controller/GraphQLController.kt | 12 +- .../main/resources/graphql-playground.html | 126 +++++++++++------- 2 files changed, 77 insertions(+), 61 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt index 433b64d6..3c0c4e03 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/controller/GraphQLController.kt @@ -25,18 +25,10 @@ object GraphQLController { } fun playground(ctx: Context) { - val playgroundHtml = javaClass.getResource("/graphql-playground.html") - - val body = playgroundHtml.openStream().bufferedReader().use { reader -> - val graphQLEndpoint = "graphql" - val subscriptionsEndpoint = "graphql" - + val body = javaClass.getResourceAsStream("/graphql-playground.html")!!.bufferedReader().use { reader -> reader.readText() - .replace("\${graphQLEndpoint}", graphQLEndpoint) - .replace("\${subscriptionsEndpoint}", subscriptionsEndpoint) } - - ctx.html(body ?: "Could not load playground") + ctx.html(body) } fun webSocket(ws: WsConfig) { diff --git a/server/src/main/resources/graphql-playground.html b/server/src/main/resources/graphql-playground.html index 894ff66f..76407218 100644 --- a/server/src/main/resources/graphql-playground.html +++ b/server/src/main/resources/graphql-playground.html @@ -1,60 +1,84 @@ - - + - - - GraphQL Playground - - - + GraphiQL + + + + + + + + + -
- - -
Loading - GraphQL Playground -
-
- + ReactDOM.render( + React.createElement(GraphiQLWithExplorer), + document.getElementById('graphiql'), + ); + - From 00370a81fa38e7f63a48a759c32dd7acb1e80ce8 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 19:10:04 -0400 Subject: [PATCH 11/49] Minor cleanup --- server/build.gradle.kts | 4 ++-- .../tachidesk/graphql/dataLoaders/CategoryDataLoader.kt | 9 +++++---- .../tachidesk/graphql/dataLoaders/ChapterDataLoader.kt | 6 +++--- .../tachidesk/graphql/dataLoaders/MangaDataLoader.kt | 6 +++--- .../tachidesk/graphql/dataLoaders/MetaDataLoader.kt | 8 ++++---- .../subscriptions/ApolloSubscriptionProtocolHandler.kt | 8 +++----- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index cb5ff96d..65591dc3 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -64,8 +64,8 @@ dependencies { // implementation(fileTree("lib/")) implementation(kotlin("script-runtime")) - implementation("com.expediagroup:graphql-kotlin-server:6.3.0") - implementation("com.expediagroup:graphql-kotlin-schema-generator:6.3.0") + implementation("com.expediagroup:graphql-kotlin-server:6.4.0") + implementation("com.expediagroup:graphql-kotlin-schema-generator:6.4.0") implementation("com.graphql-java:graphql-java-extended-scalars:20.0") testImplementation(libs.mockk) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index 61146aad..6ec126fd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -17,12 +17,12 @@ import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable -import java.util.concurrent.CompletableFuture +import suwayomi.tachidesk.server.JavalinSetup.future class CategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) CategoryTable.select { CategoryTable.id inList ids } @@ -35,10 +35,11 @@ class CategoryDataLoader : KotlinDataLoader { class CategoriesForMangaDataLoader : KotlinDataLoader> { override val dataLoaderName = "CategoriesForMangaDataLoader" override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) - val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga inList ids } + val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable) + .select { CategoryMangaTable.manga inList ids } .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } .groupBy { it.first } .mapValues { it.value.map { pair -> pair.second } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index c3567893..7bfd6d03 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -16,12 +16,12 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable -import java.util.concurrent.CompletableFuture +import suwayomi.tachidesk.server.JavalinSetup.future class ChapterDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) ChapterTable.select { ChapterTable.id inList ids } @@ -34,7 +34,7 @@ class ChapterDataLoader : KotlinDataLoader { class ChaptersForMangaDataLoader : KotlinDataLoader> { override val dataLoaderName = "ChaptersForMangaDataLoader" override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index b3e9fb8d..186b6fd5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -18,12 +18,12 @@ import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable -import java.util.concurrent.CompletableFuture +import suwayomi.tachidesk.server.JavalinSetup.future class MangaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) MangaTable.select { MangaTable.id inList ids } @@ -36,7 +36,7 @@ class MangaDataLoader : KotlinDataLoader { class MangaForCategoryDataLoader : KotlinDataLoader> { override val dataLoaderName = "MangaForCategoryDataLoader" override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) val itemsByRef = CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category inList ids } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 57ea723f..35c03cbb 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -13,12 +13,12 @@ import suwayomi.tachidesk.graphql.types.MangaMetaItem import suwayomi.tachidesk.graphql.types.MetaType import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable -import java.util.concurrent.CompletableFuture +import suwayomi.tachidesk.server.JavalinSetup.future class ChapterMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterMetaDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } @@ -33,7 +33,7 @@ class ChapterMetaDataLoader : KotlinDataLoader { class MangaMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaMetaDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } @@ -48,7 +48,7 @@ class MangaMetaDataLoader : KotlinDataLoader { class CategoryMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryMetaDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { + future { transaction { addLogger(StdOutSqlLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt index c4d418ba..338d00b7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.sample import kotlinx.coroutines.job import kotlinx.coroutines.runBlocking -import org.slf4j.LoggerFactory +import mu.KotlinLogging import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.* import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.* @@ -43,15 +43,14 @@ class ApolloSubscriptionProtocolHandler( private val objectMapper: ObjectMapper ) { private val sessionState = ApolloSubscriptionSessionState() - private val logger = LoggerFactory.getLogger(ApolloSubscriptionProtocolHandler::class.java) + private val logger = KotlinLogging.logger {} private val keepAliveMessage = SubscriptionOperationMessage(type = GQL_CONNECTION_KEEP_ALIVE.type) private val basicConnectionErrorMessage = SubscriptionOperationMessage(type = GQL_CONNECTION_ERROR.type) private val acknowledgeMessage = SubscriptionOperationMessage(GQL_CONNECTION_ACK.type) - @Suppress("Detekt.TooGenericExceptionCaught") fun handleMessage(context: WsMessageContext): Flow { val operationMessage = convertToMessageOrNull(context.message()) ?: return flowOf(basicConnectionErrorMessage) - logger.debug("GraphQL subscription client message, sessionId=${context.sessionId} operationMessage=$operationMessage") + logger.debug { "GraphQL subscription client message, sessionId=${context.sessionId} operationMessage=$operationMessage" } return try { when (operationMessage.type) { @@ -70,7 +69,6 @@ class ApolloSubscriptionProtocolHandler( onDisconnect(context) } - @Suppress("Detekt.TooGenericExceptionCaught") private fun convertToMessageOrNull(payload: String): SubscriptionOperationMessage? { return try { objectMapper.readValue(payload) From 007d20d41754efcf6eef269aa9947771a17c4e2a Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 19:44:21 -0400 Subject: [PATCH 12/49] Add Sources to Graphql --- .../graphql/dataLoaders/SourceDataLoader.kt | 62 +++++++++++++++++++ .../tachidesk/graphql/queries/SourceQuery.kt | 24 +++++++ .../TachideskDataLoaderRegistryFactory.kt | 4 +- .../graphql/server/TachideskGraphQLSchema.kt | 4 +- .../tachidesk/graphql/types/MangaType.kt | 4 ++ .../tachidesk/graphql/types/SourceType.kt | 39 ++++++++++++ 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt new file mode 100644 index 00000000..13f476db --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.SourceType +import suwayomi.tachidesk.manga.impl.Source +import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.future + +class SourceDataLoader : KotlinDataLoader { + override val dataLoaderName = "SourceDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + future { + Source.getSourceList().filter { it.id in ids } + .map { SourceType(it) } + } + } +} + +class SourceForMangaDataLoader : KotlinDataLoader { + override val dataLoaderName = "SourceForMangaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(StdOutSqlLogger) + val mangaSourceMap = MangaTable + .select { MangaTable.id inList ids } + .associate { it[MangaTable.id].value to it[MangaTable.sourceReference] } + + val sourceIds = mangaSourceMap + .values + .distinct() + .map { it.toString() } + + val sources = Source.getSourceList() + .filter { it.id in sourceIds } + .map { SourceType(it) } + .associateBy { it.id } + + val mangaSourceTypeMap = mangaSourceMap.mapValues { + sources[it.value] + } + + ids.map { + mangaSourceTypeMap[it] + } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt new file mode 100644 index 00000000..edfb44d2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.types.SourceType +import suwayomi.tachidesk.manga.impl.Source +import java.util.concurrent.CompletableFuture + +class SourceQuery { + fun source(dataFetchingEnvironment: DataFetchingEnvironment, id: Long): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) + } + + fun sources(): List { + return Source.getSourceList().map { SourceType(it) } + } +} 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 1186a1ce..639f6829 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -21,7 +21,9 @@ class TachideskDataLoaderRegistryFactory { MangaMetaDataLoader(), MangaForCategoryDataLoader(), CategoryMetaDataLoader(), - CategoriesForMangaDataLoader() + CategoriesForMangaDataLoader(), + SourceDataLoader(), + SourceForMangaDataLoader() ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index c30a1af0..113afb4c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -17,6 +17,7 @@ import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.MangaQuery +import suwayomi.tachidesk.graphql.queries.SourceQuery import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass import kotlin.reflect.KType @@ -37,7 +38,8 @@ val schema = toSchema( queries = listOf( TopLevelObject(MangaQuery()), TopLevelObject(ChapterQuery()), - TopLevelObject(CategoryQuery()) + TopLevelObject(CategoryQuery()), + TopLevelObject(SourceQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 1c1030ce..01e21816 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -95,4 +95,8 @@ class MangaType( fun categories(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { return dataFetchingEnvironment.getValueFromDataLoader>("CategoriesForMangaDataLoader", id) } + + fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourceForMangaDataLoader", id) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt new file mode 100644 index 00000000..c157747d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass +import java.util.concurrent.CompletableFuture + +class SourceType( + val id: Long, + val name: String, + val lang: String, + val iconUrl: String, + val supportsLatest: Boolean, + val isConfigurable: Boolean, + val isNsfw: Boolean, + val displayName: String +) { + constructor(source: SourceDataClass) : this( + id = source.id.toLong(), + name = source.name, + lang = source.lang, + iconUrl = source.iconUrl, + supportsLatest = source.supportsLatest, + isConfigurable = source.isConfigurable, + isNsfw = source.isNsfw, + displayName = source.displayName + ) + + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("MangaForSourceDataLoader", id) + } +} From 37f41ade43827fed827a381f1e28bd27d04a1797 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 20:29:55 -0400 Subject: [PATCH 13/49] Directly use the database for sources in graphql --- .../graphql/dataLoaders/SourceDataLoader.kt | 51 ++++++++++--------- .../tachidesk/graphql/queries/SourceQuery.kt | 14 +++-- .../tachidesk/graphql/types/MangaType.kt | 4 +- .../tachidesk/graphql/types/SourceType.kt | 29 +++++++++++ 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index 13f476db..9f5fa6ec 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -10,52 +10,53 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.SourceType -import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.server.JavalinSetup.future -class SourceDataLoader : KotlinDataLoader { +class SourceDataLoader : KotlinDataLoader { override val dataLoaderName = "SourceDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { - Source.getSourceList().filter { it.id in ids } - .map { SourceType(it) } + transaction { + SourceTable.select { SourceTable.id inList ids }.map { + SourceType(it) + } + } } } } class SourceForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "SourceForMangaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(StdOutSqlLogger) - val mangaSourceMap = MangaTable + + val itemsByRef = MangaTable.innerJoin(SourceTable) .select { MangaTable.id inList ids } - .associate { it[MangaTable.id].value to it[MangaTable.sourceReference] } + .map { Triple(it[MangaTable.id].value, it[MangaTable.sourceReference], it) } + .let { triples -> + val sources = buildMap { + triples.forEach { + if (!containsKey(it.second)) { + put(it.second, SourceType(it.third)) + } + } + } + triples.associate { + it.first to sources[it.second] + } + } - val sourceIds = mangaSourceMap - .values - .distinct() - .map { it.toString() } - - val sources = Source.getSourceList() - .filter { it.id in sourceIds } - .map { SourceType(it) } - .associateBy { it.id } - - val mangaSourceTypeMap = mangaSourceMap.mapValues { - sources[it.value] - } - - ids.map { - mangaSourceTypeMap[it] - } + ids.map { itemsByRef[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index edfb44d2..b28719c5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -9,16 +9,22 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.SourceType -import suwayomi.tachidesk.manga.impl.Source +import suwayomi.tachidesk.manga.model.table.SourceTable import java.util.concurrent.CompletableFuture class SourceQuery { - fun source(dataFetchingEnvironment: DataFetchingEnvironment, id: Long): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) + fun source(dataFetchingEnvironment: DataFetchingEnvironment, id: Long): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) } fun sources(): List { - return Source.getSourceList().map { SourceType(it) } + val results = transaction { + SourceTable.selectAll().toList().mapNotNull { SourceType(it) } + } + + return results } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 01e21816..8ded86b4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -96,7 +96,7 @@ class MangaType( return dataFetchingEnvironment.getValueFromDataLoader>("CategoriesForMangaDataLoader", id) } - fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("SourceForMangaDataLoader", id) + fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourceForMangaDataLoader", id) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index c157747d..c5c7611e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -8,8 +8,16 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.ConfigurableSource import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.select +import suwayomi.tachidesk.manga.impl.extension.Extension +import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass +import suwayomi.tachidesk.manga.model.table.ExtensionTable +import suwayomi.tachidesk.manga.model.table.SourceTable import java.util.concurrent.CompletableFuture class SourceType( @@ -33,7 +41,28 @@ class SourceType( displayName = source.displayName ) + constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this( + id = row[SourceTable.id].value, + name = row[SourceTable.name], + lang = row[SourceTable.lang], + iconUrl = Extension.getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]), + supportsLatest = catalogueSource.supportsLatest, + isConfigurable = catalogueSource is ConfigurableSource, + isNsfw = row[SourceTable.isNsfw], + displayName = catalogueSource.toString() + ) + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { return dataFetchingEnvironment.getValueFromDataLoader>("MangaForSourceDataLoader", id) } } + +fun SourceType(row: ResultRow): SourceType? { + val catalogueSource = GetCatalogueSource + .getCatalogueSourceOrNull(row[SourceTable.id].value) + ?: return null + val sourceExtension = ExtensionTable + .select { ExtensionTable.id eq row[SourceTable.extension] } + .first() + return SourceType(row, sourceExtension, catalogueSource) +} From 6541c7b5b7a2219b14ac0524f77939e9b1ee36ef Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 20:30:24 -0400 Subject: [PATCH 14/49] Serialize Long as String in graphql --- .../tachidesk/graphql/server/TachideskGraphQLSchema.kt | 4 ++-- .../kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 113afb4c..8c41df54 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -11,7 +11,7 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks import com.expediagroup.graphql.generator.toSchema -import graphql.scalars.ExtendedScalars +import graphql.Scalars import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery @@ -24,7 +24,7 @@ import kotlin.reflect.KType class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { - Long::class -> ExtendedScalars.GraphQLLong + Long::class -> Scalars.GraphQLString // encode to string for JS else -> super.willGenerateGraphQLType(type) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 8ded86b4..87672c51 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -19,7 +19,7 @@ import java.util.concurrent.CompletableFuture class MangaType( val id: Int, - val sourceId: String, + val sourceId: Long, val url: String, val title: String, val thumbnailUrl: String?, @@ -37,7 +37,7 @@ class MangaType( ) { constructor(row: ResultRow) : this( row[MangaTable.id].value, - row[MangaTable.sourceReference].toString(), + row[MangaTable.sourceReference], row[MangaTable.url], row[MangaTable.title], row[MangaTable.thumbnail_url], @@ -56,7 +56,7 @@ class MangaType( constructor(dataClass: MangaDataClass) : this( dataClass.id, - dataClass.sourceId, + dataClass.sourceId.toLong(), dataClass.url, dataClass.title, dataClass.thumbnailUrl, From 3a67ddf0f697967e4dbc33f3ce84aac142f4b791 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 20:58:18 -0400 Subject: [PATCH 15/49] Add Extensions to Graphql --- .../dataLoaders/ExtensionDataLoader.kt | 47 ++++++++++++++++++ .../graphql/dataLoaders/SourceDataLoader.kt | 20 ++++++++ .../graphql/queries/ExtensionQuery.kt | 30 ++++++++++++ .../TachideskDataLoaderRegistryFactory.kt | 5 +- .../graphql/server/TachideskGraphQLSchema.kt | 4 +- .../tachidesk/graphql/types/ExtensionType.kt | 48 +++++++++++++++++++ .../tachidesk/graphql/types/SourceType.kt | 15 ++++-- 7 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt new file mode 100644 index 00000000..37478b75 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.dataLoaders + +import com.expediagroup.graphql.dataloader.KotlinDataLoader +import org.dataloader.DataLoader +import org.dataloader.DataLoaderFactory +import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ExtensionType +import suwayomi.tachidesk.manga.model.table.ExtensionTable +import suwayomi.tachidesk.manga.model.table.SourceTable +import suwayomi.tachidesk.server.JavalinSetup.future + +class ExtensionDataLoader : KotlinDataLoader { + override val dataLoaderName = "ExtensionDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(StdOutSqlLogger) + ExtensionTable.select { ExtensionTable.pkgName inList ids } + .map { ExtensionType(it) } + } + } + } +} + +class ExtensionForSourceDataLoader : KotlinDataLoader { + override val dataLoaderName = "ExtensionForSourceDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(StdOutSqlLogger) + ExtensionTable.innerJoin(SourceTable) + .select { SourceTable.id inList ids } + .map { ExtensionType(it) } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index 9f5fa6ec..e9ce4ced 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -16,6 +16,7 @@ import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.SourceType +import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.server.JavalinSetup.future @@ -61,3 +62,22 @@ class SourceForMangaDataLoader : KotlinDataLoader { } } } + +class SourcesForExtensionDataLoader : KotlinDataLoader> { + override val dataLoaderName = "SourcesForExtensionDataLoader" + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(StdOutSqlLogger) + + val sourcesByExtensionPkg = SourceTable.innerJoin(ExtensionTable) + .select { ExtensionTable.pkgName inList ids } + .map { Pair(it[ExtensionTable.pkgName], SourceType(it)) } + .groupBy { it.first } + .mapValues { it.value.mapNotNull { pair -> pair.second } } + + ids.map { sourcesByExtensionPkg[it] ?: emptyList() } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt new file mode 100644 index 00000000..68c754b8 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ExtensionType +import suwayomi.tachidesk.manga.model.table.ExtensionTable +import java.util.concurrent.CompletableFuture + +class ExtensionQuery { + fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) + } + + fun extensions(): List { + val results = transaction { + ExtensionTable.selectAll().toList() + } + + return results.map { ExtensionType(it) } + } +} 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 639f6829..81a95a7a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -23,7 +23,10 @@ class TachideskDataLoaderRegistryFactory { CategoryMetaDataLoader(), CategoriesForMangaDataLoader(), SourceDataLoader(), - SourceForMangaDataLoader() + SourceForMangaDataLoader(), + SourcesForExtensionDataLoader(), + ExtensionDataLoader(), + ExtensionForSourceDataLoader() ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 8c41df54..665ea526 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -16,6 +16,7 @@ import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery +import suwayomi.tachidesk.graphql.queries.ExtensionQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription @@ -39,7 +40,8 @@ val schema = toSchema( TopLevelObject(MangaQuery()), TopLevelObject(ChapterQuery()), TopLevelObject(CategoryQuery()), - TopLevelObject(SourceQuery()) + TopLevelObject(SourceQuery()), + TopLevelObject(ExtensionQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt new file mode 100644 index 00000000..93bf09b1 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.manga.model.table.ExtensionTable +import java.util.concurrent.CompletableFuture + +class ExtensionType( + val apkName: String, + val iconUrl: String, + + val name: String, + val pkgName: String, + val versionName: String, + val versionCode: Int, + val lang: String, + val isNsfw: Boolean, + + val installed: Boolean, + val hasUpdate: Boolean, + val obsolete: Boolean +) { + constructor(row: ResultRow) : this( + apkName = row[ExtensionTable.apkName], + iconUrl = row[ExtensionTable.iconUrl], + name = row[ExtensionTable.name], + pkgName = row[ExtensionTable.pkgName], + versionName = row[ExtensionTable.versionName], + versionCode = row[ExtensionTable.versionCode], + lang = row[ExtensionTable.lang], + isNsfw = row[ExtensionTable.isNsfw], + installed = row[ExtensionTable.isInstalled], + hasUpdate = row[ExtensionTable.hasUpdate], + obsolete = row[ExtensionTable.isObsolete] + ) + + fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("SourcesForExtensionDataLoader", pkgName) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index c5c7611e..27d2db96 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -55,14 +55,23 @@ class SourceType( fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { return dataFetchingEnvironment.getValueFromDataLoader>("MangaForSourceDataLoader", id) } + + fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ExtensionForSourceDataLoader", id) + } } fun SourceType(row: ResultRow): SourceType? { val catalogueSource = GetCatalogueSource .getCatalogueSourceOrNull(row[SourceTable.id].value) ?: return null - val sourceExtension = ExtensionTable - .select { ExtensionTable.id eq row[SourceTable.extension] } - .first() + val sourceExtension = if (row.hasValue(ExtensionTable.id)) { + row + } else { + ExtensionTable + .select { ExtensionTable.id eq row[SourceTable.extension] } + .first() + } + return SourceType(row, sourceExtension, catalogueSource) } From 4c30d8ab05ccb59dee13b714c6a8e6df18096d03 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 22:00:01 -0400 Subject: [PATCH 16/49] Some TODOs with ideas --- .../tachidesk/graphql/queries/CategoryQuery.kt | 12 ++++++++++++ .../tachidesk/graphql/queries/ChapterQuery.kt | 15 +++++++++++++++ .../tachidesk/graphql/queries/ExtensionQuery.kt | 17 +++++++++++++++++ .../tachidesk/graphql/queries/MangaQuery.kt | 13 +++++++++++++ .../tachidesk/graphql/queries/SourceQuery.kt | 12 ++++++++++++ 5 files changed, 69 insertions(+) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 15f73722..e73b5154 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -15,6 +15,18 @@ import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryTable import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - Sort? + * - Query by name + * - In ID list + * - Paged queries + * + * TODO Mutations + * - Name + * - Order + * - Default + */ class CategoryQuery { fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 3dbaa076..83297948 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -16,6 +16,21 @@ import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - Filter by read + * - Filter by bookmarked + * - Filter by downloaded + * - Filter by scanlators + * - Sort? Upload date, source order, last read, chapter number + * + * TODO Mutations + * - Last page read + * - Read status + * - bookmark status + * - Check for updates? + * - Download + */ class ChapterQuery { fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt index 68c754b8..cd341832 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -15,6 +15,23 @@ import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.manga.model.table.ExtensionTable import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - Installed + * - HasUpdate + * - Obsolete + * - IsNsfw + * - In Pkg name list + * - Query name + * - Sort? + * - Paged Queries + * + * TODO Mutations + * - Install + * - Update + * - Uninstall + * - Check for updates (global mutation?) + */ class ExtensionQuery { fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 9f19c65e..18bc778f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -18,6 +18,19 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - Query options(optionally query the title, description, or/and) + * - Sort? + * + * TODO Mutations + * - Favorite + * - Unfavorite + * - Add to category + * - Remove from category + * - Check for updates + * - Download x(all = -1) chapters + */ class MangaQuery { fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index b28719c5..f26e12a5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -15,6 +15,18 @@ import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.manga.model.table.SourceTable import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - Filter by languages + * - Filter by name + * - Filter by NSFW + * - Sort? + * + * TODO Mutations + * - Browse with filters + * - Configure settings + * + */ class SourceQuery { fun source(dataFetchingEnvironment: DataFetchingEnvironment, id: Long): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) From eb197ebceef573defff5b3738a195c218bcb29ad Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 22:19:13 -0400 Subject: [PATCH 17/49] Switch database logger to SLF4J --- .../kotlin/suwayomi/tachidesk/graphql/GraphQL.kt | 4 +++- .../graphql/dataLoaders/CategoryDataLoader.kt | 6 +++--- .../graphql/dataLoaders/ChapterDataLoader.kt | 6 +++--- .../graphql/dataLoaders/ExtensionDataLoader.kt | 6 +++--- .../graphql/dataLoaders/MangaDataLoader.kt | 7 +++---- .../graphql/dataLoaders/MetaDataLoader.kt | 8 ++++---- .../graphql/dataLoaders/SourceDataLoader.kt | 8 ++++---- .../tachidesk/graphql/mutations/ChapterMutation.kt | 5 ++++- .../tachidesk/graphql/queries/ChapterQuery.kt | 1 + .../server/TachideskDataLoaderRegistryFactory.kt | 14 +++++++++++++- .../tachidesk/manga/controller/UpdateController.kt | 2 +- 11 files changed, 42 insertions(+), 25 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt index 8e0225e3..d7343cd0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/GraphQL.kt @@ -7,7 +7,9 @@ package suwayomi.tachidesk.graphql -import io.javalin.apibuilder.ApiBuilder.* +import io.javalin.apibuilder.ApiBuilder.get +import io.javalin.apibuilder.ApiBuilder.post +import io.javalin.apibuilder.ApiBuilder.ws import suwayomi.tachidesk.graphql.controller.GraphQLController object GraphQL { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index 6ec126fd..09dbc52d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -10,7 +10,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -24,7 +24,7 @@ class CategoryDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) CategoryTable.select { CategoryTable.id inList ids } .map { CategoryType(it) } } @@ -37,7 +37,7 @@ class CategoriesForMangaDataLoader : KotlinDataLoader> { override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable) .select { CategoryMangaTable.manga inList ids } .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 7bfd6d03..1713df8a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -10,7 +10,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -23,7 +23,7 @@ class ChapterDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) ChapterTable.select { ChapterTable.id inList ids } .map { ChapterType(it) } } @@ -36,7 +36,7 @@ class ChaptersForMangaDataLoader : KotlinDataLoader> { override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } .map { ChapterType(it) } .groupBy { it.mangaId } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt index 37478b75..034ebde9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt @@ -10,7 +10,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -24,7 +24,7 @@ class ExtensionDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) ExtensionTable.select { ExtensionTable.pkgName inList ids } .map { ExtensionType(it) } } @@ -37,7 +37,7 @@ class ExtensionForSourceDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) ExtensionTable.innerJoin(SourceTable) .select { SourceTable.id inList ids } .map { ExtensionType(it) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 186b6fd5..3725efae 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -10,8 +10,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -25,7 +24,7 @@ class MangaDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) MangaTable.select { MangaTable.id inList ids } .map { MangaType(it) } } @@ -38,7 +37,7 @@ class MangaForCategoryDataLoader : KotlinDataLoader> { override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val itemsByRef = CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category inList ids } .map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) } .groupBy { it.first } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 35c03cbb..a9465eef 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -3,7 +3,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -20,7 +20,7 @@ class ChapterMetaDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } .map { ChapterMetaItem(it) } .groupBy { it.ref } @@ -35,7 +35,7 @@ class MangaMetaDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } .map { MangaMetaItem(it) } .groupBy { it.ref } @@ -50,7 +50,7 @@ class CategoryMetaDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } .map { CategoryMetaItem(it) } .groupBy { it.ref } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index e9ce4ced..031f621e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -10,8 +10,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory -import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList -import org.jetbrains.exposed.sql.StdOutSqlLogger +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -26,6 +25,7 @@ class SourceDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { + addLogger(Slf4jSqlDebugLogger) SourceTable.select { SourceTable.id inList ids }.map { SourceType(it) } @@ -39,7 +39,7 @@ class SourceForMangaDataLoader : KotlinDataLoader { override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val itemsByRef = MangaTable.innerJoin(SourceTable) .select { MangaTable.id inList ids } @@ -68,7 +68,7 @@ class SourcesForExtensionDataLoader : KotlinDataLoader> override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader { ids -> future { transaction { - addLogger(StdOutSqlLogger) + addLogger(Slf4jSqlDebugLogger) val sourcesByExtensionPkg = SourceTable.innerJoin(ExtensionTable) .select { ExtensionTable.pkgName inList ids } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt index c84ec775..c1019cd6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -2,10 +2,13 @@ package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert +import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 83297948..e405a25f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -23,6 +23,7 @@ import java.util.concurrent.CompletableFuture * - Filter by downloaded * - Filter by scanlators * - Sort? Upload date, source order, last read, chapter number + * - Get page list? * * TODO Mutations * - Last page read 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 81a95a7a..bafdefdb 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -8,7 +8,19 @@ package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory -import suwayomi.tachidesk.graphql.dataLoaders.* +import suwayomi.tachidesk.graphql.dataLoaders.CategoriesForMangaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.CategoryMetaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ChapterDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ChapterMetaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ChaptersForMangaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.MangaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.MangaForCategoryDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.MangaMetaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.SourceDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.SourceForMangaDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.SourcesForExtensionDataLoader class TachideskDataLoaderRegistryFactory { companion object { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt index 06f055a6..78ff6d46 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -115,7 +115,7 @@ object UpdateController { updater.addMangasToQueue( mangasToUpdate - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)), + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title)) ) } From 399eb07e359fa7d87d4f566a936143eac78815ee Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 22:21:09 -0400 Subject: [PATCH 18/49] Fix imports --- .../ApolloSubscriptionProtocolHandler.kt | 11 +++++++++-- .../tachidesk/manga/controller/UpdateController.kt | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt index 338d00b7..e8da2c60 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -28,8 +28,15 @@ import kotlinx.coroutines.job import kotlinx.coroutines.runBlocking import mu.KotlinLogging import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory -import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.* -import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.* +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_TERMINATE +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_START +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_STOP +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_ACK +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_ERROR +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_KEEP_ALIVE +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_DATA +import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR import suwayomi.tachidesk.graphql.server.toGraphQLContext /** diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt index 78ff6d46..48328a31 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -13,7 +13,11 @@ import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.UpdateStatus import suwayomi.tachidesk.manga.impl.update.UpdaterSocket -import suwayomi.tachidesk.manga.model.dataclass.* +import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass +import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate +import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass +import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass +import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler From 9312f5fd14b9cb8e90be7e3ddb6cc35d6bb21e30 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 22:30:14 -0400 Subject: [PATCH 19/49] Add global meta --- .../graphql/dataLoaders/MetaDataLoader.kt | 18 +++++++++++ .../tachidesk/graphql/queries/MetaQuery.kt | 31 +++++++++++++++++++ .../TachideskDataLoaderRegistryFactory.kt | 2 ++ .../graphql/server/TachideskGraphQLSchema.kt | 4 ++- .../tachidesk/graphql/types/MetaType.kt | 7 ++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index a9465eef..3a61300a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -7,14 +7,32 @@ import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.graphql.types.CategoryMetaItem import suwayomi.tachidesk.graphql.types.ChapterMetaItem +import suwayomi.tachidesk.graphql.types.GlobalMetaItem import suwayomi.tachidesk.graphql.types.MangaMetaItem +import suwayomi.tachidesk.graphql.types.MetaItem import suwayomi.tachidesk.graphql.types.MetaType import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.server.JavalinSetup.future +class GlobalMetaDataLoader : KotlinDataLoader { + override val dataLoaderName = "GlobalMetaDataLoader" + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + future { + transaction { + addLogger(Slf4jSqlDebugLogger) + val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids } + .map { GlobalMetaItem(it) } + .associateBy { it.key } + ids.map { metasByRefId[it] } + } + } + } +} + class ChapterMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterMetaDataLoader" override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt new file mode 100644 index 00000000..ae6742a0 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.global.model.table.GlobalMetaTable +import suwayomi.tachidesk.graphql.types.GlobalMetaItem +import suwayomi.tachidesk.graphql.types.MetaItem +import java.util.concurrent.CompletableFuture + +class MetaQuery { + fun meta(dataFetchingEnvironment: DataFetchingEnvironment, key: String): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) + } + + fun metas(): List { + val results = transaction { + GlobalMetaTable.selectAll().toList() + } + + return results.map { GlobalMetaItem(it) } + } +} 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 bafdefdb..b180e9f8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -15,6 +15,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.ChapterMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.ChaptersForMangaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaForCategoryDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaMetaDataLoader @@ -29,6 +30,7 @@ class TachideskDataLoaderRegistryFactory { MangaDataLoader(), ChapterDataLoader(), ChaptersForMangaDataLoader(), + GlobalMetaDataLoader(), ChapterMetaDataLoader(), MangaMetaDataLoader(), MangaForCategoryDataLoader(), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 665ea526..92e0d2a5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -18,6 +18,7 @@ import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.ExtensionQuery import suwayomi.tachidesk.graphql.queries.MangaQuery +import suwayomi.tachidesk.graphql.queries.MetaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass @@ -41,7 +42,8 @@ val schema = toSchema( TopLevelObject(ChapterQuery()), TopLevelObject(CategoryQuery()), TopLevelObject(SourceQuery()), - TopLevelObject(ExtensionQuery()) + TopLevelObject(ExtensionQuery()), + TopLevelObject(MetaQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index a8c19240..2283104e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -2,6 +2,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable @@ -12,7 +13,7 @@ open class MetaItem( val key: String, val value: String, @GraphQLIgnore - val ref: Int + val ref: Int? ) class ChapterMetaItem( @@ -26,3 +27,7 @@ class MangaMetaItem( class CategoryMetaItem( private val row: ResultRow ) : MetaItem(row[CategoryMetaTable.key], row[CategoryMetaTable.value], row[CategoryMetaTable.ref].value) + +class GlobalMetaItem( + private val row: ResultRow +) : MetaItem(row[GlobalMetaTable.key], row[GlobalMetaTable.value], null) From 3bbda7ba549e4c9eaa2314a22f5ba3a3915a9ee3 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 22:36:01 -0400 Subject: [PATCH 20/49] More todos --- .../suwayomi/tachidesk/graphql/queries/CategoryQuery.kt | 4 ++++ .../suwayomi/tachidesk/graphql/queries/ChapterQuery.kt | 1 + .../suwayomi/tachidesk/graphql/queries/MangaQuery.kt | 3 +++ .../suwayomi/tachidesk/graphql/queries/MetaQuery.kt | 9 +++++++++ .../suwayomi/tachidesk/graphql/queries/SourceQuery.kt | 1 + 5 files changed, 18 insertions(+) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index e73b5154..ca646742 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -26,6 +26,10 @@ import java.util.concurrent.CompletableFuture * - Name * - Order * - Default + * - Create + * - Delete + * - Add/update meta + * - Delete meta */ class CategoryQuery { fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index e405a25f..e901c2cc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -31,6 +31,7 @@ import java.util.concurrent.CompletableFuture * - bookmark status * - Check for updates? * - Download + * - Delete download */ class ChapterQuery { fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 18bc778f..a6950dc0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -30,6 +30,9 @@ import java.util.concurrent.CompletableFuture * - Remove from category * - Check for updates * - Download x(all = -1) chapters + * - Delete read/all downloaded chapters + * - Add/update meta + * - Delete meta */ class MangaQuery { fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt index ae6742a0..f5b2b2cc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -16,6 +16,15 @@ import suwayomi.tachidesk.graphql.types.GlobalMetaItem import suwayomi.tachidesk.graphql.types.MetaItem import java.util.concurrent.CompletableFuture +/** + * TODO Queries + * - In list of keys + * + * TODO Mutations + * - Add/update meta + * - Delete meta + * + */ class MetaQuery { fun meta(dataFetchingEnvironment: DataFetchingEnvironment, key: String): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index f26e12a5..939b0523 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture * - Filter by languages * - Filter by name * - Filter by NSFW + * - In list of ids * - Sort? * * TODO Mutations From 05b5a7f598723f30fd5c8a0b783900a1aed6661d Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 23:03:26 -0400 Subject: [PATCH 21/49] Add updates --- .../tachidesk/graphql/queries/UpdatesQuery.kt | 48 +++++++++++++++++++ .../graphql/server/TachideskGraphQLSchema.kt | 4 +- .../tachidesk/graphql/types/UpdatesType.kt | 20 ++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt new file mode 100644 index 00000000..afb5ae72 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.queries + +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.UpdatesType +import suwayomi.tachidesk.manga.model.dataclass.PaginationFactor +import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.MangaTable + +/** + * TODO Queries + * + * TODO Mutations + * - Update the library + * - Update a category + * - Reset updater + * + */ +class UpdatesQuery { + data class UpdatesQueryInput( + val page: Int + ) + + fun updates(input: UpdatesQueryInput): List { + val results = transaction { + ChapterTable.innerJoin(MangaTable) + .select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) } + .orderBy(ChapterTable.fetchedAt to SortOrder.DESC) + .limit(PaginationFactor, (input.page - 1L) * PaginationFactor) + .map { + UpdatesType(it) + } + } + + return results + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 92e0d2a5..af87d63e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -20,6 +20,7 @@ import suwayomi.tachidesk.graphql.queries.ExtensionQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.queries.MetaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery +import suwayomi.tachidesk.graphql.queries.UpdatesQuery import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass import kotlin.reflect.KType @@ -43,7 +44,8 @@ val schema = toSchema( TopLevelObject(CategoryQuery()), TopLevelObject(SourceQuery()), TopLevelObject(ExtensionQuery()), - TopLevelObject(MetaQuery()) + TopLevelObject(MetaQuery()), + TopLevelObject(UpdatesQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt new file mode 100644 index 00000000..2e7d952c --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +package suwayomi.tachidesk.graphql.types + +import org.jetbrains.exposed.sql.ResultRow + +class UpdatesType( + val manga: MangaType, + val chapter: ChapterType +) { + constructor(row: ResultRow) : this( + manga = MangaType(row), + chapter = ChapterType(row) + ) +} From 7debb27374887a5bf6aa65ad55466ce7fedd5da9 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 31 Mar 2023 23:11:18 -0400 Subject: [PATCH 22/49] Might not need a updates query --- .../kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt index afb5ae72..e5742e25 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt @@ -20,6 +20,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable /** * TODO Queries + * - Maybe replace with a chapter query with a sort * * TODO Mutations * - Update the library From 106bda20972d453a35b1aa964d83a6ae5b96703e Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 2 Apr 2023 16:30:03 -0400 Subject: [PATCH 23/49] Proper conversion Scalar for Long to String and back --- .../graphql/server/GraphqlLongAsString.kt | 105 ++++++++++++++++++ .../graphql/server/TachideskGraphQLSchema.kt | 3 +- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt new file mode 100644 index 00000000..a5841ab7 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt @@ -0,0 +1,105 @@ +package suwayomi.tachidesk.graphql.server + +import graphql.GraphQLContext +import graphql.execution.CoercedVariables +import graphql.language.StringValue +import graphql.language.Value +import graphql.scalar.CoercingUtil +import graphql.schema.Coercing +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import graphql.schema.GraphQLScalarType +import java.util.Locale + +val GraphQLLongAsString: GraphQLScalarType = GraphQLScalarType.newScalar() + .name("LongString").description("A 64-bit signed integer as a String").coercing(GraphqlLongAsStringCoercing()).build() + +private class GraphqlLongAsStringCoercing : Coercing { + private fun toStringImpl(input: Any): String { + return input.toString() + } + + private fun parseValueImpl(input: Any, locale: Locale): Long { + if (input !is String) { + throw CoercingParseValueException( + CoercingUtil.i18nMsg( + locale, + "String.unexpectedRawValueType", + CoercingUtil.typeName(input) + ) + ) + } + return input.toLong() + } + + private fun parseLiteralImpl(input: Any, locale: Locale): Long { + if (input !is StringValue) { + throw CoercingParseLiteralException( + CoercingUtil.i18nMsg( + locale, + "Scalar.unexpectedAstType", + "StringValue", + CoercingUtil.typeName(input) + ) + ) + } + return input.value.toLong() + } + + private fun valueToLiteralImpl(input: Any): StringValue { + return StringValue.newStringValue(input.toString()).build() + } + + @Deprecated("") + override fun serialize(dataFetcherResult: Any): String { + return toStringImpl(dataFetcherResult) + } + + @Throws(CoercingSerializeException::class) + override fun serialize( + dataFetcherResult: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): String { + return toStringImpl(dataFetcherResult) + } + + @Deprecated("") + override fun parseValue(input: Any): Long { + return parseValueImpl(input, Locale.getDefault()) + } + + @Throws(CoercingParseValueException::class) + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Long { + return parseValueImpl(input, locale) + } + + @Deprecated("") + override fun parseLiteral(input: Any): Long { + return parseLiteralImpl(input, Locale.getDefault()) + } + + @Throws(CoercingParseLiteralException::class) + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): Long { + return parseLiteralImpl(input, locale) + } + + @Deprecated("") + override fun valueToLiteral(input: Any): Value<*> { + return valueToLiteralImpl(input) + } + + override fun valueToLiteral( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): Value<*> { + return valueToLiteralImpl(input) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index af87d63e..5725a029 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -11,7 +11,6 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks import com.expediagroup.graphql.generator.toSchema -import graphql.Scalars import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery @@ -27,7 +26,7 @@ import kotlin.reflect.KType class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { - Long::class -> Scalars.GraphQLString // encode to string for JS + Long::class -> GraphQLLongAsString // encode to string for JS else -> super.willGenerateGraphQLType(type) } } From d830638ee6e1ada9c4b80f6b9c308d7a42471c03 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 2 Apr 2023 16:38:15 -0400 Subject: [PATCH 24/49] Use actual MangaStatus enum --- ...aphqlLongAsString.kt => TachideskGraphQLLongAsString.kt} | 0 .../kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename server/src/main/kotlin/suwayomi/tachidesk/graphql/server/{GraphqlLongAsString.kt => TachideskGraphQLLongAsString.kt} (100%) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLLongAsString.kt similarity index 100% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/server/GraphqlLongAsString.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLLongAsString.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 87672c51..1cc4c3c1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -28,7 +28,7 @@ class MangaType( val author: String?, val description: String?, val genre: List, - val status: String, + val status: MangaStatus, val inLibrary: Boolean, val inLibraryAt: Long, val realUrl: String?, @@ -46,7 +46,7 @@ class MangaType( row[MangaTable.author], row[MangaTable.description], row[MangaTable.genre].toGenreList(), - MangaStatus.valueOf(row[MangaTable.status]).name, + MangaStatus.valueOf(row[MangaTable.status]), row[MangaTable.inLibrary], row[MangaTable.inLibraryAt], row[MangaTable.realUrl], @@ -65,7 +65,7 @@ class MangaType( dataClass.author, dataClass.description, dataClass.genre, - dataClass.status, + MangaStatus.valueOf(dataClass.status), dataClass.inLibrary, dataClass.inLibraryAt, dataClass.realUrl, From 607919f40f7ed008299ca71792f7171edbb9c895 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 2 Apr 2023 17:33:19 -0400 Subject: [PATCH 25/49] Implement more query parameters --- .../graphql/queries/CategoryQuery.kt | 41 +++++++++++++++-- .../tachidesk/graphql/queries/ChapterQuery.kt | 46 ++++++++++++++++--- .../tachidesk/graphql/queries/MangaQuery.kt | 37 ++++++++++++++- .../tachidesk/graphql/queries/UpdatesQuery.kt | 2 - .../graphql/queries/util/GreaterOrLessThan.kt | 42 +++++++++++++++++ 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index ca646742..e2cd19ab 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -9,6 +9,8 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.CategoryType @@ -17,9 +19,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Sort? - * - Query by name - * - In ID list * - Paged queries * * TODO Mutations @@ -36,9 +35,41 @@ class CategoryQuery { return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) } - fun categories(): List { + enum class CategorySort { + ID, + NAME, + ORDER + } + + data class CategoriesQueryInput( + val sort: CategorySort? = null, + val sortOrder: SortOrder? = null, + val ids: List? = null, + val query: String? = null + ) + + fun categories(input: CategoriesQueryInput? = null): List { val results = transaction { - CategoryTable.selectAll().toList() + val res = CategoryTable.selectAll() + + if (input != null) { + if (input.ids != null) { + res.andWhere { CategoryTable.id inList input.ids } + } + if (!input.query.isNullOrEmpty()) { + res.andWhere { CategoryTable.name like input.query } + } + val orderBy = when (input.sort) { + CategorySort.ID -> CategoryTable.id + CategorySort.NAME -> CategoryTable.name + CategorySort.ORDER, null -> CategoryTable.order + } + res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) + } else { + res.orderBy(CategoryTable.order) + } + + res.toList() } return results.map { CategoryType(it) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index e901c2cc..372cde67 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -9,6 +9,7 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -18,11 +19,7 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Filter by read - * - Filter by bookmarked - * - Filter by downloaded * - Filter by scanlators - * - Sort? Upload date, source order, last read, chapter number * - Get page list? * * TODO Mutations @@ -38,9 +35,23 @@ class ChapterQuery { return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) } + enum class ChapterSort { + SOURCE_ORDER, + NAME, + UPLOAD_DATE, + CHAPTER_NUMBER, + LAST_READ_AT, + FETCHED_AT + } + data class ChapterQueryInput( val ids: List? = null, val mangaIds: List? = null, + val read: Boolean? = null, + val bookmarked: Boolean? = null, + val downloaded: Boolean? = null, + val sort: ChapterSort? = null, + val sortOrder: SortOrder? = null, val page: Int? = null, val count: Int? = null ) @@ -51,15 +62,36 @@ class ChapterQuery { if (input != null) { if (input.mangaIds != null) { - res = res.andWhere { ChapterTable.manga inList input.mangaIds } + res.andWhere { ChapterTable.manga inList input.mangaIds } } if (input.ids != null) { - res = res.andWhere { ChapterTable.id inList input.ids } + res.andWhere { ChapterTable.id inList input.ids } } + if (input.read != null) { + res.andWhere { ChapterTable.isRead eq input.read } + } + if (input.bookmarked != null) { + res.andWhere { ChapterTable.isBookmarked eq input.bookmarked } + } + if (input.downloaded != null) { + res.andWhere { ChapterTable.isDownloaded eq input.downloaded } + } + val orderBy = when (input.sort) { + ChapterSort.SOURCE_ORDER, null -> ChapterTable.sourceOrder + ChapterSort.NAME -> ChapterTable.name + ChapterSort.UPLOAD_DATE -> ChapterTable.date_upload + ChapterSort.CHAPTER_NUMBER -> ChapterTable.chapter_number + ChapterSort.LAST_READ_AT -> ChapterTable.lastReadAt + ChapterSort.FETCHED_AT -> ChapterTable.fetchedAt + } + res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) + if (input.count != null) { val offset = if (input.page == null) 0 else (input.page * input.count).toLong() - res = res.limit(input.count, offset) + res.limit(input.count, offset) } + } else { + res.orderBy(ChapterTable.sourceOrder) } res.toList() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index a6950dc0..028db3f8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -9,10 +9,13 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.queries.util.GreaterOrLessThanLong +import suwayomi.tachidesk.graphql.queries.util.andWhereGreaterOrLessThen import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable @@ -21,7 +24,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries * - Query options(optionally query the title, description, or/and) - * - Sort? * * TODO Mutations * - Favorite @@ -39,9 +41,21 @@ class MangaQuery { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) } + enum class MangaSort { + ID, + TITLE, + IN_LIBRARY_AT, + LAST_FETCHED_AT + } + data class MangaQueryInput( val ids: List? = null, val categoryIds: List? = null, + val sourceIds: List? = null, + val inLibrary: Boolean? = null, + val inLibraryAt: GreaterOrLessThanLong? = null, + val sort: MangaSort? = null, + val sortOrder: SortOrder? = null, val page: Int? = null, val count: Int? = null ) @@ -58,6 +72,27 @@ class MangaQuery { if (input.ids != null) { res.andWhere { MangaTable.id inList input.ids } } + if (input.sourceIds != null) { + res.andWhere { MangaTable.sourceReference inList input.sourceIds } + } + if (input.inLibrary != null) { + res.andWhere { MangaTable.inLibrary eq input.inLibrary } + } + if (input.inLibraryAt != null) { + res.andWhereGreaterOrLessThen( + column = MangaTable.inLibraryAt, + greaterOrLessThan = input.inLibraryAt + ) + } + if (input.sort != null) { + val orderBy = when (input.sort) { + MangaSort.ID -> MangaTable.id + MangaSort.TITLE -> MangaTable.title + MangaSort.IN_LIBRARY_AT -> MangaTable.inLibraryAt + MangaSort.LAST_FETCHED_AT -> MangaTable.lastFetchedAt + } + res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) + } if (input.count != null) { val offset = if (input.page == null) 0 else (input.page * input.count).toLong() res.limit(input.count, offset) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt index e5742e25..df4f50d3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt @@ -8,8 +8,6 @@ package suwayomi.tachidesk.graphql.queries import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt new file mode 100644 index 00000000..7be592fa --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt @@ -0,0 +1,42 @@ +package suwayomi.tachidesk.graphql.queries.util + +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Query +import org.jetbrains.exposed.sql.andWhere + +interface GreaterOrLessThan> { + val value: T + val type: GreaterOrLessThanType +} + +data class GreaterOrLessThanLong( + override val value: Long, + override val type: GreaterOrLessThanType +) : GreaterOrLessThan + +enum class GreaterOrLessThanType { + GREATER_THAN, + GREATER_THAN_OR_EQ, + LESS_THAN, + LESS_THAN_OR_EQ +} + +fun > Query.andWhereGreaterOrLessThen( + column: Column, + greaterOrLessThan: GreaterOrLessThan +) { + when (greaterOrLessThan.type) { + GreaterOrLessThanType.GREATER_THAN -> andWhere { + column greater greaterOrLessThan.value // toValue() + } + GreaterOrLessThanType.GREATER_THAN_OR_EQ -> andWhere { + column greaterEq greaterOrLessThan.value // toValue() + } + GreaterOrLessThanType.LESS_THAN -> andWhere { + column less greaterOrLessThan.value // toValue() + } + GreaterOrLessThanType.LESS_THAN_OR_EQ -> andWhere { + column lessEq greaterOrLessThan.value // toValue() + } + } +} From 52bda2c08051e187584073858e247657592f79e9 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 2 Apr 2023 20:15:09 -0400 Subject: [PATCH 26/49] Start working on graphql paging --- .../graphql/dataLoaders/CategoryDataLoader.kt | 8 ++-- .../graphql/dataLoaders/ChapterDataLoader.kt | 8 ++-- .../graphql/dataLoaders/MangaDataLoader.kt | 8 ++-- .../graphql/dataLoaders/MetaDataLoader.kt | 21 +++++----- .../graphql/dataLoaders/SourceDataLoader.kt | 8 ++-- .../graphql/queries/CategoryQuery.kt | 6 ++- .../tachidesk/graphql/queries/ChapterQuery.kt | 6 ++- .../graphql/queries/ExtensionQuery.kt | 6 ++- .../tachidesk/graphql/queries/MangaQuery.kt | 6 ++- .../tachidesk/graphql/queries/MetaQuery.kt | 6 ++- .../tachidesk/graphql/queries/SourceQuery.kt | 6 ++- .../tachidesk/graphql/queries/UpdatesQuery.kt | 6 ++- .../tachidesk/graphql/types/CategoryType.kt | 41 ++++++++++++++++--- .../tachidesk/graphql/types/ChapterType.kt | 37 +++++++++++++++-- .../tachidesk/graphql/types/DownloadType.kt | 33 ++++++++++++++- .../tachidesk/graphql/types/ExtensionType.kt | 37 +++++++++++++++-- .../tachidesk/graphql/types/MangaType.kt | 41 ++++++++++++++++--- .../tachidesk/graphql/types/MetaType.kt | 35 ++++++++++++++-- .../tachidesk/graphql/types/NodeList.kt | 40 ++++++++++++++++++ .../tachidesk/graphql/types/SourceType.kt | 37 +++++++++++++++-- .../tachidesk/graphql/types/UpdatesType.kt | 33 ++++++++++++++- 21 files changed, 369 insertions(+), 60 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index 09dbc52d..833cb51e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.CategoryNodeList +import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable @@ -32,9 +34,9 @@ class CategoryDataLoader : KotlinDataLoader { } } -class CategoriesForMangaDataLoader : KotlinDataLoader> { +class CategoriesForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoriesForMangaDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -43,7 +45,7 @@ class CategoriesForMangaDataLoader : KotlinDataLoader> { .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } .groupBy { it.first } .mapValues { it.value.map { pair -> pair.second } } - ids.map { itemsByRef[it] ?: emptyList() } + ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 1713df8a..79a8a784 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterNodeList +import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.server.JavalinSetup.future @@ -31,16 +33,16 @@ class ChapterDataLoader : KotlinDataLoader { } } -class ChaptersForMangaDataLoader : KotlinDataLoader> { +class ChaptersForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChaptersForMangaDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } .map { ChapterType(it) } .groupBy { it.mangaId } - ids.map { chaptersByMangaId[it] ?: emptyList() } + ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 3725efae..3bd75c19 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.MangaNodeList +import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable @@ -32,9 +34,9 @@ class MangaDataLoader : KotlinDataLoader { } } -class MangaForCategoryDataLoader : KotlinDataLoader> { +class MangaForCategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaForCategoryDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -42,7 +44,7 @@ class MangaForCategoryDataLoader : KotlinDataLoader> { .map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) } .groupBy { it.first } .mapValues { it.value.map { pair -> pair.second } } - ids.map { itemsByRef[it] ?: emptyList() } + ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 3a61300a..6a7aca0a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -13,7 +13,8 @@ import suwayomi.tachidesk.graphql.types.ChapterMetaItem import suwayomi.tachidesk.graphql.types.GlobalMetaItem import suwayomi.tachidesk.graphql.types.MangaMetaItem import suwayomi.tachidesk.graphql.types.MetaItem -import suwayomi.tachidesk.graphql.types.MetaType +import suwayomi.tachidesk.graphql.types.MetaNodeList +import suwayomi.tachidesk.graphql.types.MetaNodeList.Companion.toNodeList import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.server.JavalinSetup.future @@ -33,46 +34,46 @@ class GlobalMetaDataLoader : KotlinDataLoader { } } -class ChapterMetaDataLoader : KotlinDataLoader { +class ChapterMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterMetaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } .map { ChapterMetaItem(it) } .groupBy { it.ref } - ids.map { metasByRefId[it] ?: emptyList() } + ids.map { (metasByRefId[it] ?: emptyList()).toNodeList() } } } } } -class MangaMetaDataLoader : KotlinDataLoader { +class MangaMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaMetaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } .map { MangaMetaItem(it) } .groupBy { it.ref } - ids.map { metasByRefId[it] ?: emptyList() } + ids.map { (metasByRefId[it] ?: emptyList()).toNodeList() } } } } } -class CategoryMetaDataLoader : KotlinDataLoader { +class CategoryMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryMetaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } .map { CategoryMetaItem(it) } .groupBy { it.ref } - ids.map { metasByRefId[it] ?: emptyList() } + ids.map { (metasByRefId[it] ?: emptyList()).toNodeList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index 031f621e..469e55b9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.SourceNodeList +import suwayomi.tachidesk.graphql.types.SourceNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.MangaTable @@ -63,9 +65,9 @@ class SourceForMangaDataLoader : KotlinDataLoader { } } -class SourcesForExtensionDataLoader : KotlinDataLoader> { +class SourcesForExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "SourcesForExtensionDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -76,7 +78,7 @@ class SourcesForExtensionDataLoader : KotlinDataLoader> .groupBy { it.first } .mapValues { it.value.mapNotNull { pair -> pair.second } } - ids.map { sourcesByExtensionPkg[it] ?: emptyList() } + ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index e2cd19ab..c85f0fa0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -13,6 +13,8 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.CategoryNodeList +import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryTable import java.util.concurrent.CompletableFuture @@ -48,7 +50,7 @@ class CategoryQuery { val query: String? = null ) - fun categories(input: CategoriesQueryInput? = null): List { + fun categories(input: CategoriesQueryInput? = null): CategoryNodeList { val results = transaction { val res = CategoryTable.selectAll() @@ -72,6 +74,6 @@ class CategoryQuery { res.toList() } - return results.map { CategoryType(it) } + return results.map { CategoryType(it) }.toNodeList() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 372cde67..8ef60158 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -13,6 +13,8 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ChapterNodeList +import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import java.util.concurrent.CompletableFuture @@ -56,7 +58,7 @@ class ChapterQuery { val count: Int? = null ) - fun chapters(input: ChapterQueryInput? = null): List { + fun chapters(input: ChapterQueryInput? = null): ChapterNodeList { val results = transaction { var res = ChapterTable.selectAll() @@ -97,6 +99,6 @@ class ChapterQuery { res.toList() } - return results.map { ChapterType(it) } + return results.map { ChapterType(it) }.toNodeList() // todo paged } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt index cd341832..3dfdd202 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -11,6 +11,8 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.ExtensionNodeList +import suwayomi.tachidesk.graphql.types.ExtensionNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.manga.model.table.ExtensionTable import java.util.concurrent.CompletableFuture @@ -37,11 +39,11 @@ class ExtensionQuery { return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) } - fun extensions(): List { + fun extensions(): ExtensionNodeList { val results = transaction { ExtensionTable.selectAll().toList() } - return results.map { ExtensionType(it) } + return results.map { ExtensionType(it) }.toNodeList() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 028db3f8..ea691d5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -16,6 +16,8 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.util.GreaterOrLessThanLong import suwayomi.tachidesk.graphql.queries.util.andWhereGreaterOrLessThen +import suwayomi.tachidesk.graphql.types.MangaNodeList +import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable @@ -60,7 +62,7 @@ class MangaQuery { val count: Int? = null ) - fun mangas(input: MangaQueryInput? = null): List { + fun mangas(input: MangaQueryInput? = null): MangaNodeList { val results = transaction { var res = MangaTable.selectAll() @@ -102,6 +104,6 @@ class MangaQuery { res.toList() } - return results.map { MangaType(it) } + return results.map { MangaType(it) }.toNodeList() // todo paged } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt index f5b2b2cc..53170fa2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.graphql.types.GlobalMetaItem import suwayomi.tachidesk.graphql.types.MetaItem +import suwayomi.tachidesk.graphql.types.MetaNodeList +import suwayomi.tachidesk.graphql.types.MetaNodeList.Companion.toNodeList import java.util.concurrent.CompletableFuture /** @@ -30,11 +32,11 @@ class MetaQuery { return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) } - fun metas(): List { + fun metas(): MetaNodeList { val results = transaction { GlobalMetaTable.selectAll().toList() } - return results.map { GlobalMetaItem(it) } + return results.map { GlobalMetaItem(it) }.toNodeList() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index 939b0523..533883af 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -11,6 +11,8 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.SourceNodeList +import suwayomi.tachidesk.graphql.types.SourceNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.manga.model.table.SourceTable import java.util.concurrent.CompletableFuture @@ -33,11 +35,11 @@ class SourceQuery { return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) } - fun sources(): List { + fun sources(): SourceNodeList { val results = transaction { SourceTable.selectAll().toList().mapNotNull { SourceType(it) } } - return results + return results.toNodeList() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt index df4f50d3..949bb544 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt @@ -11,6 +11,8 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.UpdatesNodeList +import suwayomi.tachidesk.graphql.types.UpdatesNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.UpdatesType import suwayomi.tachidesk.manga.model.dataclass.PaginationFactor import suwayomi.tachidesk.manga.model.table.ChapterTable @@ -31,7 +33,7 @@ class UpdatesQuery { val page: Int ) - fun updates(input: UpdatesQueryInput): List { + fun updates(input: UpdatesQueryInput): UpdatesNodeList { val results = transaction { ChapterTable.innerJoin(MangaTable) .select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) } @@ -42,6 +44,6 @@ class UpdatesQuery { } } - return results + return results.toNodeList() // todo paged } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt index ada7129e..727c475a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt @@ -18,7 +18,7 @@ class CategoryType( val order: Int, val name: String, val default: Boolean -) { +) : Node { constructor(row: ResultRow) : this( row[CategoryTable.id].value, row[CategoryTable.order], @@ -26,11 +26,42 @@ class CategoryType( row[CategoryTable.isDefault] ) - fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { - return dataFetchingEnvironment.getValueFromDataLoader>("MangaForCategoryDataLoader", id) + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaForCategoryDataLoader", id) } - fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("CategoryMetaDataLoader", id) + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("CategoryMetaDataLoader", id) + } +} + +data class CategoryNodeList( + override val nodes: List, + override val edges: CategoryEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class CategoryEdges( + override val cursor: Cursor, + override val node: CategoryType? + ) : Edges() + + companion object { + fun List.toNodeList(): CategoryNodeList { + return CategoryNodeList( + nodes = this, + edges = CategoryEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 1d8f0e2c..01276a44 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -31,7 +31,7 @@ class ChapterType( val isDownloaded: Boolean, val pageCount: Int // val chapterCount: Int?, -) { +) : Node { constructor(row: ResultRow) : this( row[ChapterTable.id].value, row[ChapterTable.url], @@ -73,7 +73,38 @@ class ChapterType( return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", mangaId) } - fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("ChapterMetaDataLoader", id) + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ChapterMetaDataLoader", id) + } +} + +data class ChapterNodeList( + override val nodes: List, + override val edges: ChapterEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class ChapterEdges( + override val cursor: Cursor, + override val node: ChapterType? + ) : Edges() + + companion object { + fun List.toNodeList(): ChapterNodeList { + return ChapterNodeList( + nodes = this, + edges = ChapterEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt index 9e58d813..e65adfc6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt @@ -24,7 +24,7 @@ class DownloadType( var mangaDataClass: MangaDataClass, @GraphQLIgnore var chapterDataClass: ChapterDataClass -) { +) : Node { constructor(downloadChapter: DownloadChapter) : this( downloadChapter.chapter.id, downloadChapter.chapterIndex, @@ -44,3 +44,34 @@ class DownloadType( return ChapterType(chapterDataClass) } } + +data class DownloadNodeList( + override val nodes: List, + override val edges: DownloadEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class DownloadEdges( + override val cursor: Cursor, + override val node: DownloadType? + ) : Edges() + + companion object { + fun List.toNodeList(): DownloadNodeList { + return DownloadNodeList( + nodes = this, + edges = DownloadEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt index 93bf09b1..c725687d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt @@ -27,7 +27,7 @@ class ExtensionType( val installed: Boolean, val hasUpdate: Boolean, val obsolete: Boolean -) { +) : Node { constructor(row: ResultRow) : this( apkName = row[ExtensionTable.apkName], iconUrl = row[ExtensionTable.iconUrl], @@ -42,7 +42,38 @@ class ExtensionType( obsolete = row[ExtensionTable.isObsolete] ) - fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { - return dataFetchingEnvironment.getValueFromDataLoader>("SourcesForExtensionDataLoader", pkgName) + fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourcesForExtensionDataLoader", pkgName) + } +} + +data class ExtensionNodeList( + override val nodes: List, + override val edges: ExtensionEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class ExtensionEdges( + override val cursor: Cursor, + override val node: ExtensionType? + ) : Edges() + + companion object { + fun List.toNodeList(): ExtensionNodeList { + return ExtensionNodeList( + nodes = this, + edges = ExtensionEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 1cc4c3c1..2b20a301 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -34,7 +34,7 @@ class MangaType( val realUrl: String?, var lastFetchedAt: Long?, var chaptersLastFetchedAt: Long? -) { +) : Node { constructor(row: ResultRow) : this( row[MangaTable.id].value, row[MangaTable.sourceReference], @@ -88,15 +88,46 @@ class MangaType( return Instant.now().epochSecond.minus(chaptersLastFetchedAt!!) } - fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("MangaMetaDataLoader", id) + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaMetaDataLoader", id) } - fun categories(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { - return dataFetchingEnvironment.getValueFromDataLoader>("CategoriesForMangaDataLoader", id) + fun categories(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("CategoriesForMangaDataLoader", id) } fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("SourceForMangaDataLoader", id) } } + +data class MangaNodeList( + override val nodes: List, + override val edges: MangaEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class MangaEdges( + override val cursor: Cursor, + override val node: MangaType? + ) : Edges() + + companion object { + fun List.toNodeList(): MangaNodeList { + return MangaNodeList( + nodes = this, + edges = MangaEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index 2283104e..fe85a61a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -7,14 +7,12 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable -typealias MetaType = List - open class MetaItem( val key: String, val value: String, @GraphQLIgnore val ref: Int? -) +) : Node class ChapterMetaItem( private val row: ResultRow @@ -31,3 +29,34 @@ class CategoryMetaItem( class GlobalMetaItem( private val row: ResultRow ) : MetaItem(row[GlobalMetaTable.key], row[GlobalMetaTable.value], null) + +data class MetaNodeList( + override val nodes: List, + override val edges: MetaEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class MetaEdges( + override val cursor: Cursor, + override val node: MetaItem? + ) : Edges() + + companion object { + fun List.toNodeList(): MetaNodeList { + return MetaNodeList( + nodes = this, + edges = MetaEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt new file mode 100644 index 00000000..0d0ced8f --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt @@ -0,0 +1,40 @@ +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription + +interface Node + +typealias Cursor = Int + +abstract class NodeList { + @GraphQLDescription("A list of [T] objects.") + abstract val nodes: List + + @GraphQLDescription("A list of edges which contains the [T] and cursor to aid in pagination.") + abstract val edges: Edges + + @GraphQLDescription("Information to aid in pagination.") + abstract val pageInfo: PageInfo + + @GraphQLDescription("The count of all nodes you could get from the connection.") + abstract val totalCount: Int +} + +data class PageInfo( + @GraphQLDescription("When paginating forwards, are there more items?") + val hasNextPage: Boolean, + @GraphQLDescription("When paginating backwards, are there more items?") + val hasPreviousPage: Boolean, + @GraphQLDescription("When paginating backwards, the cursor to continue.") + val startCursor: Cursor, + @GraphQLDescription("When paginating forwards, the cursor to continue.") + val endCursor: Cursor +) + +abstract class Edges { + @GraphQLDescription("A cursor for use in pagination.") + abstract val cursor: Cursor + + @GraphQLDescription("The [T] at the end of the edge.") + abstract val node: Node? +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index 27d2db96..2c9e19aa 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -29,7 +29,7 @@ class SourceType( val isConfigurable: Boolean, val isNsfw: Boolean, val displayName: String -) { +) : Node { constructor(source: SourceDataClass) : this( id = source.id.toLong(), name = source.name, @@ -52,8 +52,8 @@ class SourceType( displayName = catalogueSource.toString() ) - fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { - return dataFetchingEnvironment.getValueFromDataLoader>("MangaForSourceDataLoader", id) + fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaForSourceDataLoader", id) } fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { @@ -75,3 +75,34 @@ fun SourceType(row: ResultRow): SourceType? { return SourceType(row, sourceExtension, catalogueSource) } + +data class SourceNodeList( + override val nodes: List, + override val edges: SourceEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class SourceEdges( + override val cursor: Cursor, + override val node: SourceType? + ) : Edges() + + companion object { + fun List.toNodeList(): SourceNodeList { + return SourceNodeList( + nodes = this, + edges = SourceEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt index 2e7d952c..bacfd014 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt @@ -12,9 +12,40 @@ import org.jetbrains.exposed.sql.ResultRow class UpdatesType( val manga: MangaType, val chapter: ChapterType -) { +) : Node { constructor(row: ResultRow) : this( manga = MangaType(row), chapter = ChapterType(row) ) } + +data class UpdatesNodeList( + override val nodes: List, + override val edges: UpdatesEdges, + override val pageInfo: PageInfo, + override val totalCount: Int +) : NodeList() { + data class UpdatesEdges( + override val cursor: Cursor, + override val node: UpdatesType? + ) : Edges() + + companion object { + fun List.toNodeList(): UpdatesNodeList { + return UpdatesNodeList( + nodes = this, + edges = UpdatesEdges( + cursor = lastIndex, + node = lastOrNull() + ), + pageInfo = PageInfo( + hasNextPage = false, + hasPreviousPage = false, + startCursor = 0, + endCursor = lastIndex + ), + totalCount = size + ) + } + } +} From e8c2bad18796f5c329e41a99cd650415bb2260e4 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 2 Apr 2023 20:39:56 -0400 Subject: [PATCH 27/49] Handle missing objects in graphql --- .../graphql/dataLoaders/CategoryDataLoader.kt | 8 +++-- .../graphql/dataLoaders/ChapterDataLoader.kt | 8 +++-- .../dataLoaders/ExtensionDataLoader.kt | 30 ++++++++++++++----- .../graphql/dataLoaders/MangaDataLoader.kt | 8 +++-- .../graphql/dataLoaders/SourceDataLoader.kt | 7 +++-- .../graphql/queries/CategoryQuery.kt | 4 +-- .../tachidesk/graphql/queries/ChapterQuery.kt | 4 +-- .../graphql/queries/ExtensionQuery.kt | 4 +-- .../tachidesk/graphql/queries/MangaQuery.kt | 4 +-- 9 files changed, 50 insertions(+), 27 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index 833cb51e..a4c5df83 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -21,14 +21,16 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.server.JavalinSetup.future -class CategoryDataLoader : KotlinDataLoader { +class CategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) - CategoryTable.select { CategoryTable.id inList ids } + val categories = CategoryTable.select { CategoryTable.id inList ids } .map { CategoryType(it) } + .associateBy { it.id } + ids.map { categories[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 79a8a784..89a2af0a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -20,14 +20,16 @@ import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.server.JavalinSetup.future -class ChapterDataLoader : KotlinDataLoader { +class ChapterDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) - ChapterTable.select { ChapterTable.id inList ids } + val chapters = ChapterTable.select { ChapterTable.id inList ids } .map { ChapterType(it) } + .associateBy { it.id } + ids.map { chapters[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt index 034ebde9..7e8c0763 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt @@ -19,28 +19,44 @@ import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.server.JavalinSetup.future -class ExtensionDataLoader : KotlinDataLoader { +class ExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) - ExtensionTable.select { ExtensionTable.pkgName inList ids } + val extensions = ExtensionTable.select { ExtensionTable.pkgName inList ids } .map { ExtensionType(it) } + .associateBy { it.pkgName } + ids.map { extensions[it] } } } } } -class ExtensionForSourceDataLoader : KotlinDataLoader { +class ExtensionForSourceDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionForSourceDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) - ExtensionTable.innerJoin(SourceTable) + val extensions = ExtensionTable.innerJoin(SourceTable) .select { SourceTable.id inList ids } - .map { ExtensionType(it) } + .toList() + .map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) } + .let { triples -> + val sources = buildMap { + triples.forEach { + if (!containsKey(it.second)) { + put(it.second, ExtensionType(it.third)) + } + } + } + triples.associate { + it.first to sources[it.second] + } + } + ids.map { extensions[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 3bd75c19..8788739d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -21,14 +21,16 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.server.JavalinSetup.future -class MangaDataLoader : KotlinDataLoader { +class MangaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) - MangaTable.select { MangaTable.id inList ids } + val manga = MangaTable.select { MangaTable.id inList ids } .map { MangaType(it) } + .associateBy { it.id } + ids.map { manga[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index 469e55b9..18eacbcf 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -28,9 +28,10 @@ class SourceDataLoader : KotlinDataLoader { future { transaction { addLogger(Slf4jSqlDebugLogger) - SourceTable.select { SourceTable.id inList ids }.map { - SourceType(it) - } + val source = SourceTable.select { SourceTable.id inList ids } + .mapNotNull { SourceType(it) } + .associateBy { it.id } + ids.map { source[it] } } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index c85f0fa0..7e47dc20 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -33,8 +33,8 @@ import java.util.concurrent.CompletableFuture * - Delete meta */ class CategoryQuery { - fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) + fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) } enum class CategorySort { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 8ef60158..1363d99c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -33,8 +33,8 @@ import java.util.concurrent.CompletableFuture * - Delete download */ class ChapterQuery { - fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) + fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) } enum class ChapterSort { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt index 3dfdd202..ff1b0f8a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -35,8 +35,8 @@ import java.util.concurrent.CompletableFuture * - Check for updates (global mutation?) */ class ExtensionQuery { - fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) + fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) } fun extensions(): ExtensionNodeList { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index ea691d5d..b9158a74 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -39,8 +39,8 @@ import java.util.concurrent.CompletableFuture * - Delete meta */ class MangaQuery { - fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { - return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) + fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) } enum class MangaSort { From a6dddf311c4c6f89794305793983b02905279a74 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Mon, 3 Apr 2023 22:04:46 -0400 Subject: [PATCH 28/49] Basically finish MangaQuery, only paging left --- .../tachidesk/graphql/queries/MangaQuery.kt | 173 ++++++--- .../graphql/queries/filter/Filter.kt | 329 ++++++++++++++++++ .../graphql/queries/util/GreaterOrLessThan.kt | 42 --- 3 files changed, 455 insertions(+), 89 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index b9158a74..de53dd70 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -9,23 +9,34 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction -import suwayomi.tachidesk.graphql.queries.util.GreaterOrLessThanLong -import suwayomi.tachidesk.graphql.queries.util.andWhereGreaterOrLessThen +import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.IntFilter +import suwayomi.tachidesk.graphql.queries.filter.LongFilter +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString +import suwayomi.tachidesk.graphql.queries.filter.getOp import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable +import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Query options(optionally query the title, description, or/and) * * TODO Mutations * - Favorite @@ -43,62 +54,130 @@ class MangaQuery { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) } - enum class MangaSort { + enum class MangaOrderBy { ID, TITLE, IN_LIBRARY_AT, LAST_FETCHED_AT } - data class MangaQueryInput( - val ids: List? = null, - val categoryIds: List? = null, - val sourceIds: List? = null, + data class MangaCondition( + val id: Int? = null, + val sourceId: Long? = null, + val url: String? = null, + val title: String? = null, + val thumbnailUrl: String? = null, + val initialized: Boolean? = null, + val artist: String? = null, + val author: String? = null, + val description: String? = null, + val genre: List? = null, + val status: MangaStatus? = null, val inLibrary: Boolean? = null, - val inLibraryAt: GreaterOrLessThanLong? = null, - val sort: MangaSort? = null, - val sortOrder: SortOrder? = null, - val page: Int? = null, - val count: Int? = null - ) + val inLibraryAt: Long? = null, + val realUrl: String? = null, + var lastFetchedAt: Long? = null, + var chaptersLastFetchedAt: Long? = null + ) { + fun getOp(): Op? { + val opAnd = OpAnd() + fun eq(value: T?, column: Column) = opAnd.andWhere(value) { column eq it } + fun > eq(value: T?, column: Column>) = opAnd.andWhere(value) { column eq it } + eq(id, MangaTable.id) + eq(sourceId, MangaTable.sourceReference) + eq(url, MangaTable.url) + eq(title, MangaTable.title) + eq(thumbnailUrl, MangaTable.thumbnail_url) + eq(initialized, MangaTable.initialized) + eq(artist, MangaTable.artist) + eq(author, MangaTable.author) + eq(description, MangaTable.description) + eq(genre?.joinToString(), MangaTable.genre) + eq(status?.value, MangaTable.status) + eq(inLibrary, MangaTable.inLibrary) + eq(inLibraryAt, MangaTable.inLibraryAt) + eq(realUrl, MangaTable.realUrl) + eq(lastFetchedAt, MangaTable.lastFetchedAt) + eq(chaptersLastFetchedAt, MangaTable.chaptersLastFetchedAt) - fun mangas(input: MangaQueryInput? = null): MangaNodeList { + return opAnd.op + } + } + + data class MangaFilter( + val id: IntFilter? = null, + val sourceId: LongFilter? = null, + val url: StringFilter? = null, + val title: StringFilter? = null, + val thumbnailUrl: StringFilter? = null, + val initialized: BooleanFilter? = null, + val artist: StringFilter? = null, + val author: StringFilter? = null, + val description: StringFilter? = null, + // val genre: List? = null, // todo + // val status: MangaStatus? = null, // todo + val inLibrary: BooleanFilter? = null, + val inLibraryAt: LongFilter? = null, + val realUrl: StringFilter? = null, + var lastFetchedAt: LongFilter? = null, + var chaptersLastFetchedAt: LongFilter? = null, + val category: IntFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: MangaFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareEntity(MangaTable.id, id), + andFilterWithCompare(MangaTable.sourceReference, sourceId), + andFilterWithCompareString(MangaTable.url, url), + andFilterWithCompareString(MangaTable.title, title), + andFilterWithCompareString(MangaTable.thumbnail_url, thumbnailUrl), + andFilterWithCompare(MangaTable.initialized, initialized), + andFilterWithCompareString(MangaTable.artist, artist), + andFilterWithCompareString(MangaTable.author, author), + andFilterWithCompareString(MangaTable.description, description), + andFilterWithCompare(MangaTable.inLibrary, inLibrary), + andFilterWithCompare(MangaTable.inLibraryAt, inLibraryAt), + andFilterWithCompareString(MangaTable.realUrl, realUrl), + andFilterWithCompare(MangaTable.inLibraryAt, lastFetchedAt), + andFilterWithCompare(MangaTable.inLibraryAt, chaptersLastFetchedAt) + ) + } + + fun getCategoryOp() = andFilterWithCompareEntity(CategoryMangaTable.category, category) + } + + fun mangas( + condition: MangaCondition? = null, + filter: MangaFilter? = null, + orderBy: MangaOrderBy? = null, + orderByType: SortOrder? = null + ): MangaNodeList { val results = transaction { var res = MangaTable.selectAll() - if (input != null) { - if (input.categoryIds != null) { - res = MangaTable.innerJoin(CategoryMangaTable) - .select { CategoryMangaTable.category inList input.categoryIds } - } - if (input.ids != null) { - res.andWhere { MangaTable.id inList input.ids } - } - if (input.sourceIds != null) { - res.andWhere { MangaTable.sourceReference inList input.sourceIds } - } - if (input.inLibrary != null) { - res.andWhere { MangaTable.inLibrary eq input.inLibrary } - } - if (input.inLibraryAt != null) { - res.andWhereGreaterOrLessThen( - column = MangaTable.inLibraryAt, - greaterOrLessThan = input.inLibraryAt - ) - } - if (input.sort != null) { - val orderBy = when (input.sort) { - MangaSort.ID -> MangaTable.id - MangaSort.TITLE -> MangaTable.title - MangaSort.IN_LIBRARY_AT -> MangaTable.inLibraryAt - MangaSort.LAST_FETCHED_AT -> MangaTable.lastFetchedAt - } - res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) - } - if (input.count != null) { - val offset = if (input.page == null) 0 else (input.page * input.count).toLong() - res.limit(input.count, offset) + val categoryOp = filter?.getCategoryOp() + if (categoryOp != null) { + res = MangaTable.innerJoin(CategoryMangaTable) + .select { categoryOp } + } + val conditionOp = condition?.getOp() + if (conditionOp != null) { + res.andWhere { conditionOp } + } + val filterOp = filter?.getOp() + if (filterOp != null) { + res.andWhere { filterOp } + } + if (orderBy != null) { + val orderByColumn = when (orderBy) { + MangaOrderBy.ID -> MangaTable.id + MangaOrderBy.TITLE -> MangaTable.title + MangaOrderBy.IN_LIBRARY_AT -> MangaTable.inLibraryAt + MangaOrderBy.LAST_FETCHED_AT -> MangaTable.lastFetchedAt } + res.orderBy(orderByColumn, order = orderByType ?: SortOrder.ASC) } res.toList() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt new file mode 100644 index 00000000..9f75f98e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -0,0 +1,329 @@ +package suwayomi.tachidesk.graphql.queries.filter + +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.ComparisonOp +import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.ExpressionWithColumnType +import org.jetbrains.exposed.sql.LikeEscapeOp +import org.jetbrains.exposed.sql.LikePattern +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.QueryBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.not +import org.jetbrains.exposed.sql.or +import org.jetbrains.exposed.sql.stringParam +import org.jetbrains.exposed.sql.upperCase + +class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, val escapeChar: Char?) : ComparisonOp(expr1, expr2, if (like) "ILIKE" else "NOT ILIKE") { + override fun toQueryBuilder(queryBuilder: QueryBuilder) { + super.toQueryBuilder(queryBuilder) + if (escapeChar != null) { + with(queryBuilder) { + +" ESCAPE " + +stringParam(escapeChar.toString()) + } + } + } + + companion object { + fun iLike(expression: Expression, pattern: String): LikeEscapeOp = iLike(expression, LikePattern(pattern)) + fun iNotLike(expression: Expression, pattern: String): LikeEscapeOp = iNotLike(expression, LikePattern(pattern)) + fun iLike(expression: Expression, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar) + fun iNotLike(expression: Expression, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), false, pattern.escapeChar) + } +} + +class DistinctFromOp(expr1: Expression<*>, expr2: Expression<*>, not: Boolean) : ComparisonOp(expr1, expr2, if (not) "IS NOT DISTINCT FROM" else "IS DISTINCT FROM") { + companion object { + fun distinctFrom(expression: ExpressionWithColumnType, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + false + ) + fun notDistinctFrom(expression: ExpressionWithColumnType, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + true + ) + fun > distinctFrom(expression: ExpressionWithColumnType>, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + false + ) + fun > notDistinctFrom(expression: ExpressionWithColumnType>, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + true + ) + } +} + +interface Filter> { + val and: List? + val or: List? + val not: T? + + fun getOpList(): List> +} + +interface ScalarFilter { + val isNull: Boolean? + val equalTo: T? + val notEqualTo: T? + val distinctFrom: T? + val notDistinctFrom: T? + val `in`: List? + val notIn: List? +} + +interface ComparableScalarFilter> : ScalarFilter { + val lessThan: T? + val lessThanOrEqualTo: T? + val greaterThan: T? + val greaterThanOrEqualTo: T? +} + +data class LongFilter( + override val isNull: Boolean? = null, + override val equalTo: Long? = null, + override val notEqualTo: Long? = null, + override val distinctFrom: Long? = null, + override val notDistinctFrom: Long? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: Long? = null, + override val lessThanOrEqualTo: Long? = null, + override val greaterThan: Long? = null, + override val greaterThanOrEqualTo: Long? = null +) : ComparableScalarFilter + +data class BooleanFilter( + override val isNull: Boolean? = null, + override val equalTo: Boolean? = null, + override val notEqualTo: Boolean? = null, + override val distinctFrom: Boolean? = null, + override val notDistinctFrom: Boolean? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: Boolean? = null, + override val lessThanOrEqualTo: Boolean? = null, + override val greaterThan: Boolean? = null, + override val greaterThanOrEqualTo: Boolean? = null +) : ComparableScalarFilter + +data class IntFilter( + override val isNull: Boolean? = null, + override val equalTo: Int? = null, + override val notEqualTo: Int? = null, + override val distinctFrom: Int? = null, + override val notDistinctFrom: Int? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: Int? = null, + override val lessThanOrEqualTo: Int? = null, + override val greaterThan: Int? = null, + override val greaterThanOrEqualTo: Int? = null +) : ComparableScalarFilter + +data class StringFilter( + override val isNull: Boolean? = null, + override val equalTo: String? = null, + override val notEqualTo: String? = null, + override val distinctFrom: String? = null, + override val notDistinctFrom: String? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: String? = null, + override val lessThanOrEqualTo: String? = null, + override val greaterThan: String? = null, + override val greaterThanOrEqualTo: String? = null, + val includes: String? = null, + val notIncludes: String? = null, + val includesInsensitive: String? = null, + val notIncludesInsensitive: String? = null, + val startsWith: String? = null, + val notStartsWith: String? = null, + val startsWithInsensitive: String? = null, + val notStartsWithInsensitive: String? = null, + val endsWith: String? = null, + val notEndsWith: String? = null, + val endsWithInsensitive: String? = null, + val notEndsWithInsensitive: String? = null, + val like: String? = null, + val notLike: String? = null, + val likeInsensitive: String? = null, + val notLikeInsensitive: String? = null, + val distinctFromInsensitive: String? = null, + val notDistinctFromInsensitive: String? = null, + val inInsensitive: List? = null, + val notInInsensitive: List? = null, + val lessThanInsensitive: String? = null, + val lessThanOrEqualToInsensitive: String? = null, + val greaterThanInsensitive: String? = null, + val greaterThanOrEqualToInsensitive: String? = null +) : ComparableScalarFilter + +fun andFilterWithCompareString( + column: Column, + filter: StringFilter? +): Op? { + filter ?: return null + val opAnd = OpAnd() + opAnd.andWhere(filter.includes) { column like "%$it%" } + opAnd.andWhere(filter.notIncludes) { column notLike "%$it%" } + opAnd.andWhere(filter.includesInsensitive) { ILikeEscapeOp.iLike(column, "%$it%") } + opAnd.andWhere(filter.notIncludesInsensitive) { ILikeEscapeOp.iNotLike(column, "%$it%") } + + opAnd.andWhere(filter.startsWith) { column like "$it%" } + opAnd.andWhere(filter.notStartsWith) { column notLike "$it%" } + opAnd.andWhere(filter.startsWithInsensitive) { ILikeEscapeOp.iLike(column, "$it%") } + opAnd.andWhere(filter.notStartsWithInsensitive) { ILikeEscapeOp.iNotLike(column, "$it%") } + + opAnd.andWhere(filter.endsWith) { column like "%$it" } + opAnd.andWhere(filter.notEndsWith) { column notLike "%$it" } + opAnd.andWhere(filter.endsWithInsensitive) { ILikeEscapeOp.iLike(column, "%$it") } + opAnd.andWhere(filter.notEndsWithInsensitive) { ILikeEscapeOp.iNotLike(column, "%$it") } + + opAnd.andWhere(filter.like) { column like it } + opAnd.andWhere(filter.notLike) { column notLike it } + opAnd.andWhere(filter.likeInsensitive) { ILikeEscapeOp.iLike(column, it) } + opAnd.andWhere(filter.notLikeInsensitive) { ILikeEscapeOp.iNotLike(column, it) } + + opAnd.andWhere(filter.distinctFromInsensitive) { DistinctFromOp.distinctFrom(column.upperCase(), it.uppercase() as T) } + opAnd.andWhere(filter.notDistinctFromInsensitive) { DistinctFromOp.notDistinctFrom(column.upperCase(), it.uppercase() as T) } + + opAnd.andWhere(filter.inInsensitive) { column.upperCase() inList (it.map { it.uppercase() } as List) } + opAnd.andWhere(filter.notInInsensitive) { column.upperCase() notInList (it.map { it.uppercase() } as List) } + + opAnd.andWhere(filter.lessThanInsensitive) { column.upperCase() less it.uppercase() } + opAnd.andWhere(filter.lessThanOrEqualToInsensitive) { column.upperCase() lessEq it.uppercase() } + opAnd.andWhere(filter.greaterThanInsensitive) { column.upperCase() greater it.uppercase() } + opAnd.andWhere(filter.greaterThanOrEqualToInsensitive) { column.upperCase() greaterEq it.uppercase() } + + return opAnd.op +} + +class OpAnd(var op: Op? = null) { + fun andWhere(value: T?, andPart: SqlExpressionBuilder.(T) -> Op) { + value ?: return + val expr = Op.build { andPart(value) } + op = if (op == null) expr else (op!! and expr) + } +} + +fun > andFilterWithCompare( + column: Column, + filter: ComparableScalarFilter? +): Op? { + filter ?: return null + val opAnd = OpAnd(andFilter(column, filter)) + + opAnd.andWhere(filter.lessThan) { column less it } + opAnd.andWhere(filter.lessThanOrEqualTo) { column lessEq it } + opAnd.andWhere(filter.greaterThan) { column greater it } + opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it } + + return opAnd.op +} + +fun > andFilterWithCompareEntity( + column: Column>, + filter: ComparableScalarFilter? +): Op? { + filter ?: return null + val opAnd = OpAnd(andFilterEntity(column, filter)) + + opAnd.andWhere(filter.lessThan) { column less it } + opAnd.andWhere(filter.lessThanOrEqualTo) { column lessEq it } + opAnd.andWhere(filter.greaterThan) { column greater it } + opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it } + + return opAnd.op +} + +fun > andFilter( + column: Column, + filter: ScalarFilter? +): Op? { + filter ?: return null + val opAnd = OpAnd() + + opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() } + opAnd.andWhere(filter.equalTo) { column eq it } + opAnd.andWhere(filter.notEqualTo) { column neq it } + opAnd.andWhere(filter.distinctFrom) { DistinctFromOp.distinctFrom(column, it) } + opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) } + if (!filter.`in`.isNullOrEmpty()) { + opAnd.andWhere(filter.`in`) { column inList it } + } + if (!filter.notIn.isNullOrEmpty()) { + opAnd.andWhere(filter.notIn) { column notInList it } + } + return opAnd.op +} + +fun > andFilterEntity( + column: Column>, + filter: ScalarFilter? +): Op? { + filter ?: return null + val opAnd = OpAnd() + + opAnd.andWhere(filter.isNull) { if (filter.isNull!!) column.isNull() else column.isNotNull() } + opAnd.andWhere(filter.equalTo) { column eq filter.equalTo!! } + opAnd.andWhere(filter.notEqualTo) { column neq filter.notEqualTo!! } + opAnd.andWhere(filter.distinctFrom) { DistinctFromOp.distinctFrom(column, it) } + opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) } + if (!filter.`in`.isNullOrEmpty()) { + opAnd.andWhere(filter.`in`) { column inList filter.`in`!! } + } + if (!filter.notIn.isNullOrEmpty()) { + opAnd.andWhere(filter.notIn) { column notInList filter.notIn!! } + } + return opAnd.op +} + +fun > Filter.getOp(): Op? { + var op: Op? = null + fun newOp( + otherOp: Op?, + operator: (Op, Op) -> Op + ) { + when { + op == null && otherOp == null -> Unit + op == null && otherOp != null -> op = otherOp + op != null && otherOp == null -> Unit + op != null && otherOp != null -> op = operator(op!!, otherOp) + } + } + fun andOp(andOp: Op?) { + newOp(andOp, Op::and) + } + fun orOp(orOp: Op?) { + newOp(orOp, Op::or) + } + getOpList().forEach { + andOp(it) + } + and?.forEach { + andOp(it.getOp()) + } + or?.forEach { + orOp(it.getOp()) + } + if (not != null) { + andOp(not!!.getOp()?.let(::not)) + } + return op +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt deleted file mode 100644 index 7be592fa..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt +++ /dev/null @@ -1,42 +0,0 @@ -package suwayomi.tachidesk.graphql.queries.util - -import org.jetbrains.exposed.sql.Column -import org.jetbrains.exposed.sql.Query -import org.jetbrains.exposed.sql.andWhere - -interface GreaterOrLessThan> { - val value: T - val type: GreaterOrLessThanType -} - -data class GreaterOrLessThanLong( - override val value: Long, - override val type: GreaterOrLessThanType -) : GreaterOrLessThan - -enum class GreaterOrLessThanType { - GREATER_THAN, - GREATER_THAN_OR_EQ, - LESS_THAN, - LESS_THAN_OR_EQ -} - -fun > Query.andWhereGreaterOrLessThen( - column: Column, - greaterOrLessThan: GreaterOrLessThan -) { - when (greaterOrLessThan.type) { - GreaterOrLessThanType.GREATER_THAN -> andWhere { - column greater greaterOrLessThan.value // toValue() - } - GreaterOrLessThanType.GREATER_THAN_OR_EQ -> andWhere { - column greaterEq greaterOrLessThan.value // toValue() - } - GreaterOrLessThanType.LESS_THAN -> andWhere { - column less greaterOrLessThan.value // toValue() - } - GreaterOrLessThanType.LESS_THAN_OR_EQ -> andWhere { - column lessEq greaterOrLessThan.value // toValue() - } - } -} From 1ed9bef2a1ede5646ca1829205af0e0e6a027e64 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Mon, 3 Apr 2023 22:07:10 -0400 Subject: [PATCH 29/49] Fix the playground explorer and add a updated default query --- .../main/resources/graphql-playground.html | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/main/resources/graphql-playground.html b/server/src/main/resources/graphql-playground.html index 76407218..bed46d95 100644 --- a/server/src/main/resources/graphql-playground.html +++ b/server/src/main/resources/graphql-playground.html @@ -40,6 +40,15 @@ --> + @@ -60,7 +69,17 @@ function GraphiQLWithExplorer() { const [query, setQuery] = React.useState( - 'query AllCategories {\n categories {\n manga {\n title\n }\n }\n}', + 'query AllCategories {\n' + + ' categories {\n' + + ' nodes {\n' + + ' manga {\n' + + ' nodes {\n' + + ' title\n' + + ' }\n' + + ' }\n' + + ' }\n' + + ' }\n' + + '}', ); const explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ query: query, From 17877e0f17f461ab0dffe606934ceb823f12bde7 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Mon, 3 Apr 2023 22:12:38 -0400 Subject: [PATCH 30/49] Fix case insensitive --- .../suwayomi/tachidesk/graphql/queries/filter/Filter.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt index 9f75f98e..995377a6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -5,7 +5,6 @@ import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.ComparisonOp import org.jetbrains.exposed.sql.Expression import org.jetbrains.exposed.sql.ExpressionWithColumnType -import org.jetbrains.exposed.sql.LikeEscapeOp import org.jetbrains.exposed.sql.LikePattern import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.QueryBuilder @@ -29,10 +28,10 @@ class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, v } companion object { - fun iLike(expression: Expression, pattern: String): LikeEscapeOp = iLike(expression, LikePattern(pattern)) - fun iNotLike(expression: Expression, pattern: String): LikeEscapeOp = iNotLike(expression, LikePattern(pattern)) - fun iLike(expression: Expression, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar) - fun iNotLike(expression: Expression, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), false, pattern.escapeChar) + fun iLike(expression: Expression, pattern: String): ILikeEscapeOp = iLike(expression, LikePattern(pattern)) + fun iNotLike(expression: Expression, pattern: String): ILikeEscapeOp = iNotLike(expression, LikePattern(pattern)) + fun iLike(expression: Expression, pattern: LikePattern): ILikeEscapeOp = ILikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar) + fun iNotLike(expression: Expression, pattern: LikePattern): ILikeEscapeOp = ILikeEscapeOp(expression, stringParam(pattern.pattern), false, pattern.escapeChar) } } From a589049cc7f5357cb41ead6eb79186c6ad00cec0 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Tue, 4 Apr 2023 21:01:43 -0400 Subject: [PATCH 31/49] Move things around and introduce Cursor type --- .../graphql/queries/filter/Filter.kt | 1 + .../graphql/server/TachideskGraphQLSchema.kt | 4 + .../graphql/server/primitives/Cursor.kt | 119 ++++++++++++++++++ .../LongAsString.kt} | 2 +- .../{types => server/primitives}/NodeList.kt | 6 +- .../tachidesk/graphql/types/CategoryType.kt | 15 ++- .../tachidesk/graphql/types/ChapterType.kt | 15 ++- .../tachidesk/graphql/types/DownloadType.kt | 15 ++- .../tachidesk/graphql/types/ExtensionType.kt | 15 ++- .../tachidesk/graphql/types/MangaType.kt | 15 ++- .../tachidesk/graphql/types/MetaType.kt | 15 ++- .../tachidesk/graphql/types/SourceType.kt | 15 ++- .../tachidesk/graphql/types/UpdatesType.kt | 15 ++- 13 files changed, 207 insertions(+), 45 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/Cursor.kt rename server/src/main/kotlin/suwayomi/tachidesk/graphql/server/{TachideskGraphQLLongAsString.kt => primitives/LongAsString.kt} (98%) rename server/src/main/kotlin/suwayomi/tachidesk/graphql/{types => server/primitives}/NodeList.kt (92%) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt index 995377a6..87283a23 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -173,6 +173,7 @@ data class StringFilter( val greaterThanOrEqualToInsensitive: String? = null ) : ComparableScalarFilter +@Suppress("UNCHECKED_CAST") fun andFilterWithCompareString( column: Column, filter: StringFilter? diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 5725a029..292aa016 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -20,6 +20,9 @@ import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.queries.MetaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery import suwayomi.tachidesk.graphql.queries.UpdatesQuery +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor +import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription import kotlin.reflect.KClass import kotlin.reflect.KType @@ -27,6 +30,7 @@ import kotlin.reflect.KType class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { Long::class -> GraphQLLongAsString // encode to string for JS + Cursor::class -> GraphQLCursor else -> super.willGenerateGraphQLType(type) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/Cursor.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/Cursor.kt new file mode 100644 index 00000000..eb07e470 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/Cursor.kt @@ -0,0 +1,119 @@ +package suwayomi.tachidesk.graphql.server.primitives + +import graphql.GraphQLContext +import graphql.execution.CoercedVariables +import graphql.language.StringValue +import graphql.language.Value +import graphql.scalar.CoercingUtil +import graphql.schema.Coercing +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException +import graphql.schema.GraphQLScalarType +import java.util.Locale + +data class Cursor(val value: String) + +val GraphQLCursor: GraphQLScalarType = GraphQLScalarType.newScalar() + .name("Cursor").description("A location in a connection that can be used for resuming pagination.").coercing(GraphqlCursorCoercing()).build() + +private class GraphqlCursorCoercing : Coercing { + private fun toStringImpl(input: Any): String? { + return (input as? Cursor)?.value + } + + private fun parseValueImpl(input: Any, locale: Locale): Cursor { + if (input !is String) { + throw CoercingParseValueException( + CoercingUtil.i18nMsg( + locale, + "String.unexpectedRawValueType", + CoercingUtil.typeName(input) + ) + ) + } + return Cursor(input) + } + + private fun parseLiteralImpl(input: Any, locale: Locale): Cursor { + if (input !is StringValue) { + throw CoercingParseLiteralException( + CoercingUtil.i18nMsg( + locale, + "Scalar.unexpectedAstType", + "StringValue", + CoercingUtil.typeName(input) + ) + ) + } + return Cursor(input.value) + } + + private fun valueToLiteralImpl(input: Any): StringValue { + return StringValue.newStringValue(input.toString()).build() + } + + @Deprecated("") + override fun serialize(dataFetcherResult: Any): String { + return toStringImpl(dataFetcherResult) ?: throw CoercingSerializeException( + CoercingUtil.i18nMsg( + Locale.getDefault(), + "String.unexpectedRawValueType", + CoercingUtil.typeName(dataFetcherResult) + ) + ) + } + + @Throws(CoercingSerializeException::class) + override fun serialize( + dataFetcherResult: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): String { + return toStringImpl(dataFetcherResult) ?: throw CoercingSerializeException( + CoercingUtil.i18nMsg( + locale, + "String.unexpectedRawValueType", + CoercingUtil.typeName(dataFetcherResult) + ) + ) + } + + @Deprecated("") + override fun parseValue(input: Any): Cursor { + return parseValueImpl(input, Locale.getDefault()) + } + + @Throws(CoercingParseValueException::class) + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Cursor { + return parseValueImpl(input, locale) + } + + @Deprecated("") + override fun parseLiteral(input: Any): Cursor { + return parseLiteralImpl(input, Locale.getDefault()) + } + + @Throws(CoercingParseLiteralException::class) + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): Cursor { + return parseLiteralImpl(input, locale) + } + + @Deprecated("") + override fun valueToLiteral(input: Any): Value<*> { + return valueToLiteralImpl(input) + } + + override fun valueToLiteral( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): Value<*> { + return valueToLiteralImpl(input) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLLongAsString.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/LongAsString.kt similarity index 98% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLLongAsString.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/LongAsString.kt index a5841ab7..e5157ee6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLLongAsString.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/LongAsString.kt @@ -1,4 +1,4 @@ -package suwayomi.tachidesk.graphql.server +package suwayomi.tachidesk.graphql.server.primitives import graphql.GraphQLContext import graphql.execution.CoercedVariables diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt similarity index 92% rename from server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt rename to server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt index 0d0ced8f..9a886f9f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/NodeList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt @@ -1,11 +1,9 @@ -package suwayomi.tachidesk.graphql.types +package suwayomi.tachidesk.graphql.server.primitives import com.expediagroup.graphql.generator.annotations.GraphQLDescription interface Node -typealias Cursor = Int - abstract class NodeList { @GraphQLDescription("A list of [T] objects.") abstract val nodes: List @@ -36,5 +34,5 @@ abstract class Edges { abstract val cursor: Cursor @GraphQLDescription("The [T] at the end of the edge.") - abstract val node: Node? + abstract val node: Node } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt index 727c475a..16d2a83b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt @@ -10,6 +10,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.CategoryTable import java.util.concurrent.CompletableFuture @@ -43,7 +48,7 @@ data class CategoryNodeList( ) : NodeList() { data class CategoryEdges( override val cursor: Cursor, - override val node: CategoryType? + override val node: CategoryType ) : Edges() companion object { @@ -51,14 +56,14 @@ data class CategoryNodeList( return CategoryNodeList( nodes = this, edges = CategoryEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 01276a44..8d87d8f8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -10,6 +10,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import java.util.concurrent.CompletableFuture @@ -86,7 +91,7 @@ data class ChapterNodeList( ) : NodeList() { data class ChapterEdges( override val cursor: Cursor, - override val node: ChapterType? + override val node: ChapterType ) : Edges() companion object { @@ -94,14 +99,14 @@ data class ChapterNodeList( return ChapterNodeList( nodes = this, edges = ChapterEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt index e65adfc6..fc6565b8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt @@ -8,6 +8,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.impl.download.model.DownloadChapter import suwayomi.tachidesk.manga.impl.download.model.DownloadState import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass @@ -53,7 +58,7 @@ data class DownloadNodeList( ) : NodeList() { data class DownloadEdges( override val cursor: Cursor, - override val node: DownloadType? + override val node: DownloadType ) : Edges() companion object { @@ -61,14 +66,14 @@ data class DownloadNodeList( return DownloadNodeList( nodes = this, edges = DownloadEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt index c725687d..adee3407 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt @@ -10,6 +10,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.ExtensionTable import java.util.concurrent.CompletableFuture @@ -55,7 +60,7 @@ data class ExtensionNodeList( ) : NodeList() { data class ExtensionEdges( override val cursor: Cursor, - override val node: ExtensionType? + override val node: ExtensionType ) : Edges() companion object { @@ -63,14 +68,14 @@ data class ExtensionNodeList( return ExtensionNodeList( nodes = this, edges = ExtensionEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 2b20a301..816b793a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -10,6 +10,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.table.MangaStatus @@ -109,7 +114,7 @@ data class MangaNodeList( ) : NodeList() { data class MangaEdges( override val cursor: Cursor, - override val node: MangaType? + override val node: MangaType ) : Edges() companion object { @@ -117,14 +122,14 @@ data class MangaNodeList( return MangaNodeList( nodes = this, edges = MangaEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index fe85a61a..288170b6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -3,6 +3,11 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.global.model.table.GlobalMetaTable +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable @@ -38,7 +43,7 @@ data class MetaNodeList( ) : NodeList() { data class MetaEdges( override val cursor: Cursor, - override val node: MetaItem? + override val node: MetaItem ) : Edges() companion object { @@ -46,14 +51,14 @@ data class MetaNodeList( return MetaNodeList( nodes = this, edges = MetaEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index 2c9e19aa..bb0b9496 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -13,6 +13,11 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.select +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +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.impl.extension.Extension import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass @@ -84,7 +89,7 @@ data class SourceNodeList( ) : NodeList() { data class SourceEdges( override val cursor: Cursor, - override val node: SourceType? + override val node: SourceType ) : Edges() companion object { @@ -92,14 +97,14 @@ data class SourceNodeList( return SourceNodeList( nodes = this, edges = SourceEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt index bacfd014..e32ecb1a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt @@ -8,6 +8,11 @@ package suwayomi.tachidesk.graphql.types import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.Edges +import suwayomi.tachidesk.graphql.server.primitives.Node +import suwayomi.tachidesk.graphql.server.primitives.NodeList +import suwayomi.tachidesk.graphql.server.primitives.PageInfo class UpdatesType( val manga: MangaType, @@ -27,7 +32,7 @@ data class UpdatesNodeList( ) : NodeList() { data class UpdatesEdges( override val cursor: Cursor, - override val node: UpdatesType? + override val node: UpdatesType ) : Edges() companion object { @@ -35,14 +40,14 @@ data class UpdatesNodeList( return UpdatesNodeList( nodes = this, edges = UpdatesEdges( - cursor = lastIndex, - node = lastOrNull() + cursor = Cursor(lastIndex.toString()), + node = last() ), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, - startCursor = 0, - endCursor = lastIndex + startCursor = Cursor(0.toString()), + endCursor = Cursor(lastIndex.toString()) ), totalCount = size ) From 84881a0d52e82271c7bd616c080691fa1a27bcdf Mon Sep 17 00:00:00 2001 From: Syer10 Date: Tue, 4 Apr 2023 21:02:29 -0400 Subject: [PATCH 32/49] Complete MangaQuery --- .../tachidesk/graphql/queries/MangaQuery.kt | 103 ++++++++++++++++-- .../graphql/server/primitives/QueryResults.kt | 5 + 2 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/QueryResults.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index de53dd70..f2622df6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -27,8 +27,10 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.getOp +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults import suwayomi.tachidesk.graphql.types.MangaNodeList -import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaStatus @@ -61,6 +63,16 @@ class MangaQuery { LAST_FETCHED_AT } + private fun getAsCursor(orderBy: MangaOrderBy?, manga: MangaType): Cursor { + val value = when (orderBy) { + MangaOrderBy.ID, null -> manga.id.toString() + MangaOrderBy.TITLE -> manga.title + MangaOrderBy.IN_LIBRARY_AT -> manga.inLibraryAt.toString() + MangaOrderBy.LAST_FETCHED_AT -> manga.lastFetchedAt.toString() + } + return Cursor(value) + } + data class MangaCondition( val id: Int? = null, val sourceId: Long? = null, @@ -152,9 +164,14 @@ class MangaQuery { condition: MangaCondition? = null, filter: MangaFilter? = null, orderBy: MangaOrderBy? = null, - orderByType: SortOrder? = null + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null ): MangaNodeList { - val results = transaction { + val queryResults = transaction { var res = MangaTable.selectAll() val categoryOp = filter?.getCategoryOp() @@ -170,19 +187,89 @@ class MangaQuery { if (filterOp != null) { res.andWhere { filterOp } } - if (orderBy != null) { + if (orderBy != null || (last != null || before != null)) { val orderByColumn = when (orderBy) { - MangaOrderBy.ID -> MangaTable.id + MangaOrderBy.ID, null -> MangaTable.id MangaOrderBy.TITLE -> MangaTable.title MangaOrderBy.IN_LIBRARY_AT -> MangaTable.inLibraryAt MangaOrderBy.LAST_FETCHED_AT -> MangaTable.lastFetchedAt } - res.orderBy(orderByColumn, order = orderByType ?: SortOrder.ASC) + val orderType = if (last != null || before != null) { + when (orderByType) { + SortOrder.ASC -> SortOrder.DESC + SortOrder.DESC -> SortOrder.ASC + SortOrder.ASC_NULLS_FIRST -> SortOrder.DESC_NULLS_LAST + SortOrder.DESC_NULLS_FIRST -> SortOrder.ASC_NULLS_LAST + SortOrder.ASC_NULLS_LAST -> SortOrder.DESC_NULLS_FIRST + SortOrder.DESC_NULLS_LAST -> SortOrder.ASC_NULLS_FIRST + null -> SortOrder.DESC + } + } else { + orderByType ?: SortOrder.ASC + } + res.orderBy(orderByColumn, order = orderType) } - res.toList() + val total = res.count() + val firstResult = res.first()[MangaTable.id].value + val lastResult = res.last()[MangaTable.id].value + + if (after != null) { + when (orderBy) { + MangaOrderBy.ID, null -> res.andWhere { + MangaTable.id greater after.value.toInt() + } + MangaOrderBy.TITLE -> res.andWhere { + MangaTable.title greater after.value + } + MangaOrderBy.IN_LIBRARY_AT -> res.andWhere { + MangaTable.inLibraryAt greater after.value.toLong() + } + MangaOrderBy.LAST_FETCHED_AT -> res.andWhere { + MangaTable.lastFetchedAt greater after.value.toLong() + } + } + } else if (before != null) { + when (orderBy) { + MangaOrderBy.ID, null -> res.andWhere { + MangaTable.id less before.value.toInt() + } + MangaOrderBy.TITLE -> res.andWhere { + MangaTable.title less before.value + } + MangaOrderBy.IN_LIBRARY_AT -> res.andWhere { + MangaTable.inLibraryAt less before.value.toLong() + } + MangaOrderBy.LAST_FETCHED_AT -> res.andWhere { + MangaTable.lastFetchedAt less before.value.toLong() + } + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) } - return results.map { MangaType(it) }.toNodeList() // todo paged + val resultsAsType = queryResults.results.map { MangaType(it) } + + return MangaNodeList( + resultsAsType, + MangaNodeList.MangaEdges( + cursor = getAsCursor(orderBy, resultsAsType.last()), + node = resultsAsType.last() + ), + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.last().id, + hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, + startCursor = getAsCursor(orderBy, resultsAsType.first()), + endCursor = getAsCursor(orderBy, resultsAsType.last()) + ), + totalCount = queryResults.total.toInt() + ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/QueryResults.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/QueryResults.kt new file mode 100644 index 00000000..a3f604f2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/QueryResults.kt @@ -0,0 +1,5 @@ +package suwayomi.tachidesk.graphql.server.primitives + +import org.jetbrains.exposed.sql.ResultRow + +data class QueryResults(val total: Long, val firstKey: T, val lastKey: T, val results: List) From 671466a737ae47645bf4b0f93f01c6e26bf6900f Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 6 Apr 2023 21:53:30 -0400 Subject: [PATCH 33/49] Complete CategoryQuery --- .../graphql/queries/CategoryQuery.kt | 178 +++++++++++++++--- .../tachidesk/graphql/queries/MangaQuery.kt | 10 +- 2 files changed, 158 insertions(+), 30 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 7e47dc20..8374d7cf 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -9,12 +9,26 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.IntFilter +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString +import suwayomi.tachidesk.graphql.queries.filter.getOp +import suwayomi.tachidesk.graphql.server.primitives.Cursor +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults import suwayomi.tachidesk.graphql.types.CategoryNodeList -import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryTable import java.util.concurrent.CompletableFuture @@ -37,43 +51,157 @@ class CategoryQuery { return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) } - enum class CategorySort { + enum class CategoryOrderBy { ID, NAME, ORDER } - data class CategoriesQueryInput( - val sort: CategorySort? = null, - val sortOrder: SortOrder? = null, - val ids: List? = null, - val query: String? = null - ) + private fun getAsCursor(orderBy: CategoryOrderBy?, type: CategoryType): Cursor { + val value = when (orderBy) { + CategoryOrderBy.ID, null -> type.id.toString() + CategoryOrderBy.NAME -> type.name + CategoryOrderBy.ORDER -> type.order.toString() + } + return Cursor(value) + } - fun categories(input: CategoriesQueryInput? = null): CategoryNodeList { - val results = transaction { + data class CategoryCondition( + val id: Int? = null, + val order: Int? = null, + val name: String? = null, + val default: Boolean? = null + ) { + fun getOp(): Op? { + val opAnd = OpAnd() + fun eq(value: T?, column: Column) = opAnd.andWhere(value) { column eq it } + fun > eq(value: T?, column: Column>) = opAnd.andWhere(value) { column eq it } + eq(id, CategoryTable.id) + eq(order, CategoryTable.order) + eq(name, CategoryTable.name) + eq(default, CategoryTable.isDefault) + + return opAnd.op + } + } + + data class CategoryFilter( + val id: IntFilter? = null, + val order: IntFilter? = null, + val name: StringFilter? = null, + val default: BooleanFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: CategoryFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareEntity(CategoryTable.id, id), + andFilterWithCompare(CategoryTable.order, order), + andFilterWithCompareString(CategoryTable.name, name), + andFilterWithCompare(CategoryTable.isDefault, default), + ) + } + } + + fun categories( + condition: CategoryCondition? = null, + filter: CategoryFilter? = null, + orderBy: CategoryOrderBy? = null, + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null + ): CategoryNodeList { + val queryResults = transaction { val res = CategoryTable.selectAll() - if (input != null) { - if (input.ids != null) { - res.andWhere { CategoryTable.id inList input.ids } + val conditionOp = condition?.getOp() + if (conditionOp != null) { + res.andWhere { conditionOp } + } + val filterOp = filter?.getOp() + if (filterOp != null) { + res.andWhere { filterOp } + } + if (orderBy != null || (last != null || before != null)) { + val orderByColumn = when (orderBy) { + CategoryOrderBy.ID, null -> CategoryTable.id + CategoryOrderBy.NAME -> CategoryTable.name + CategoryOrderBy.ORDER -> CategoryTable.order } - if (!input.query.isNullOrEmpty()) { - res.andWhere { CategoryTable.name like input.query } + val orderType = if (last != null || before != null) { + when (orderByType) { + SortOrder.ASC -> SortOrder.DESC + SortOrder.DESC -> SortOrder.ASC + SortOrder.ASC_NULLS_FIRST -> SortOrder.DESC_NULLS_LAST + SortOrder.DESC_NULLS_FIRST -> SortOrder.ASC_NULLS_LAST + SortOrder.ASC_NULLS_LAST -> SortOrder.DESC_NULLS_FIRST + SortOrder.DESC_NULLS_LAST -> SortOrder.ASC_NULLS_FIRST + null -> SortOrder.DESC + } + } else { + orderByType ?: SortOrder.ASC } - val orderBy = when (input.sort) { - CategorySort.ID -> CategoryTable.id - CategorySort.NAME -> CategoryTable.name - CategorySort.ORDER, null -> CategoryTable.order - } - res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) - } else { - res.orderBy(CategoryTable.order) + res.orderBy(orderByColumn, order = orderType) } - res.toList() + val total = res.count() + val firstResult = res.first()[CategoryTable.id].value + val lastResult = res.last()[CategoryTable.id].value + + if (after != null) { + when (orderBy) { + CategoryOrderBy.ID, null -> res.andWhere { + CategoryTable.id greater after.value.toInt() + } + CategoryOrderBy.NAME -> res.andWhere { + CategoryTable.name greater after.value + } + CategoryOrderBy.ORDER -> res.andWhere { + CategoryTable.order greater after.value.toInt() + } + } + } else if (before != null) { + when (orderBy) { + CategoryOrderBy.ID, null -> res.andWhere { + CategoryTable.id less before.value.toInt() + } + CategoryOrderBy.NAME -> res.andWhere { + CategoryTable.name less before.value + } + CategoryOrderBy.ORDER -> res.andWhere { + CategoryTable.order less before.value.toInt() + } + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) } - return results.map { CategoryType(it) }.toNodeList() + val resultsAsType = queryResults.results.map { CategoryType(it) } + + return CategoryNodeList( + resultsAsType, + CategoryNodeList.CategoryEdges( + cursor = getAsCursor(orderBy, resultsAsType.last()), + node = resultsAsType.last() + ), + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.last().id, + hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, + startCursor = getAsCursor(orderBy, resultsAsType.first()), + endCursor = getAsCursor(orderBy, resultsAsType.last()) + ), + totalCount = queryResults.total.toInt() + ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index f2622df6..f2b487ca 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -63,12 +63,12 @@ class MangaQuery { LAST_FETCHED_AT } - private fun getAsCursor(orderBy: MangaOrderBy?, manga: MangaType): Cursor { + private fun getAsCursor(orderBy: MangaOrderBy?, type: MangaType): Cursor { val value = when (orderBy) { - MangaOrderBy.ID, null -> manga.id.toString() - MangaOrderBy.TITLE -> manga.title - MangaOrderBy.IN_LIBRARY_AT -> manga.inLibraryAt.toString() - MangaOrderBy.LAST_FETCHED_AT -> manga.lastFetchedAt.toString() + MangaOrderBy.ID, null -> type.id.toString() + MangaOrderBy.TITLE -> type.title + MangaOrderBy.IN_LIBRARY_AT -> type.inLibraryAt.toString() + MangaOrderBy.LAST_FETCHED_AT -> type.lastFetchedAt.toString() } return Cursor(value) } From 0b88207ad524e8e34c96caf35a67252cd3c5fba2 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 7 Apr 2023 00:02:00 -0400 Subject: [PATCH 34/49] Fix empty results errors --- .../graphql/queries/CategoryQuery.kt | 28 ++++++++++++++----- .../tachidesk/graphql/queries/MangaQuery.kt | 26 +++++++++++++---- .../graphql/server/primitives/NodeList.kt | 8 +++--- .../tachidesk/graphql/types/CategoryType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/ChapterType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/DownloadType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/ExtensionType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/MangaType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/MetaType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/SourceType.kt | 27 ++++++++++++------ .../tachidesk/graphql/types/UpdatesType.kt | 27 ++++++++++++------ 11 files changed, 197 insertions(+), 81 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 8374d7cf..418bb05b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -99,7 +99,7 @@ class CategoryQuery { andFilterWithCompareEntity(CategoryTable.id, id), andFilterWithCompare(CategoryTable.order, order), andFilterWithCompareString(CategoryTable.name, name), - andFilterWithCompare(CategoryTable.isDefault, default), + andFilterWithCompare(CategoryTable.isDefault, default) ) } } @@ -191,15 +191,29 @@ class CategoryQuery { return CategoryNodeList( resultsAsType, - CategoryNodeList.CategoryEdges( - cursor = getAsCursor(orderBy, resultsAsType.last()), - node = resultsAsType.last() - ), + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOf( + resultsAsType.first().let { + CategoryNodeList.CategoryEdge( + getAsCursor(orderBy, it), + it + ) + }, + resultsAsType.last().let { + CategoryNodeList.CategoryEdge( + getAsCursor(orderBy, it), + it + ) + } + ) + }, pageInfo = PageInfo( hasNextPage = queryResults.lastKey != resultsAsType.last().id, hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, - startCursor = getAsCursor(orderBy, resultsAsType.first()), - endCursor = getAsCursor(orderBy, resultsAsType.last()) + startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(orderBy, it) }, + endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(orderBy, it) } ), totalCount = queryResults.total.toInt() ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index f2b487ca..df1b1dc6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -259,15 +259,29 @@ class MangaQuery { return MangaNodeList( resultsAsType, - MangaNodeList.MangaEdges( - cursor = getAsCursor(orderBy, resultsAsType.last()), - node = resultsAsType.last() - ), + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOf( + resultsAsType.first().let { + MangaNodeList.MangaEdge( + getAsCursor(orderBy, it), + it + ) + }, + resultsAsType.last().let { + MangaNodeList.MangaEdge( + getAsCursor(orderBy, it), + it + ) + } + ) + }, pageInfo = PageInfo( hasNextPage = queryResults.lastKey != resultsAsType.last().id, hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, - startCursor = getAsCursor(orderBy, resultsAsType.first()), - endCursor = getAsCursor(orderBy, resultsAsType.last()) + startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(orderBy, it) }, + endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(orderBy, it) } ), totalCount = queryResults.total.toInt() ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt index 9a886f9f..ae4ac253 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/NodeList.kt @@ -9,7 +9,7 @@ abstract class NodeList { abstract val nodes: List @GraphQLDescription("A list of edges which contains the [T] and cursor to aid in pagination.") - abstract val edges: Edges + abstract val edges: List @GraphQLDescription("Information to aid in pagination.") abstract val pageInfo: PageInfo @@ -24,12 +24,12 @@ data class PageInfo( @GraphQLDescription("When paginating backwards, are there more items?") val hasPreviousPage: Boolean, @GraphQLDescription("When paginating backwards, the cursor to continue.") - val startCursor: Cursor, + val startCursor: Cursor?, @GraphQLDescription("When paginating forwards, the cursor to continue.") - val endCursor: Cursor + val endCursor: Cursor? ) -abstract class Edges { +abstract class Edge { @GraphQLDescription("A cursor for use in pagination.") abstract val cursor: Cursor diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt index 16d2a83b..b535c6cc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/CategoryType.kt @@ -11,7 +11,7 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -42,23 +42,20 @@ class CategoryType( data class CategoryNodeList( override val nodes: List, - override val edges: CategoryEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class CategoryEdges( + data class CategoryEdge( override val cursor: Cursor, override val node: CategoryType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): CategoryNodeList { return CategoryNodeList( nodes = this, - edges = CategoryEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -68,5 +65,19 @@ data class CategoryNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + CategoryEdge( + cursor = Cursor("0"), + node = first() + ), + CategoryEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 8d87d8f8..8ec24372 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -11,7 +11,7 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -85,23 +85,20 @@ class ChapterType( data class ChapterNodeList( override val nodes: List, - override val edges: ChapterEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class ChapterEdges( + data class ChapterEdge( override val cursor: Cursor, override val node: ChapterType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): ChapterNodeList { return ChapterNodeList( nodes = this, - edges = ChapterEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -111,5 +108,19 @@ data class ChapterNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + ChapterEdge( + cursor = Cursor("0"), + node = first() + ), + ChapterEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt index fc6565b8..37d7e8f3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/DownloadType.kt @@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -52,23 +52,20 @@ class DownloadType( data class DownloadNodeList( override val nodes: List, - override val edges: DownloadEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class DownloadEdges( + data class DownloadEdge( override val cursor: Cursor, override val node: DownloadType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): DownloadNodeList { return DownloadNodeList( nodes = this, - edges = DownloadEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -78,5 +75,19 @@ data class DownloadNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + DownloadEdge( + cursor = Cursor("0"), + node = first() + ), + DownloadEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt index adee3407..1c7c11a0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt @@ -11,7 +11,7 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -54,23 +54,20 @@ class ExtensionType( data class ExtensionNodeList( override val nodes: List, - override val edges: ExtensionEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class ExtensionEdges( + data class ExtensionEdge( override val cursor: Cursor, override val node: ExtensionType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): ExtensionNodeList { return ExtensionNodeList( nodes = this, - edges = ExtensionEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -80,5 +77,19 @@ data class ExtensionNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + ExtensionEdge( + cursor = Cursor("0"), + node = first() + ), + ExtensionEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 816b793a..4abe5b27 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -11,7 +11,7 @@ import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -108,23 +108,20 @@ class MangaType( data class MangaNodeList( override val nodes: List, - override val edges: MangaEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class MangaEdges( + data class MangaEdge( override val cursor: Cursor, override val node: MangaType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): MangaNodeList { return MangaNodeList( nodes = this, - edges = MangaEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -134,5 +131,19 @@ data class MangaNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + MangaEdge( + cursor = Cursor("0"), + node = first() + ), + MangaEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index 288170b6..5c5ba1c0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -4,7 +4,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -37,23 +37,20 @@ class GlobalMetaItem( data class MetaNodeList( override val nodes: List, - override val edges: MetaEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class MetaEdges( + data class MetaEdge( override val cursor: Cursor, override val node: MetaItem - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): MetaNodeList { return MetaNodeList( nodes = this, - edges = MetaEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -63,5 +60,19 @@ data class MetaNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + MetaEdge( + cursor = Cursor("0"), + node = first() + ), + MetaEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index bb0b9496..a3018c39 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -14,7 +14,7 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.select import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -83,23 +83,20 @@ fun SourceType(row: ResultRow): SourceType? { data class SourceNodeList( override val nodes: List, - override val edges: SourceEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class SourceEdges( + data class SourceEdge( override val cursor: Cursor, override val node: SourceType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): SourceNodeList { return SourceNodeList( nodes = this, - edges = SourceEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -109,5 +106,19 @@ data class SourceNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + SourceEdge( + cursor = Cursor("0"), + node = first() + ), + SourceEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt index e32ecb1a..7260e5a2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt @@ -9,7 +9,7 @@ package suwayomi.tachidesk.graphql.types import org.jetbrains.exposed.sql.ResultRow import suwayomi.tachidesk.graphql.server.primitives.Cursor -import suwayomi.tachidesk.graphql.server.primitives.Edges +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 @@ -26,23 +26,20 @@ class UpdatesType( data class UpdatesNodeList( override val nodes: List, - override val edges: UpdatesEdges, + override val edges: List, override val pageInfo: PageInfo, override val totalCount: Int ) : NodeList() { - data class UpdatesEdges( + data class UpdatesEdge( override val cursor: Cursor, override val node: UpdatesType - ) : Edges() + ) : Edge() companion object { fun List.toNodeList(): UpdatesNodeList { return UpdatesNodeList( nodes = this, - edges = UpdatesEdges( - cursor = Cursor(lastIndex.toString()), - node = last() - ), + edges = getEdges(), pageInfo = PageInfo( hasNextPage = false, hasPreviousPage = false, @@ -52,5 +49,19 @@ data class UpdatesNodeList( totalCount = size ) } + + private fun List.getEdges(): List { + if (isEmpty()) return emptyList() + return listOf( + UpdatesEdge( + cursor = Cursor("0"), + node = first() + ), + UpdatesEdge( + cursor = Cursor(lastIndex.toString()), + node = last() + ) + ) + } } } From d8567eadb2ef2355416a3a31c31c74124565a73a Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 7 Apr 2023 21:10:38 -0400 Subject: [PATCH 35/49] Simplify queries --- .../graphql/queries/CategoryQuery.kt | 139 ++++++------- .../tachidesk/graphql/queries/MangaQuery.kt | 186 ++++++++---------- .../graphql/queries/filter/Filter.kt | 87 ++++---- .../graphql/server/primitives/OrderBy.kt | 32 +++ 4 files changed, 228 insertions(+), 216 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 418bb05b..5d0d5397 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -9,25 +9,29 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp import suwayomi.tachidesk.graphql.queries.filter.IntFilter import suwayomi.tachidesk.graphql.queries.filter.OpAnd import suwayomi.tachidesk.graphql.queries.filter.StringFilter import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString -import suwayomi.tachidesk.graphql.queries.filter.getOp +import suwayomi.tachidesk.graphql.queries.filter.applyOps import suwayomi.tachidesk.graphql.server.primitives.Cursor +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.maybeSwap import suwayomi.tachidesk.graphql.types.CategoryNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryTable @@ -51,19 +55,35 @@ class CategoryQuery { return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) } - enum class CategoryOrderBy { - ID, - NAME, - ORDER - } + enum class CategoryOrderBy(override val column: Column>) : OrderBy { + ID(CategoryTable.id), + NAME(CategoryTable.name), + ORDER(CategoryTable.order); - private fun getAsCursor(orderBy: CategoryOrderBy?, type: CategoryType): Cursor { - val value = when (orderBy) { - CategoryOrderBy.ID, null -> type.id.toString() - CategoryOrderBy.NAME -> type.name - CategoryOrderBy.ORDER -> type.order.toString() + override fun greater(cursor: Cursor): Op { + return when (this) { + ID -> CategoryTable.id greater cursor.value.toInt() + NAME -> CategoryTable.name greater cursor.value + ORDER -> CategoryTable.order greater cursor.value.toInt() + } + } + + override fun less(cursor: Cursor): Op { + return when (this) { + ID -> CategoryTable.id less cursor.value.toInt() + NAME -> CategoryTable.name less cursor.value + ORDER -> CategoryTable.order less cursor.value.toInt() + } + } + + override fun asCursor(type: CategoryType): Cursor { + val value = when (this) { + ID -> type.id.toString() + NAME -> type.name + ORDER -> type.order.toString() + } + return Cursor(value) } - return Cursor(value) } data class CategoryCondition( @@ -71,15 +91,13 @@ class CategoryQuery { val order: Int? = null, val name: String? = null, val default: Boolean? = null - ) { - fun getOp(): Op? { + ) : HasGetOp { + override fun getOp(): Op? { val opAnd = OpAnd() - fun eq(value: T?, column: Column) = opAnd.andWhere(value) { column eq it } - fun > eq(value: T?, column: Column>) = opAnd.andWhere(value) { column eq it } - eq(id, CategoryTable.id) - eq(order, CategoryTable.order) - eq(name, CategoryTable.name) - eq(default, CategoryTable.isDefault) + opAnd.eq(id, CategoryTable.id) + opAnd.eq(order, CategoryTable.order) + opAnd.eq(name, CategoryTable.name) + opAnd.eq(default, CategoryTable.isDefault) return opAnd.op } @@ -118,63 +136,26 @@ class CategoryQuery { val queryResults = transaction { val res = CategoryTable.selectAll() - val conditionOp = condition?.getOp() - if (conditionOp != null) { - res.andWhere { conditionOp } - } - val filterOp = filter?.getOp() - if (filterOp != null) { - res.andWhere { filterOp } - } + res.applyOps(condition, filter) + if (orderBy != null || (last != null || before != null)) { - val orderByColumn = when (orderBy) { - CategoryOrderBy.ID, null -> CategoryTable.id - CategoryOrderBy.NAME -> CategoryTable.name - CategoryOrderBy.ORDER -> CategoryTable.order - } - val orderType = if (last != null || before != null) { - when (orderByType) { - SortOrder.ASC -> SortOrder.DESC - SortOrder.DESC -> SortOrder.ASC - SortOrder.ASC_NULLS_FIRST -> SortOrder.DESC_NULLS_LAST - SortOrder.DESC_NULLS_FIRST -> SortOrder.ASC_NULLS_LAST - SortOrder.ASC_NULLS_LAST -> SortOrder.DESC_NULLS_FIRST - SortOrder.DESC_NULLS_LAST -> SortOrder.ASC_NULLS_FIRST - null -> SortOrder.DESC - } - } else { - orderByType ?: SortOrder.ASC - } + val orderByColumn = orderBy?.column ?: CategoryTable.id + val orderType = orderByType.maybeSwap(last ?: before) + res.orderBy(orderByColumn, order = orderType) } val total = res.count() - val firstResult = res.first()[CategoryTable.id].value - val lastResult = res.last()[CategoryTable.id].value + val firstResult = res.firstOrNull()?.get(CategoryTable.id)?.value + val lastResult = res.lastOrNull()?.get(CategoryTable.id)?.value if (after != null) { - when (orderBy) { - CategoryOrderBy.ID, null -> res.andWhere { - CategoryTable.id greater after.value.toInt() - } - CategoryOrderBy.NAME -> res.andWhere { - CategoryTable.name greater after.value - } - CategoryOrderBy.ORDER -> res.andWhere { - CategoryTable.order greater after.value.toInt() - } + res.andWhere { + (orderBy ?: CategoryOrderBy.ID).greater(after) } } else if (before != null) { - when (orderBy) { - CategoryOrderBy.ID, null -> res.andWhere { - CategoryTable.id less before.value.toInt() - } - CategoryOrderBy.NAME -> res.andWhere { - CategoryTable.name less before.value - } - CategoryOrderBy.ORDER -> res.andWhere { - CategoryTable.order less before.value.toInt() - } + res.andWhere { + (orderBy ?: CategoryOrderBy.ID).less(before) } } @@ -187,6 +168,8 @@ class CategoryQuery { QueryResults(total, firstResult, lastResult, res.toList()) } + val getAsCursor: (CategoryType) -> Cursor = (orderBy ?: CategoryOrderBy.ID)::asCursor + val resultsAsType = queryResults.results.map { CategoryType(it) } return CategoryNodeList( @@ -194,26 +177,26 @@ class CategoryQuery { if (resultsAsType.isEmpty()) { emptyList() } else { - listOf( - resultsAsType.first().let { + listOfNotNull( + resultsAsType.firstOrNull()?.let { CategoryNodeList.CategoryEdge( - getAsCursor(orderBy, it), + getAsCursor(it), it ) }, - resultsAsType.last().let { + resultsAsType.lastOrNull()?.let { CategoryNodeList.CategoryEdge( - getAsCursor(orderBy, it), + getAsCursor(it), it ) } ) }, pageInfo = PageInfo( - hasNextPage = queryResults.lastKey != resultsAsType.last().id, - hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, - startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(orderBy, it) }, - endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(orderBy, it) } + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id, + 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/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index df1b1dc6..0a79da30 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -9,16 +9,18 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp import suwayomi.tachidesk.graphql.queries.filter.IntFilter import suwayomi.tachidesk.graphql.queries.filter.LongFilter import suwayomi.tachidesk.graphql.queries.filter.OpAnd @@ -26,10 +28,13 @@ import suwayomi.tachidesk.graphql.queries.filter.StringFilter import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString -import suwayomi.tachidesk.graphql.queries.filter.getOp +import suwayomi.tachidesk.graphql.queries.filter.applyOps import suwayomi.tachidesk.graphql.server.primitives.Cursor +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.maybeSwap +import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable @@ -56,21 +61,39 @@ class MangaQuery { return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) } - enum class MangaOrderBy { - ID, - TITLE, - IN_LIBRARY_AT, - LAST_FETCHED_AT - } + enum class MangaOrderBy(override val column: Column>) : OrderBy { + ID(MangaTable.id), + TITLE(MangaTable.title), + IN_LIBRARY_AT(MangaTable.inLibraryAt), + LAST_FETCHED_AT(MangaTable.lastFetchedAt); - private fun getAsCursor(orderBy: MangaOrderBy?, type: MangaType): Cursor { - val value = when (orderBy) { - MangaOrderBy.ID, null -> type.id.toString() - MangaOrderBy.TITLE -> type.title - MangaOrderBy.IN_LIBRARY_AT -> type.inLibraryAt.toString() - MangaOrderBy.LAST_FETCHED_AT -> type.lastFetchedAt.toString() + override fun greater(cursor: Cursor): Op { + return when (this) { + ID -> MangaTable.id greater cursor.value.toInt() + TITLE -> MangaTable.title greater cursor.value + IN_LIBRARY_AT -> MangaTable.inLibraryAt greater cursor.value.toLong() + LAST_FETCHED_AT -> MangaTable.lastFetchedAt greater cursor.value.toLong() + } + } + + override fun less(cursor: Cursor): Op { + return when (this) { + ID -> MangaTable.id less cursor.value.toInt() + TITLE -> MangaTable.title less cursor.value + IN_LIBRARY_AT -> MangaTable.inLibraryAt less cursor.value.toLong() + LAST_FETCHED_AT -> MangaTable.lastFetchedAt less cursor.value.toLong() + } + } + + override fun asCursor(type: MangaType): Cursor { + val value = when (this) { + ID -> type.id.toString() + TITLE -> type.title + IN_LIBRARY_AT -> type.inLibraryAt.toString() + LAST_FETCHED_AT -> type.lastFetchedAt.toString() + } + return Cursor(value) } - return Cursor(value) } data class MangaCondition( @@ -88,29 +111,27 @@ class MangaQuery { val inLibrary: Boolean? = null, val inLibraryAt: Long? = null, val realUrl: String? = null, - var lastFetchedAt: Long? = null, - var chaptersLastFetchedAt: Long? = null - ) { - fun getOp(): Op? { + val lastFetchedAt: Long? = null, + val chaptersLastFetchedAt: Long? = null + ) : HasGetOp { + override fun getOp(): Op? { val opAnd = OpAnd() - fun eq(value: T?, column: Column) = opAnd.andWhere(value) { column eq it } - fun > eq(value: T?, column: Column>) = opAnd.andWhere(value) { column eq it } - eq(id, MangaTable.id) - eq(sourceId, MangaTable.sourceReference) - eq(url, MangaTable.url) - eq(title, MangaTable.title) - eq(thumbnailUrl, MangaTable.thumbnail_url) - eq(initialized, MangaTable.initialized) - eq(artist, MangaTable.artist) - eq(author, MangaTable.author) - eq(description, MangaTable.description) - eq(genre?.joinToString(), MangaTable.genre) - eq(status?.value, MangaTable.status) - eq(inLibrary, MangaTable.inLibrary) - eq(inLibraryAt, MangaTable.inLibraryAt) - eq(realUrl, MangaTable.realUrl) - eq(lastFetchedAt, MangaTable.lastFetchedAt) - eq(chaptersLastFetchedAt, MangaTable.chaptersLastFetchedAt) + opAnd.eq(id, MangaTable.id) + opAnd.eq(sourceId, MangaTable.sourceReference) + opAnd.eq(url, MangaTable.url) + opAnd.eq(title, MangaTable.title) + opAnd.eq(thumbnailUrl, MangaTable.thumbnail_url) + opAnd.eq(initialized, MangaTable.initialized) + opAnd.eq(artist, MangaTable.artist) + opAnd.eq(author, MangaTable.author) + opAnd.eq(description, MangaTable.description) + opAnd.eq(genre?.joinToString(), MangaTable.genre) + opAnd.eq(status?.value, MangaTable.status) + opAnd.eq(inLibrary, MangaTable.inLibrary) + opAnd.eq(inLibraryAt, MangaTable.inLibraryAt) + opAnd.eq(realUrl, MangaTable.realUrl) + opAnd.eq(lastFetchedAt, MangaTable.lastFetchedAt) + opAnd.eq(chaptersLastFetchedAt, MangaTable.chaptersLastFetchedAt) return opAnd.op } @@ -131,8 +152,8 @@ class MangaQuery { val inLibrary: BooleanFilter? = null, val inLibraryAt: LongFilter? = null, val realUrl: StringFilter? = null, - var lastFetchedAt: LongFilter? = null, - var chaptersLastFetchedAt: LongFilter? = null, + val lastFetchedAt: LongFilter? = null, + val chaptersLastFetchedAt: LongFilter? = null, val category: IntFilter? = null, override val and: List? = null, override val or: List? = null, @@ -179,70 +200,27 @@ class MangaQuery { res = MangaTable.innerJoin(CategoryMangaTable) .select { categoryOp } } - val conditionOp = condition?.getOp() - if (conditionOp != null) { - res.andWhere { conditionOp } - } - val filterOp = filter?.getOp() - if (filterOp != null) { - res.andWhere { filterOp } - } + + res.applyOps(condition, filter) + if (orderBy != null || (last != null || before != null)) { - val orderByColumn = when (orderBy) { - MangaOrderBy.ID, null -> MangaTable.id - MangaOrderBy.TITLE -> MangaTable.title - MangaOrderBy.IN_LIBRARY_AT -> MangaTable.inLibraryAt - MangaOrderBy.LAST_FETCHED_AT -> MangaTable.lastFetchedAt - } - val orderType = if (last != null || before != null) { - when (orderByType) { - SortOrder.ASC -> SortOrder.DESC - SortOrder.DESC -> SortOrder.ASC - SortOrder.ASC_NULLS_FIRST -> SortOrder.DESC_NULLS_LAST - SortOrder.DESC_NULLS_FIRST -> SortOrder.ASC_NULLS_LAST - SortOrder.ASC_NULLS_LAST -> SortOrder.DESC_NULLS_FIRST - SortOrder.DESC_NULLS_LAST -> SortOrder.ASC_NULLS_FIRST - null -> SortOrder.DESC - } - } else { - orderByType ?: SortOrder.ASC - } + val orderByColumn = orderBy?.column ?: MangaTable.id + val orderType = orderByType.maybeSwap(last ?: before) + res.orderBy(orderByColumn, order = orderType) } val total = res.count() - val firstResult = res.first()[MangaTable.id].value - val lastResult = res.last()[MangaTable.id].value + val firstResult = res.firstOrNull()?.get(MangaTable.id)?.value + val lastResult = res.lastOrNull()?.get(MangaTable.id)?.value if (after != null) { - when (orderBy) { - MangaOrderBy.ID, null -> res.andWhere { - MangaTable.id greater after.value.toInt() - } - MangaOrderBy.TITLE -> res.andWhere { - MangaTable.title greater after.value - } - MangaOrderBy.IN_LIBRARY_AT -> res.andWhere { - MangaTable.inLibraryAt greater after.value.toLong() - } - MangaOrderBy.LAST_FETCHED_AT -> res.andWhere { - MangaTable.lastFetchedAt greater after.value.toLong() - } + res.andWhere { + (orderBy ?: MangaOrderBy.ID).greater(after) } } else if (before != null) { - when (orderBy) { - MangaOrderBy.ID, null -> res.andWhere { - MangaTable.id less before.value.toInt() - } - MangaOrderBy.TITLE -> res.andWhere { - MangaTable.title less before.value - } - MangaOrderBy.IN_LIBRARY_AT -> res.andWhere { - MangaTable.inLibraryAt less before.value.toLong() - } - MangaOrderBy.LAST_FETCHED_AT -> res.andWhere { - MangaTable.lastFetchedAt less before.value.toLong() - } + res.andWhere { + (orderBy ?: MangaOrderBy.ID).less(before) } } @@ -255,6 +233,8 @@ class MangaQuery { QueryResults(total, firstResult, lastResult, res.toList()) } + val getAsCursor: (MangaType) -> Cursor = (orderBy ?: MangaOrderBy.ID)::asCursor + val resultsAsType = queryResults.results.map { MangaType(it) } return MangaNodeList( @@ -262,26 +242,26 @@ class MangaQuery { if (resultsAsType.isEmpty()) { emptyList() } else { - listOf( - resultsAsType.first().let { + listOfNotNull( + resultsAsType.firstOrNull()?.let { MangaNodeList.MangaEdge( - getAsCursor(orderBy, it), + getAsCursor(it), it ) }, - resultsAsType.last().let { + resultsAsType.lastOrNull()?.let { MangaNodeList.MangaEdge( - getAsCursor(orderBy, it), + getAsCursor(it), it ) } ) }, pageInfo = PageInfo( - hasNextPage = queryResults.lastKey != resultsAsType.last().id, - hasPreviousPage = queryResults.firstKey != resultsAsType.first().id, - startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(orderBy, it) }, - endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(orderBy, it) } + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id, + 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/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt index 87283a23..d02340c0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -7,10 +7,13 @@ import org.jetbrains.exposed.sql.Expression import org.jetbrains.exposed.sql.ExpressionWithColumnType import org.jetbrains.exposed.sql.LikePattern import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.SqlExpressionBuilder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.not import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.stringParam @@ -68,12 +71,57 @@ class DistinctFromOp(expr1: Expression<*>, expr2: Expression<*>, not: Boolean) : } } -interface Filter> { +interface HasGetOp { + fun getOp(): Op? +} + +fun Query.applyOps(vararg ops: HasGetOp?) { + ops.mapNotNull { it?.getOp() }.forEach { + andWhere { it } + } +} + +interface Filter> : HasGetOp { val and: List? val or: List? val not: T? fun getOpList(): List> + + override fun getOp(): Op? { + var op: Op? = null + fun newOp( + otherOp: Op?, + operator: (Op, Op) -> Op + ) { + when { + op == null && otherOp == null -> Unit + op == null && otherOp != null -> op = otherOp + op != null && otherOp == null -> Unit + op != null && otherOp != null -> op = operator(op!!, otherOp) + } + } + fun andOp(andOp: Op?) { + newOp(andOp, Op::and) + } + fun orOp(orOp: Op?) { + newOp(orOp, Op::or) + } + getOpList().forEach { + andOp(it) + } + and?.forEach { + andOp(it.getOp()) + } + or?.forEach { + orOp(it.getOp()) + } + if (not != null) { + andOp(not!!.getOp()?.let(::not)) + } + return op + } + } interface ScalarFilter { @@ -220,6 +268,9 @@ class OpAnd(var op: Op? = null) { val expr = Op.build { andPart(value) } op = if (op == null) expr else (op!! and expr) } + + fun eq(value: T?, column: Column) = andWhere(value) { column eq it } + fun > eq(value: T?, column: Column>) = andWhere(value) { column eq it } } fun > andFilterWithCompare( @@ -293,37 +344,3 @@ fun > andFilterEntity( } return opAnd.op } - -fun > Filter.getOp(): Op? { - var op: Op? = null - fun newOp( - otherOp: Op?, - operator: (Op, Op) -> Op - ) { - when { - op == null && otherOp == null -> Unit - op == null && otherOp != null -> op = otherOp - op != null && otherOp == null -> Unit - op != null && otherOp != null -> op = operator(op!!, otherOp) - } - } - fun andOp(andOp: Op?) { - newOp(andOp, Op::and) - } - fun orOp(orOp: Op?) { - newOp(orOp, Op::or) - } - getOpList().forEach { - andOp(it) - } - and?.forEach { - andOp(it.getOp()) - } - or?.forEach { - orOp(it.getOp()) - } - if (not != null) { - andOp(not!!.getOp()?.let(::not)) - } - return op -} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt new file mode 100644 index 00000000..671e4b7b --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -0,0 +1,32 @@ +package suwayomi.tachidesk.graphql.server.primitives + +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SortOrder + +interface OrderBy { + val column: Column> + + fun asCursor(type: T): Cursor + + fun greater(cursor: Cursor): Op + + fun less(cursor: Cursor): Op +} + + +fun SortOrder?.maybeSwap(value: Any?): SortOrder { + return if (value != null) { + when (this) { + SortOrder.ASC -> SortOrder.DESC + SortOrder.DESC -> SortOrder.ASC + SortOrder.ASC_NULLS_FIRST -> SortOrder.DESC_NULLS_LAST + SortOrder.DESC_NULLS_FIRST -> SortOrder.ASC_NULLS_LAST + SortOrder.ASC_NULLS_LAST -> SortOrder.DESC_NULLS_FIRST + SortOrder.DESC_NULLS_LAST -> SortOrder.ASC_NULLS_FIRST + null -> SortOrder.DESC + } + } else { + this ?: SortOrder.ASC + } +} \ No newline at end of file From a4dfcf80e4741180867cf523876989962ae3ef5a Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 7 Apr 2023 21:30:20 -0400 Subject: [PATCH 36/49] Implement manga status filter --- .../tachidesk/graphql/queries/MangaQuery.kt | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 0a79da30..d930d074 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -19,6 +19,7 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.ComparableScalarFilter import suwayomi.tachidesk.graphql.queries.filter.Filter import suwayomi.tachidesk.graphql.queries.filter.HasGetOp import suwayomi.tachidesk.graphql.queries.filter.IntFilter @@ -137,6 +138,34 @@ class MangaQuery { } } + data class MangaStatusFilter( + override val isNull: Boolean? = null, + override val equalTo: MangaStatus? = null, + override val notEqualTo: MangaStatus? = null, + override val distinctFrom: MangaStatus? = null, + override val notDistinctFrom: MangaStatus? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: MangaStatus? = null, + override val lessThanOrEqualTo: MangaStatus? = null, + override val greaterThan: MangaStatus? = null, + override val greaterThanOrEqualTo: MangaStatus? = null + ) : ComparableScalarFilter { + fun asIntFilter() = IntFilter( + equalTo = equalTo?.value, + notEqualTo = notEqualTo?.value, + distinctFrom = distinctFrom?.value, + notDistinctFrom = notDistinctFrom?.value, + `in` = `in`?.map { it.value }, + notIn = notIn?.map { it.value }, + lessThan = lessThan?.value, + lessThanOrEqualTo = lessThanOrEqualTo?.value, + greaterThan = greaterThan?.value, + greaterThanOrEqualTo = greaterThanOrEqualTo?.value, + + ) + } + data class MangaFilter( val id: IntFilter? = null, val sourceId: LongFilter? = null, @@ -148,7 +177,7 @@ class MangaQuery { val author: StringFilter? = null, val description: StringFilter? = null, // val genre: List? = null, // todo - // val status: MangaStatus? = null, // todo + val status: MangaStatusFilter? = null, val inLibrary: BooleanFilter? = null, val inLibraryAt: LongFilter? = null, val realUrl: StringFilter? = null, @@ -170,6 +199,7 @@ class MangaQuery { andFilterWithCompareString(MangaTable.artist, artist), andFilterWithCompareString(MangaTable.author, author), andFilterWithCompareString(MangaTable.description, description), + andFilterWithCompare(MangaTable.status, status?.asIntFilter()), andFilterWithCompare(MangaTable.inLibrary, inLibrary), andFilterWithCompare(MangaTable.inLibraryAt, inLibraryAt), andFilterWithCompareString(MangaTable.realUrl, realUrl), From 0e84b8a1541663083e92ae7fde6eaace42c92a01 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 13:36:28 -0400 Subject: [PATCH 37/49] Lint --- .../kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt | 1 - .../suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt index d02340c0..e66aa2a2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -121,7 +121,6 @@ interface Filter> : HasGetOp { } return op } - } interface ScalarFilter { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt index 671e4b7b..4f6733c4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -14,7 +14,6 @@ interface OrderBy { fun less(cursor: Cursor): Op } - fun SortOrder?.maybeSwap(value: Any?): SortOrder { return if (value != null) { when (this) { @@ -29,4 +28,4 @@ fun SortOrder?.maybeSwap(value: Any?): SortOrder { } else { this ?: SortOrder.ASC } -} \ No newline at end of file +} From 58a623d44dc9e08a460246cba0c477f5790bf39d Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 13:37:09 -0400 Subject: [PATCH 38/49] Fix keyset pagination for non-unique order by modes --- .../graphql/queries/CategoryQuery.kt | 41 ++++++++++--- .../tachidesk/graphql/queries/MangaQuery.kt | 57 +++++++++++++++---- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 5d0d5397..3fcb6057 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -12,9 +12,12 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter @@ -39,7 +42,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Paged queries * * TODO Mutations * - Name @@ -63,24 +65,40 @@ class CategoryQuery { override fun greater(cursor: Cursor): Op { return when (this) { ID -> CategoryTable.id greater cursor.value.toInt() - NAME -> CategoryTable.name greater cursor.value - ORDER -> CategoryTable.order greater cursor.value.toInt() + NAME -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-') + (CategoryTable.name greater value) or ((CategoryTable.name eq value) and (CategoryTable.id greater id)) + } + ORDER -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toInt() + (CategoryTable.order greater value) or ((CategoryTable.order eq value) and (CategoryTable.id greater id)) + } } } override fun less(cursor: Cursor): Op { return when (this) { ID -> CategoryTable.id less cursor.value.toInt() - NAME -> CategoryTable.name less cursor.value - ORDER -> CategoryTable.order less cursor.value.toInt() + NAME -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-') + (CategoryTable.name less value) or ((CategoryTable.name eq value) and (CategoryTable.id less id)) + } + ORDER -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toInt() + (CategoryTable.order less value) or ((CategoryTable.order eq value) and (CategoryTable.id less id)) + } } } override fun asCursor(type: CategoryType): Cursor { val value = when (this) { ID -> type.id.toString() - NAME -> type.name - ORDER -> type.order.toString() + NAME -> type.id.toString() + "-" + type.name + ORDER -> type.id.toString() + "-" + type.order } return Cursor(value) } @@ -142,7 +160,14 @@ class CategoryQuery { val orderByColumn = orderBy?.column ?: CategoryTable.id val orderType = orderByType.maybeSwap(last ?: before) - res.orderBy(orderByColumn, order = orderType) + if (orderBy == CategoryOrderBy.ID || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + CategoryTable.id to SortOrder.ASC + ) + } } val total = res.count() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index d930d074..666936ee 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -12,9 +12,12 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -35,7 +38,6 @@ 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.maybeSwap -import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable @@ -71,27 +73,51 @@ class MangaQuery { override fun greater(cursor: Cursor): Op { return when (this) { ID -> MangaTable.id greater cursor.value.toInt() - TITLE -> MangaTable.title greater cursor.value - IN_LIBRARY_AT -> MangaTable.inLibraryAt greater cursor.value.toLong() - LAST_FETCHED_AT -> MangaTable.lastFetchedAt greater cursor.value.toLong() + TITLE -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-') + (MangaTable.title greater value) or ((MangaTable.title eq value) and (MangaTable.id greater id)) + } + IN_LIBRARY_AT -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toLong() + (MangaTable.inLibraryAt greater value) or ((MangaTable.inLibraryAt eq value) and (MangaTable.id greater id)) + } + LAST_FETCHED_AT -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toLong() + (MangaTable.lastFetchedAt greater value) or ((MangaTable.lastFetchedAt eq value) and (MangaTable.id greater id)) + } } } override fun less(cursor: Cursor): Op { return when (this) { ID -> MangaTable.id less cursor.value.toInt() - TITLE -> MangaTable.title less cursor.value - IN_LIBRARY_AT -> MangaTable.inLibraryAt less cursor.value.toLong() - LAST_FETCHED_AT -> MangaTable.lastFetchedAt less cursor.value.toLong() + TITLE -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-') + (MangaTable.title less value) or ((MangaTable.title eq value) and (MangaTable.id less id)) + } + IN_LIBRARY_AT -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toLong() + (MangaTable.inLibraryAt less value) or ((MangaTable.inLibraryAt eq value) and (MangaTable.id less id)) + } + LAST_FETCHED_AT -> { + val id = cursor.value.substringBefore('-').toInt() + val value = cursor.value.substringAfter('-').toLong() + (MangaTable.lastFetchedAt less value) or ((MangaTable.lastFetchedAt eq value) and (MangaTable.id less id)) + } } } override fun asCursor(type: MangaType): Cursor { val value = when (this) { ID -> type.id.toString() - TITLE -> type.title - IN_LIBRARY_AT -> type.inLibraryAt.toString() - LAST_FETCHED_AT -> type.lastFetchedAt.toString() + TITLE -> type.id.toString() + "-" + type.title + IN_LIBRARY_AT -> type.id.toString() + "-" + type.inLibraryAt.toString() + LAST_FETCHED_AT -> type.id.toString() + "-" + type.lastFetchedAt.toString() } return Cursor(value) } @@ -161,7 +187,7 @@ class MangaQuery { lessThan = lessThan?.value, lessThanOrEqualTo = lessThanOrEqualTo?.value, greaterThan = greaterThan?.value, - greaterThanOrEqualTo = greaterThanOrEqualTo?.value, + greaterThanOrEqualTo = greaterThanOrEqualTo?.value ) } @@ -237,7 +263,14 @@ class MangaQuery { val orderByColumn = orderBy?.column ?: MangaTable.id val orderType = orderByType.maybeSwap(last ?: before) - res.orderBy(orderByColumn, order = orderType) + if (orderBy == MangaOrderBy.ID || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + MangaTable.id to SortOrder.ASC + ) + } } val total = res.count() From 891fb0b4794adc233273ce2286012decd646523f Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 15:27:18 -0400 Subject: [PATCH 39/49] Simplify keyset pagination --- .../graphql/queries/CategoryQuery.kt | 29 +++---------- .../tachidesk/graphql/queries/MangaQuery.kt | 41 ++++--------------- .../graphql/server/primitives/OrderBy.kt | 18 ++++++++ .../tachidesk/graphql/types/MangaType.kt | 4 +- 4 files changed, 34 insertions(+), 58 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 3fcb6057..94d5cfd7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -12,12 +12,9 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less -import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter @@ -34,6 +31,8 @@ import suwayomi.tachidesk.graphql.server.primitives.Cursor 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.greaterNotUnique +import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.CategoryNodeList import suwayomi.tachidesk.graphql.types.CategoryType @@ -65,32 +64,16 @@ class CategoryQuery { override fun greater(cursor: Cursor): Op { return when (this) { ID -> CategoryTable.id greater cursor.value.toInt() - NAME -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-') - (CategoryTable.name greater value) or ((CategoryTable.name eq value) and (CategoryTable.id greater id)) - } - ORDER -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toInt() - (CategoryTable.order greater value) or ((CategoryTable.order eq value) and (CategoryTable.id greater id)) - } + NAME -> greaterNotUnique(CategoryTable.name, CategoryTable.id, cursor, String::toString) + ORDER -> greaterNotUnique(CategoryTable.order, CategoryTable.id, cursor, String::toInt) } } override fun less(cursor: Cursor): Op { return when (this) { ID -> CategoryTable.id less cursor.value.toInt() - NAME -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-') - (CategoryTable.name less value) or ((CategoryTable.name eq value) and (CategoryTable.id less id)) - } - ORDER -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toInt() - (CategoryTable.order less value) or ((CategoryTable.order eq value) and (CategoryTable.id less id)) - } + NAME -> lessNotUnique(CategoryTable.name, CategoryTable.id, cursor, String::toString) + ORDER -> lessNotUnique(CategoryTable.order, CategoryTable.id, cursor, String::toInt) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 666936ee..95a6f48c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -12,12 +12,9 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less -import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction @@ -37,6 +34,8 @@ import suwayomi.tachidesk.graphql.server.primitives.Cursor 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.greaterNotUnique +import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaType @@ -73,42 +72,18 @@ class MangaQuery { override fun greater(cursor: Cursor): Op { return when (this) { ID -> MangaTable.id greater cursor.value.toInt() - TITLE -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-') - (MangaTable.title greater value) or ((MangaTable.title eq value) and (MangaTable.id greater id)) - } - IN_LIBRARY_AT -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toLong() - (MangaTable.inLibraryAt greater value) or ((MangaTable.inLibraryAt eq value) and (MangaTable.id greater id)) - } - LAST_FETCHED_AT -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toLong() - (MangaTable.lastFetchedAt greater value) or ((MangaTable.lastFetchedAt eq value) and (MangaTable.id greater id)) - } + TITLE -> greaterNotUnique(MangaTable.title, MangaTable.id, cursor, String::toString) + IN_LIBRARY_AT -> greaterNotUnique(MangaTable.inLibraryAt, MangaTable.id, cursor, String::toLong) + LAST_FETCHED_AT -> greaterNotUnique(MangaTable.lastFetchedAt, MangaTable.id, cursor, String::toLong) } } override fun less(cursor: Cursor): Op { return when (this) { ID -> MangaTable.id less cursor.value.toInt() - TITLE -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-') - (MangaTable.title less value) or ((MangaTable.title eq value) and (MangaTable.id less id)) - } - IN_LIBRARY_AT -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toLong() - (MangaTable.inLibraryAt less value) or ((MangaTable.inLibraryAt eq value) and (MangaTable.id less id)) - } - LAST_FETCHED_AT -> { - val id = cursor.value.substringBefore('-').toInt() - val value = cursor.value.substringAfter('-').toLong() - (MangaTable.lastFetchedAt less value) or ((MangaTable.lastFetchedAt eq value) and (MangaTable.id less id)) - } + TITLE -> lessNotUnique(MangaTable.title, MangaTable.id, cursor, String::toString) + IN_LIBRARY_AT -> lessNotUnique(MangaTable.inLibraryAt, MangaTable.id, cursor, String::toLong) + LAST_FETCHED_AT -> lessNotUnique(MangaTable.lastFetchedAt, MangaTable.id, cursor, String::toLong) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt index 4f6733c4..0a7bb7bc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -1,8 +1,14 @@ package suwayomi.tachidesk.graphql.server.primitives +import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.or interface OrderBy { val column: Column> @@ -29,3 +35,15 @@ fun SortOrder?.maybeSwap(value: Any?): SortOrder { this ?: SortOrder.ASC } } + +fun > greaterNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { + val id = cursor.value.substringBefore('-').toInt() + val value = toValue(cursor.value.substringAfter('-')) + return (column greater value) or ((column eq value) and (idColumn greater id)) +} + +fun > lessNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { + val id = cursor.value.substringBefore('-').toInt() + val value = toValue(cursor.value.substringAfter('-')) + return (column less value) or ((column eq value) and (idColumn less id)) +} \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 4abe5b27..979f0dfc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -37,8 +37,8 @@ class MangaType( val inLibrary: Boolean, val inLibraryAt: Long, val realUrl: String?, - var lastFetchedAt: Long?, - var chaptersLastFetchedAt: Long? + var lastFetchedAt: Long?, //todo + var chaptersLastFetchedAt: Long? //todo ) : Node { constructor(row: ResultRow) : this( row[MangaTable.id].value, From a90e5d13ea71310db9b2eb76f21aeecf9188a5b3 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 15:47:10 -0400 Subject: [PATCH 40/49] Complete MetaQuery --- .../tachidesk/graphql/queries/MetaQuery.kt | 162 +++++++++++++++++- .../graphql/server/primitives/OrderBy.kt | 30 +++- .../tachidesk/graphql/types/MangaType.kt | 4 +- 3 files changed, 186 insertions(+), 10 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt index 53170fa2..c760c9e0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -9,13 +9,31 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.model.table.GlobalMetaTable +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.OrderBy +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults +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.GlobalMetaItem import suwayomi.tachidesk.graphql.types.MetaItem import suwayomi.tachidesk.graphql.types.MetaNodeList -import suwayomi.tachidesk.graphql.types.MetaNodeList.Companion.toNodeList import java.util.concurrent.CompletableFuture /** @@ -32,11 +50,145 @@ class MetaQuery { return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) } - fun metas(): MetaNodeList { - val results = transaction { - GlobalMetaTable.selectAll().toList() + enum class MetaOrderBy(override val column: Column>) : OrderBy { + KEY(GlobalMetaTable.key), + VALUE(GlobalMetaTable.value); + + override fun greater(cursor: Cursor): Op { + return when (this) { + KEY -> GlobalMetaTable.key greater cursor.value + VALUE -> greaterNotUnique(GlobalMetaTable.value, GlobalMetaTable.key, cursor, String::toString) + } } - return results.map { GlobalMetaItem(it) }.toNodeList() + override fun less(cursor: Cursor): Op { + return when (this) { + KEY -> GlobalMetaTable.key less cursor.value + VALUE -> lessNotUnique(GlobalMetaTable.value, GlobalMetaTable.key, cursor, String::toString) + } + } + + override fun asCursor(type: MetaItem): Cursor { + val value = when (this) { + KEY -> type.key + VALUE -> type.key + "-" + type.value + } + return Cursor(value) + } + } + + data class MetaCondition( + val key: String? = null, + val value: String? = null + ) : HasGetOp { + override fun getOp(): Op? { + val opAnd = OpAnd() + opAnd.eq(key, GlobalMetaTable.key) + opAnd.eq(value, GlobalMetaTable.value) + + return opAnd.op + } + } + + data class MetaFilter( + val key: StringFilter? = null, + val value: StringFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: MetaFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareString(GlobalMetaTable.key, key), + andFilterWithCompareString(GlobalMetaTable.value, value) + ) + } + } + + fun metas( + condition: MetaCondition? = null, + filter: MetaFilter? = null, + orderBy: MetaOrderBy? = null, + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null + ): MetaNodeList { + val queryResults = transaction { + val res = GlobalMetaTable.selectAll() + + res.applyOps(condition, filter) + + if (orderBy != null || (last != null || before != null)) { + val orderByColumn = orderBy?.column ?: GlobalMetaTable.key + val orderType = orderByType.maybeSwap(last ?: before) + + if (orderBy == MetaOrderBy.KEY || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + GlobalMetaTable.key to SortOrder.ASC + ) + } + } + + val total = res.count() + val firstResult = res.firstOrNull()?.get(GlobalMetaTable.key) + val lastResult = res.lastOrNull()?.get(GlobalMetaTable.key) + + if (after != null) { + res.andWhere { + (orderBy ?: MetaOrderBy.KEY).greater(after) + } + } else if (before != null) { + res.andWhere { + (orderBy ?: MetaOrderBy.KEY).less(before) + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) + } + + val getAsCursor: (MetaItem) -> Cursor = (orderBy ?: MetaOrderBy.KEY)::asCursor + + val resultsAsType = queryResults.results.map { GlobalMetaItem(it) } + + return MetaNodeList( + resultsAsType, + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOfNotNull( + resultsAsType.firstOrNull()?.let { + MetaNodeList.MetaEdge( + getAsCursor(it), + it + ) + }, + resultsAsType.lastOrNull()?.let { + MetaNodeList.MetaEdge( + getAsCursor(it), + it + ) + } + ) + }, + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.key, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.key, + 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/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt index 0a7bb7bc..a70ceecd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -36,14 +36,38 @@ fun SortOrder?.maybeSwap(value: Any?): SortOrder { } } -fun > greaterNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { +fun > greaterNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { val id = cursor.value.substringBefore('-').toInt() val value = toValue(cursor.value.substringAfter('-')) return (column greater value) or ((column eq value) and (idColumn greater id)) } -fun > lessNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { +@JvmName("greaterNotUniqueStringKey") +fun > greaterNotUnique( + column: Column, + idColumn: Column, + cursor: Cursor, + toValue: (String) -> T +): Op { + val id = cursor.value.substringBefore('-') + val value = toValue(cursor.value.substringAfter('-')) + return (column greater value) or ((column eq value) and (idColumn greater id)) +} + +fun > lessNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { val id = cursor.value.substringBefore('-').toInt() val value = toValue(cursor.value.substringAfter('-')) return (column less value) or ((column eq value) and (idColumn less id)) -} \ No newline at end of file +} + +@JvmName("lessNotUniqueStringKey") +fun > lessNotUnique( + column: Column, + idColumn: Column, + cursor: Cursor, + toValue: (String) -> T +): Op { + val id = cursor.value.substringBefore('-') + val value = toValue(cursor.value.substringAfter('-')) + return (column less value) or ((column eq value) and (idColumn less id)) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index 979f0dfc..9631d854 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -37,8 +37,8 @@ class MangaType( val inLibrary: Boolean, val inLibraryAt: Long, val realUrl: String?, - var lastFetchedAt: Long?, //todo - var chaptersLastFetchedAt: Long? //todo + var lastFetchedAt: Long?, // todo + var chaptersLastFetchedAt: Long? // todo ) : Node { constructor(row: ResultRow) : this( row[MangaTable.id].value, From cf73804c7162749cbf99045acd8570a8970e4875 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 16:42:55 -0400 Subject: [PATCH 41/49] Complete ExtensionQuery --- .../graphql/queries/ExtensionQuery.kt | 212 ++++++++++++++++-- .../tachidesk/graphql/queries/MetaQuery.kt | 3 +- .../graphql/server/primitives/OrderBy.kt | 22 +- .../tachidesk/graphql/types/ExtensionType.kt | 8 +- 4 files changed, 220 insertions(+), 25 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt index ff1b0f8a..4a87a983 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -9,24 +9,37 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp +import suwayomi.tachidesk.graphql.queries.filter.IntFilter +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare +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.OrderBy +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults +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.ExtensionNodeList -import suwayomi.tachidesk.graphql.types.ExtensionNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.manga.model.table.ExtensionTable import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Installed - * - HasUpdate - * - Obsolete - * - IsNsfw - * - In Pkg name list - * - Query name - * - Sort? - * - Paged Queries * * TODO Mutations * - Install @@ -39,11 +52,184 @@ class ExtensionQuery { return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) } - fun extensions(): ExtensionNodeList { - val results = transaction { - ExtensionTable.selectAll().toList() + enum class ExtensionOrderBy(override val column: Column>) : OrderBy { + PKG_NAME(ExtensionTable.pkgName), + NAME(ExtensionTable.name), + APK_NAME(ExtensionTable.apkName); + + override fun greater(cursor: Cursor): Op { + return when (this) { + PKG_NAME -> ExtensionTable.pkgName greater cursor.value + NAME -> greaterNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString) + APK_NAME -> greaterNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString) + } } - return results.map { ExtensionType(it) }.toNodeList() + override fun less(cursor: Cursor): Op { + return when (this) { + PKG_NAME -> ExtensionTable.pkgName less cursor.value + NAME -> lessNotUnique(ExtensionTable.name, ExtensionTable.pkgName, cursor, String::toString) + APK_NAME -> lessNotUnique(ExtensionTable.apkName, ExtensionTable.pkgName, cursor, String::toString) + } + } + + override fun asCursor(type: ExtensionType): Cursor { + val value = when (this) { + PKG_NAME -> type.pkgName + NAME -> type.pkgName + "\\-" + type.name + APK_NAME -> type.pkgName + "\\-" + type.apkName + } + return Cursor(value) + } + } + + data class ExtensionCondition( + val apkName: String? = null, + val iconUrl: String? = null, + val name: String? = null, + val pkgName: String? = null, + val versionName: String? = null, + val versionCode: Int? = null, + val lang: String? = null, + val isNsfw: Boolean? = null, + val isInstalled: Boolean? = null, + val hasUpdate: Boolean? = null, + val isObsolete: Boolean? = null + ) : HasGetOp { + override fun getOp(): Op? { + val opAnd = OpAnd() + opAnd.eq(apkName, ExtensionTable.apkName) + opAnd.eq(iconUrl, ExtensionTable.iconUrl) + opAnd.eq(name, ExtensionTable.name) + opAnd.eq(versionName, ExtensionTable.versionName) + opAnd.eq(versionCode, ExtensionTable.versionCode) + opAnd.eq(lang, ExtensionTable.lang) + opAnd.eq(isNsfw, ExtensionTable.isNsfw) + opAnd.eq(isInstalled, ExtensionTable.isInstalled) + opAnd.eq(hasUpdate, ExtensionTable.hasUpdate) + opAnd.eq(isObsolete, ExtensionTable.isObsolete) + + return opAnd.op + } + } + + data class ExtensionFilter( + val apkName: StringFilter? = null, + val iconUrl: StringFilter? = null, + val name: StringFilter? = null, + val pkgName: StringFilter? = null, + val versionName: StringFilter? = null, + val versionCode: IntFilter? = null, + val lang: StringFilter? = null, + val isNsfw: BooleanFilter? = null, + val isInstalled: BooleanFilter? = null, + val hasUpdate: BooleanFilter? = null, + val isObsolete: BooleanFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: ExtensionFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareString(ExtensionTable.apkName, apkName), + andFilterWithCompareString(ExtensionTable.iconUrl, iconUrl), + andFilterWithCompareString(ExtensionTable.name, name), + andFilterWithCompareString(ExtensionTable.pkgName, pkgName), + andFilterWithCompareString(ExtensionTable.versionName, versionName), + andFilterWithCompare(ExtensionTable.versionCode, versionCode), + andFilterWithCompareString(ExtensionTable.lang, lang), + andFilterWithCompare(ExtensionTable.isNsfw, isNsfw), + andFilterWithCompare(ExtensionTable.isInstalled, isInstalled), + andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate), + andFilterWithCompare(ExtensionTable.isObsolete, isObsolete) + ) + } + } + + fun extensions( + condition: ExtensionCondition? = null, + filter: ExtensionFilter? = null, + orderBy: ExtensionOrderBy? = null, + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null + ): ExtensionNodeList { + val queryResults = transaction { + val res = ExtensionTable.selectAll() + + res.applyOps(condition, filter) + + if (orderBy != null || (last != null || before != null)) { + val orderByColumn = orderBy?.column ?: ExtensionTable.pkgName + val orderType = orderByType.maybeSwap(last ?: before) + + if (orderBy == ExtensionOrderBy.PKG_NAME || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + ExtensionTable.pkgName to SortOrder.ASC + ) + } + } + + val total = res.count() + val firstResult = res.firstOrNull()?.get(ExtensionTable.pkgName) + val lastResult = res.lastOrNull()?.get(ExtensionTable.pkgName) + + if (after != null) { + res.andWhere { + (orderBy ?: ExtensionOrderBy.PKG_NAME).greater(after) + } + } else if (before != null) { + res.andWhere { + (orderBy ?: ExtensionOrderBy.PKG_NAME).less(before) + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) + } + + val getAsCursor: (ExtensionType) -> Cursor = (orderBy ?: ExtensionOrderBy.PKG_NAME)::asCursor + + val resultsAsType = queryResults.results.map { ExtensionType(it) } + + return ExtensionNodeList( + resultsAsType, + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOfNotNull( + resultsAsType.firstOrNull()?.let { + ExtensionNodeList.ExtensionEdge( + getAsCursor(it), + it + ) + }, + resultsAsType.lastOrNull()?.let { + ExtensionNodeList.ExtensionEdge( + getAsCursor(it), + it + ) + } + ) + }, + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.pkgName, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.pkgName, + 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/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt index c760c9e0..f934ce2e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -38,7 +38,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - In list of keys * * TODO Mutations * - Add/update meta @@ -71,7 +70,7 @@ class MetaQuery { override fun asCursor(type: MetaItem): Cursor { val value = when (this) { KEY -> type.key - VALUE -> type.key + "-" + type.value + VALUE -> type.key + "\\-" + type.value } return Cursor(value) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt index a70ceecd..5b3e5f0c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -36,7 +36,12 @@ fun SortOrder?.maybeSwap(value: Any?): SortOrder { } } -fun > greaterNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { +fun > greaterNotUnique( + column: Column, + idColumn: Column>, + cursor: Cursor, + toValue: (String) -> T +): Op { val id = cursor.value.substringBefore('-').toInt() val value = toValue(cursor.value.substringAfter('-')) return (column greater value) or ((column eq value) and (idColumn greater id)) @@ -49,12 +54,17 @@ fun > greaterNotUnique( cursor: Cursor, toValue: (String) -> T ): Op { - val id = cursor.value.substringBefore('-') - val value = toValue(cursor.value.substringAfter('-')) + val id = cursor.value.substringBefore("\\-") + val value = toValue(cursor.value.substringAfter("\\-")) return (column greater value) or ((column eq value) and (idColumn greater id)) } -fun > lessNotUnique(column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T): Op { +fun > lessNotUnique( + column: Column, + idColumn: Column>, + cursor: Cursor, + toValue: (String) -> T +): Op { val id = cursor.value.substringBefore('-').toInt() val value = toValue(cursor.value.substringAfter('-')) return (column less value) or ((column eq value) and (idColumn less id)) @@ -67,7 +77,7 @@ fun > lessNotUnique( cursor: Cursor, toValue: (String) -> T ): Op { - val id = cursor.value.substringBefore('-') - val value = toValue(cursor.value.substringAfter('-')) + val id = cursor.value.substringBefore("\\-") + val value = toValue(cursor.value.substringAfter("\\-")) return (column less value) or ((column eq value) and (idColumn less id)) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt index 1c7c11a0..14d36f89 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ExtensionType.kt @@ -29,9 +29,9 @@ class ExtensionType( val lang: String, val isNsfw: Boolean, - val installed: Boolean, + val isInstalled: Boolean, val hasUpdate: Boolean, - val obsolete: Boolean + val isObsolete: Boolean ) : Node { constructor(row: ResultRow) : this( apkName = row[ExtensionTable.apkName], @@ -42,9 +42,9 @@ class ExtensionType( versionCode = row[ExtensionTable.versionCode], lang = row[ExtensionTable.lang], isNsfw = row[ExtensionTable.isNsfw], - installed = row[ExtensionTable.isInstalled], + isInstalled = row[ExtensionTable.isInstalled], hasUpdate = row[ExtensionTable.hasUpdate], - obsolete = row[ExtensionTable.isObsolete] + isObsolete = row[ExtensionTable.isObsolete] ) fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { From c80f488a13a19903c0ceaf013b6d8443613ae6f4 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 20:40:18 -0400 Subject: [PATCH 42/49] Complete ChapterQuery --- .../tachidesk/graphql/queries/ChapterQuery.kt | 297 ++++++++++++++---- .../graphql/queries/filter/Filter.kt | 38 ++- .../tachidesk/graphql/types/ChapterType.kt | 3 + 3 files changed, 275 insertions(+), 63 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 1363d99c..162b9f3d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -9,19 +9,41 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.FloatFilter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp +import suwayomi.tachidesk.graphql.queries.filter.IntFilter +import suwayomi.tachidesk.graphql.queries.filter.LongFilter +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity +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.OrderBy +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults +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.ChapterNodeList -import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Filter by scanlators + * - Filter in library * - Get page list? * * TODO Mutations @@ -37,68 +59,221 @@ class ChapterQuery { return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) } - enum class ChapterSort { - SOURCE_ORDER, - NAME, - UPLOAD_DATE, - CHAPTER_NUMBER, - LAST_READ_AT, - FETCHED_AT - } + enum class ChapterOrderBy(override val column: Column>) : OrderBy { + ID(ChapterTable.id), + SOURCE_ORDER(ChapterTable.sourceOrder), + NAME(ChapterTable.name), + UPLOAD_DATE(ChapterTable.date_upload), + CHAPTER_NUMBER(ChapterTable.chapter_number), + LAST_READ_AT(ChapterTable.lastReadAt), + FETCHED_AT(ChapterTable.fetchedAt); - data class ChapterQueryInput( - val ids: List? = null, - val mangaIds: List? = null, - val read: Boolean? = null, - val bookmarked: Boolean? = null, - val downloaded: Boolean? = null, - val sort: ChapterSort? = null, - val sortOrder: SortOrder? = null, - val page: Int? = null, - val count: Int? = null - ) - - fun chapters(input: ChapterQueryInput? = null): ChapterNodeList { - val results = transaction { - var res = ChapterTable.selectAll() - - if (input != null) { - if (input.mangaIds != null) { - res.andWhere { ChapterTable.manga inList input.mangaIds } - } - if (input.ids != null) { - res.andWhere { ChapterTable.id inList input.ids } - } - if (input.read != null) { - res.andWhere { ChapterTable.isRead eq input.read } - } - if (input.bookmarked != null) { - res.andWhere { ChapterTable.isBookmarked eq input.bookmarked } - } - if (input.downloaded != null) { - res.andWhere { ChapterTable.isDownloaded eq input.downloaded } - } - val orderBy = when (input.sort) { - ChapterSort.SOURCE_ORDER, null -> ChapterTable.sourceOrder - ChapterSort.NAME -> ChapterTable.name - ChapterSort.UPLOAD_DATE -> ChapterTable.date_upload - ChapterSort.CHAPTER_NUMBER -> ChapterTable.chapter_number - ChapterSort.LAST_READ_AT -> ChapterTable.lastReadAt - ChapterSort.FETCHED_AT -> ChapterTable.fetchedAt - } - res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC) - - if (input.count != null) { - val offset = if (input.page == null) 0 else (input.page * input.count).toLong() - res.limit(input.count, offset) - } - } else { - res.orderBy(ChapterTable.sourceOrder) + override fun greater(cursor: Cursor): Op { + return when (this) { + ID -> ChapterTable.id greater cursor.value.toInt() + SOURCE_ORDER -> greaterNotUnique(ChapterTable.sourceOrder, ChapterTable.id, cursor, String::toInt) + NAME -> greaterNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString) + UPLOAD_DATE -> greaterNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong) + CHAPTER_NUMBER -> greaterNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat) + LAST_READ_AT -> greaterNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong) + FETCHED_AT -> greaterNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong) } - - res.toList() } - return results.map { ChapterType(it) }.toNodeList() // todo paged + override fun less(cursor: Cursor): Op { + return when (this) { + ID -> ChapterTable.id less cursor.value.toInt() + SOURCE_ORDER -> lessNotUnique(ChapterTable.sourceOrder, ChapterTable.id, cursor, String::toInt) + NAME -> lessNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString) + UPLOAD_DATE -> lessNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong) + CHAPTER_NUMBER -> lessNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat) + LAST_READ_AT -> lessNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong) + FETCHED_AT -> lessNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong) + } + } + + override fun asCursor(type: ChapterType): Cursor { + val value = when (this) { + ID -> type.id.toString() + SOURCE_ORDER -> type.id.toString() + "-" + type.sourceOrder + NAME -> type.id.toString() + "-" + type.name + UPLOAD_DATE -> type.id.toString() + "-" + type.uploadDate + CHAPTER_NUMBER -> type.id.toString() + "-" + type.chapterNumber + LAST_READ_AT -> type.id.toString() + "-" + type.lastReadAt + FETCHED_AT -> type.id.toString() + "-" + type.fetchedAt + } + return Cursor(value) + } + } + + data class ChapterCondition( + val id: Int? = null, + val url: String? = null, + val name: String? = null, + val uploadDate: Long? = null, + val chapterNumber: Float? = null, + val scanlator: String? = null, + val mangaId: Int? = null, + val isRead: Boolean? = null, + val isBookmarked: Boolean? = null, + val lastPageRead: Int? = null, + val lastReadAt: Long? = null, + val sourceOrder: Int? = null, + val realUrl: String? = null, + val fetchedAt: Long? = null, + val isDownloaded: Boolean? = null, + val pageCount: Int? = null + ) : HasGetOp { + override fun getOp(): Op? { + val opAnd = OpAnd() + opAnd.eq(id, ChapterTable.id) + opAnd.eq(url, ChapterTable.url) + opAnd.eq(name, ChapterTable.name) + opAnd.eq(uploadDate, ChapterTable.date_upload) + opAnd.eq(chapterNumber, ChapterTable.chapter_number) + opAnd.eq(scanlator, ChapterTable.scanlator) + opAnd.eq(mangaId, ChapterTable.manga) + opAnd.eq(isRead, ChapterTable.isRead) + opAnd.eq(isBookmarked, ChapterTable.isBookmarked) + opAnd.eq(lastPageRead, ChapterTable.lastPageRead) + opAnd.eq(lastReadAt, ChapterTable.lastReadAt) + opAnd.eq(sourceOrder, ChapterTable.sourceOrder) + opAnd.eq(realUrl, ChapterTable.realUrl) + opAnd.eq(fetchedAt, ChapterTable.fetchedAt) + opAnd.eq(isDownloaded, ChapterTable.isDownloaded) + opAnd.eq(pageCount, ChapterTable.pageCount) + + return opAnd.op + } + } + + data class ChapterFilter( + val id: IntFilter? = null, + val url: StringFilter? = null, + val name: StringFilter? = null, + val uploadDate: LongFilter? = null, + val chapterNumber: FloatFilter? = null, + val scanlator: StringFilter? = null, + val mangaId: IntFilter? = null, + val isRead: BooleanFilter? = null, + val isBookmarked: BooleanFilter? = null, + val lastPageRead: IntFilter? = null, + val lastReadAt: LongFilter? = null, + val sourceOrder: IntFilter? = null, + val realUrl: StringFilter? = null, + val fetchedAt: LongFilter? = null, + val isDownloaded: BooleanFilter? = null, + val pageCount: IntFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: ChapterFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareEntity(ChapterTable.id, id), + andFilterWithCompareString(ChapterTable.url, url), + andFilterWithCompareString(ChapterTable.name, name), + andFilterWithCompare(ChapterTable.date_upload, uploadDate), + andFilterWithCompare(ChapterTable.chapter_number, chapterNumber), + andFilterWithCompareString(ChapterTable.scanlator, scanlator), + andFilterWithCompareEntity(ChapterTable.manga, mangaId), + andFilterWithCompare(ChapterTable.isRead, isRead), + andFilterWithCompare(ChapterTable.isBookmarked, isBookmarked), + andFilterWithCompare(ChapterTable.lastPageRead, lastPageRead), + andFilterWithCompare(ChapterTable.lastReadAt, lastReadAt), + andFilterWithCompare(ChapterTable.sourceOrder, sourceOrder), + andFilterWithCompareString(ChapterTable.realUrl, realUrl), + andFilterWithCompare(ChapterTable.fetchedAt, fetchedAt), + andFilterWithCompare(ChapterTable.isDownloaded, isDownloaded), + andFilterWithCompare(ChapterTable.pageCount, pageCount) + ) + } + } + + fun chapters( + condition: ChapterCondition? = null, + filter: ChapterFilter? = null, + orderBy: ChapterOrderBy? = null, + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null + ): ChapterNodeList { + val queryResults = transaction { + val res = ChapterTable.selectAll() + + res.applyOps(condition, filter) + + if (orderBy != null || (last != null || before != null)) { + val orderByColumn = orderBy?.column ?: ChapterTable.id + val orderType = orderByType.maybeSwap(last ?: before) + + if (orderBy == ChapterOrderBy.ID || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + ChapterTable.id to SortOrder.ASC + ) + } + } + + val total = res.count() + val firstResult = res.firstOrNull()?.get(ChapterTable.id)?.value + val lastResult = res.lastOrNull()?.get(ChapterTable.id)?.value + + if (after != null) { + res.andWhere { + (orderBy ?: ChapterOrderBy.ID).greater(after) + } + } else if (before != null) { + res.andWhere { + (orderBy ?: ChapterOrderBy.ID).less(before) + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()) + } + + val getAsCursor: (ChapterType) -> Cursor = (orderBy ?: ChapterOrderBy.ID)::asCursor + + val resultsAsType = queryResults.results.map { ChapterType(it) } + + return ChapterNodeList( + resultsAsType, + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOfNotNull( + resultsAsType.firstOrNull()?.let { + ChapterNodeList.ChapterEdge( + getAsCursor(it), + it + ) + }, + resultsAsType.lastOrNull()?.let { + ChapterNodeList.ChapterEdge( + getAsCursor(it), + it + ) + } + ) + }, + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id, + 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/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt index e66aa2a2..96458393 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -10,8 +10,6 @@ import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.QueryBuilder import org.jetbrains.exposed.sql.SqlExpressionBuilder -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.not @@ -140,6 +138,12 @@ interface ComparableScalarFilter> : ScalarFilter { val greaterThanOrEqualTo: T? } +interface ListScalarFilter> : ScalarFilter { + val hasAny: List? + val hasAll: List? + val hasNone: List? +} + data class LongFilter( override val isNull: Boolean? = null, override val equalTo: Long? = null, @@ -182,6 +186,20 @@ data class IntFilter( override val greaterThanOrEqualTo: Int? = null ) : ComparableScalarFilter +data class FloatFilter( + override val isNull: Boolean? = null, + override val equalTo: Float? = null, + override val notEqualTo: Float? = null, + override val distinctFrom: Float? = null, + override val notDistinctFrom: Float? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val lessThan: Float? = null, + override val lessThanOrEqualTo: Float? = null, + override val greaterThan: Float? = null, + override val greaterThanOrEqualTo: Float? = null +) : ComparableScalarFilter + data class StringFilter( override val isNull: Boolean? = null, override val equalTo: String? = null, @@ -220,6 +238,22 @@ data class StringFilter( val greaterThanOrEqualToInsensitive: String? = null ) : ComparableScalarFilter +data class StringListFilter( + override val isNull: Boolean? = null, + override val equalTo: String? = null, + override val notEqualTo: String? = null, + override val distinctFrom: String? = null, + override val notDistinctFrom: String? = null, + override val `in`: List? = null, + override val notIn: List? = null, + override val hasAny: List? = null, + override val hasAll: List? = null, + override val hasNone: List? = null, + val hasAnyInsensitive: List? = null, + val hasAllInsensitive: List? = null, + val hasNoneInsensitive: List? = null +) : ListScalarFilter> + @Suppress("UNCHECKED_CAST") fun andFilterWithCompareString( column: Column, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 8ec24372..321f8cf5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -32,6 +32,7 @@ class ChapterType( val lastPageRead: Int, val lastReadAt: Long, val sourceOrder: Int, + val realUrl: String?, val fetchedAt: Long, val isDownloaded: Boolean, val pageCount: Int @@ -50,6 +51,7 @@ class ChapterType( row[ChapterTable.lastPageRead], row[ChapterTable.lastReadAt], row[ChapterTable.sourceOrder], + row[ChapterTable.realUrl], row[ChapterTable.fetchedAt], row[ChapterTable.isDownloaded], row[ChapterTable.pageCount] @@ -69,6 +71,7 @@ class ChapterType( dataClass.lastPageRead, dataClass.lastReadAt, dataClass.index, + dataClass.realUrl, dataClass.fetchedAt, dataClass.downloaded, dataClass.pageCount From 050ab170193517bf38823365e7f376fabe6e3a88 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 21:05:05 -0400 Subject: [PATCH 43/49] Complete SourceQuery --- .../tachidesk/graphql/queries/SourceQuery.kt | 178 +++++++++++++++++- .../graphql/server/primitives/OrderBy.kt | 46 ++++- 2 files changed, 217 insertions(+), 7 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index 533883af..b1513072 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -9,10 +9,32 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater +import org.jetbrains.exposed.sql.SqlExpressionBuilder.less +import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter +import suwayomi.tachidesk.graphql.queries.filter.Filter +import suwayomi.tachidesk.graphql.queries.filter.HasGetOp +import suwayomi.tachidesk.graphql.queries.filter.LongFilter +import suwayomi.tachidesk.graphql.queries.filter.OpAnd +import suwayomi.tachidesk.graphql.queries.filter.StringFilter +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare +import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity +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.OrderBy +import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.graphql.server.primitives.QueryResults +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.SourceNodeList -import suwayomi.tachidesk.graphql.types.SourceNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.manga.model.table.SourceTable import java.util.concurrent.CompletableFuture @@ -35,11 +57,157 @@ class SourceQuery { return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) } - fun sources(): SourceNodeList { - val results = transaction { - SourceTable.selectAll().toList().mapNotNull { SourceType(it) } + enum class SourceOrderBy(override val column: Column>) : OrderBy { + ID(SourceTable.id), + NAME(SourceTable.name), + LANG(SourceTable.lang); + + override fun greater(cursor: Cursor): Op { + return when (this) { + ID -> SourceTable.id greater cursor.value.toLong() + NAME -> greaterNotUnique(SourceTable.name, SourceTable.id, cursor, String::toString) + LANG -> greaterNotUnique(SourceTable.lang, SourceTable.id, cursor, String::toString) + } } - return results.toNodeList() + override fun less(cursor: Cursor): Op { + return when (this) { + ID -> SourceTable.id less cursor.value.toLong() + NAME -> lessNotUnique(SourceTable.name, SourceTable.id, cursor, String::toString) + LANG -> lessNotUnique(SourceTable.lang, SourceTable.id, cursor, String::toString) + } + } + + override fun asCursor(type: SourceType): Cursor { + val value = when (this) { + ID -> type.id.toString() + NAME -> type.id.toString() + "-" + type.name + LANG -> type.id.toString() + "-" + type.lang + } + return Cursor(value) + } + } + + data class SourceCondition( + val id: Long? = null, + val name: String? = null, + val lang: String? = null, + val isNsfw: Boolean? = null + ) : HasGetOp { + override fun getOp(): Op? { + val opAnd = OpAnd() + opAnd.eq(id, SourceTable.id) + opAnd.eq(name, SourceTable.name) + opAnd.eq(lang, SourceTable.lang) + opAnd.eq(isNsfw, SourceTable.isNsfw) + + return opAnd.op + } + } + + data class SourceFilter( + val id: LongFilter? = null, + val name: StringFilter? = null, + val lang: StringFilter? = null, + val isNsfw: BooleanFilter? = null, + override val and: List? = null, + override val or: List? = null, + override val not: SourceFilter? = null + ) : Filter { + override fun getOpList(): List> { + return listOfNotNull( + andFilterWithCompareEntity(SourceTable.id, id), + andFilterWithCompareString(SourceTable.name, name), + andFilterWithCompareString(SourceTable.lang, lang), + andFilterWithCompare(SourceTable.isNsfw, isNsfw) + ) + } + } + + fun sources( + condition: SourceCondition? = null, + filter: SourceFilter? = null, + orderBy: SourceOrderBy? = null, + orderByType: SortOrder? = null, + before: Cursor? = null, + after: Cursor? = null, + first: Int? = null, + last: Int? = null, + offset: Int? = null + ): SourceNodeList { + val (queryResults, resultsAsType) = transaction { + val res = SourceTable.selectAll() + + res.applyOps(condition, filter) + + if (orderBy != null || (last != null || before != null)) { + val orderByColumn = orderBy?.column ?: SourceTable.id + val orderType = orderByType.maybeSwap(last ?: before) + + if (orderBy == SourceOrderBy.ID || orderBy == null) { + res.orderBy(orderByColumn to orderType) + } else { + res.orderBy( + orderByColumn to orderType, + SourceTable.id to SortOrder.ASC + ) + } + } + + val total = res.count() + val firstResult = res.firstOrNull()?.get(SourceTable.id)?.value + val lastResult = res.lastOrNull()?.get(SourceTable.id)?.value + + if (after != null) { + res.andWhere { + (orderBy ?: SourceOrderBy.ID).greater(after) + } + } else if (before != null) { + res.andWhere { + (orderBy ?: SourceOrderBy.ID).less(before) + } + } + + if (first != null) { + res.limit(first, offset?.toLong() ?: 0) + } else if (last != null) { + res.limit(last) + } + + QueryResults(total, firstResult, lastResult, res.toList()).let { + it to it.results.mapNotNull { SourceType(it) } + } + } + + val getAsCursor: (SourceType) -> Cursor = (orderBy ?: SourceOrderBy.ID)::asCursor + + return SourceNodeList( + resultsAsType, + if (resultsAsType.isEmpty()) { + emptyList() + } else { + listOfNotNull( + resultsAsType.firstOrNull()?.let { + SourceNodeList.SourceEdge( + getAsCursor(it), + it + ) + }, + resultsAsType.lastOrNull()?.let { + SourceNodeList.SourceEdge( + getAsCursor(it), + it + ) + } + ) + }, + pageInfo = PageInfo( + hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id, + hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id, + 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/primitives/OrderBy.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt index 5b3e5f0c..4cf89215 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/primitives/OrderBy.kt @@ -36,13 +36,34 @@ fun SortOrder?.maybeSwap(value: Any?): SortOrder { } } +@JvmName("greaterNotUniqueIntKey") fun > greaterNotUnique( column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T ): Op { - val id = cursor.value.substringBefore('-').toInt() + return greaterNotUniqueImpl(column, idColumn, cursor, String::toInt, toValue) +} + +@JvmName("greaterNotUniqueLongKey") +fun > greaterNotUnique( + column: Column, + idColumn: Column>, + cursor: Cursor, + toValue: (String) -> T +): Op { + return greaterNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue) +} + +private fun , V : Comparable> greaterNotUniqueImpl( + column: Column, + idColumn: Column>, + cursor: Cursor, + toKey: (String) -> K, + toValue: (String) -> V +): Op { + val id = toKey(cursor.value.substringBefore('-')) val value = toValue(cursor.value.substringAfter('-')) return (column greater value) or ((column eq value) and (idColumn greater id)) } @@ -59,13 +80,34 @@ fun > greaterNotUnique( return (column greater value) or ((column eq value) and (idColumn greater id)) } +@JvmName("lessNotUniqueIntKey") fun > lessNotUnique( column: Column, idColumn: Column>, cursor: Cursor, toValue: (String) -> T ): Op { - val id = cursor.value.substringBefore('-').toInt() + return lessNotUniqueImpl(column, idColumn, cursor, String::toInt, toValue) +} + +@JvmName("lessNotUniqueLongKey") +fun > lessNotUnique( + column: Column, + idColumn: Column>, + cursor: Cursor, + toValue: (String) -> T +): Op { + return lessNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue) +} + +private fun , V : Comparable> lessNotUniqueImpl( + column: Column, + idColumn: Column>, + cursor: Cursor, + toKey: (String) -> K, + toValue: (String) -> V +): Op { + val id = toKey(cursor.value.substringBefore('-')) val value = toValue(cursor.value.substringAfter('-')) return (column less value) or ((column eq value) and (idColumn less id)) } From 442e24521682354aa5000d47fd95893fd97dbebf Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 22:37:51 -0400 Subject: [PATCH 44/49] Update TODO --- .../kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index b1513072..84114389 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -41,11 +41,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - Filter by languages - * - Filter by name - * - Filter by NSFW - * - In list of ids - * - Sort? * * TODO Mutations * - Browse with filters From 313da995365d41100eec73025d927b0a9f84e275 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 22:54:56 -0400 Subject: [PATCH 45/49] Add in library filter for chapters --- .../tachidesk/graphql/queries/ChapterQuery.kt | 12 ++++++++++++ .../suwayomi/tachidesk/graphql/queries/MangaQuery.kt | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 162b9f3d..821639fc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -39,6 +39,7 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.ChapterNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.MangaTable import java.util.concurrent.CompletableFuture /** @@ -164,6 +165,7 @@ class ChapterQuery { val fetchedAt: LongFilter? = null, val isDownloaded: BooleanFilter? = null, val pageCount: IntFilter? = null, + val inLibrary: BooleanFilter? = null, override val and: List? = null, override val or: List? = null, override val not: ChapterFilter? = null @@ -188,6 +190,8 @@ class ChapterQuery { andFilterWithCompare(ChapterTable.pageCount, pageCount) ) } + + fun getLibraryOp() = andFilterWithCompare(MangaTable.inLibrary, inLibrary) } fun chapters( @@ -204,6 +208,14 @@ class ChapterQuery { val queryResults = transaction { val res = ChapterTable.selectAll() + val libraryOp = filter?.getLibraryOp() + if (libraryOp != null) { + res.adjustColumnSet { + innerJoin(MangaTable) + } + res.andWhere { libraryOp } + } + res.applyOps(condition, filter) if (orderBy != null || (last != null || before != null)) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 95a6f48c..b8375037 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -15,7 +15,6 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter @@ -224,12 +223,14 @@ class MangaQuery { offset: Int? = null ): MangaNodeList { val queryResults = transaction { - var res = MangaTable.selectAll() + val res = MangaTable.selectAll() val categoryOp = filter?.getCategoryOp() if (categoryOp != null) { - res = MangaTable.innerJoin(CategoryMangaTable) - .select { categoryOp } + res.adjustColumnSet { + innerJoin(CategoryMangaTable) + } + res.andWhere { categoryOp } } res.applyOps(condition, filter) From b617250effc103adcf915de47b9098f0f1063e22 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 8 Apr 2023 22:55:56 -0400 Subject: [PATCH 46/49] Delete updates query since the chapters query can now mimic it --- .../tachidesk/graphql/queries/UpdatesQuery.kt | 49 -------------- .../graphql/server/TachideskGraphQLSchema.kt | 4 +- .../tachidesk/graphql/types/UpdatesType.kt | 67 ------------------- 3 files changed, 1 insertion(+), 119 deletions(-) delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt deleted file mode 100644 index 949bb544..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdatesQuery.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -package suwayomi.tachidesk.graphql.queries - -import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.transactions.transaction -import suwayomi.tachidesk.graphql.types.UpdatesNodeList -import suwayomi.tachidesk.graphql.types.UpdatesNodeList.Companion.toNodeList -import suwayomi.tachidesk.graphql.types.UpdatesType -import suwayomi.tachidesk.manga.model.dataclass.PaginationFactor -import suwayomi.tachidesk.manga.model.table.ChapterTable -import suwayomi.tachidesk.manga.model.table.MangaTable - -/** - * TODO Queries - * - Maybe replace with a chapter query with a sort - * - * TODO Mutations - * - Update the library - * - Update a category - * - Reset updater - * - */ -class UpdatesQuery { - data class UpdatesQueryInput( - val page: Int - ) - - fun updates(input: UpdatesQueryInput): UpdatesNodeList { - val results = transaction { - ChapterTable.innerJoin(MangaTable) - .select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) } - .orderBy(ChapterTable.fetchedAt to SortOrder.DESC) - .limit(PaginationFactor, (input.page - 1L) * PaginationFactor) - .map { - UpdatesType(it) - } - } - - return results.toNodeList() // todo paged - } -} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 292aa016..dbefa851 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -19,7 +19,6 @@ import suwayomi.tachidesk.graphql.queries.ExtensionQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.queries.MetaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery -import suwayomi.tachidesk.graphql.queries.UpdatesQuery import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString @@ -47,8 +46,7 @@ val schema = toSchema( TopLevelObject(CategoryQuery()), TopLevelObject(SourceQuery()), TopLevelObject(ExtensionQuery()), - TopLevelObject(MetaQuery()), - TopLevelObject(UpdatesQuery()) + TopLevelObject(MetaQuery()) ), mutations = listOf( TopLevelObject(ChapterMutation()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt deleted file mode 100644 index 7260e5a2..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdatesType.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -package suwayomi.tachidesk.graphql.types - -import org.jetbrains.exposed.sql.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 - -class UpdatesType( - val manga: MangaType, - val chapter: ChapterType -) : Node { - constructor(row: ResultRow) : this( - manga = MangaType(row), - chapter = ChapterType(row) - ) -} - -data class UpdatesNodeList( - override val nodes: List, - override val edges: List, - override val pageInfo: PageInfo, - override val totalCount: Int -) : NodeList() { - data class UpdatesEdge( - override val cursor: Cursor, - override val node: UpdatesType - ) : Edge() - - companion object { - fun List.toNodeList(): UpdatesNodeList { - return UpdatesNodeList( - 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( - UpdatesEdge( - cursor = Cursor("0"), - node = first() - ), - UpdatesEdge( - cursor = Cursor(lastIndex.toString()), - node = last() - ) - ) - } - } -} From 988853be63fdb0e462018ee087a91b0de078895d Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 28 Apr 2023 21:28:18 -0400 Subject: [PATCH 47/49] Seems like this should return null if it errors --- .../tachidesk/graphql/server/JavalinGraphQLRequestParser.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt index c2497cd2..efd5daad 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt @@ -14,10 +14,10 @@ import java.io.IOException class JavalinGraphQLRequestParser : GraphQLRequestParser { - @Suppress("BlockingMethodInNonBlockingContext", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") - override suspend fun parseRequest(context: Context): GraphQLServerRequest = try { + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override suspend fun parseRequest(context: Context): GraphQLServerRequest? = try { context.bodyAsClass(GraphQLServerRequest::class.java) } catch (e: IOException) { - throw IOException("Unable to parse GraphQL payload.") + null } } From da8ca2349688237cba6d390c65ef32c62f9c1be3 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 28 Apr 2023 21:29:06 -0400 Subject: [PATCH 48/49] Start working on mutations --- .../graphql/mutations/ChapterMutation.kt | 95 +++++++++++-------- .../tachidesk/graphql/queries/ChapterQuery.kt | 8 -- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt index c1019cd6..9e75b86e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -1,76 +1,91 @@ package suwayomi.tachidesk.graphql.mutations +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader import graphql.schema.DataFetchingEnvironment -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.batchInsert -import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.types.ChapterType -import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable import java.time.Instant import java.util.concurrent.CompletableFuture +/** + * TODO Mutations + * - Check for updates? + * - Download + * - Delete download + */ class ChapterMutation { - data class MetaTypeInput( - val key: String, - val value: String? - ) - - data class ChapterAttributesInput( + data class UpdateChapterPatch( val isBookmarked: Boolean? = null, val isRead: Boolean? = null, - val lastPageRead: Int? = null, - val meta: List? = null + val lastPageRead: Int? = null ) + data class UpdateChapterPayload( + val clientMutationId: String?, + val chapter: ChapterType + ) data class UpdateChapterInput( - val ids: List, - val attributes: ChapterAttributesInput + val clientMutationId: String?, + val id: Int, + val patch: UpdateChapterPatch ) - fun updateChapters(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput): CompletableFuture> { - val (ids, attributes) = input + data class UpdateChaptersPayload( + val clientMutationId: String?, + val chapters: List + ) + data class UpdateChaptersInput( + val clientMutationId: String?, + val ids: List, + val patch: UpdateChapterPatch + ) + private fun updateChapters(ids: List, patch: UpdateChapterPatch) { transaction { - if (attributes.isRead != null || attributes.isBookmarked != null || attributes.lastPageRead != null) { + if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) { val now = Instant.now().epochSecond ChapterTable.update({ ChapterTable.id inList ids }) { update -> - attributes.isRead?.also { + patch.isRead?.also { update[isRead] = it } - attributes.isBookmarked?.also { + patch.isBookmarked?.also { update[isBookmarked] = it } - attributes.lastPageRead?.also { + patch.lastPageRead?.also { update[lastPageRead] = it update[lastReadAt] = now } } } - - if (attributes.meta != null) { - attributes.meta.forEach { metaItem -> - // Delete any existing values - // Even when updating, it is easier to just delete all and create new - ChapterMetaTable.deleteWhere { - (key eq metaItem.key) and (ref inList ids) - } - if (metaItem.value != null) { - ChapterMetaTable.batchInsert(ids) { chapterId -> - this[ChapterMetaTable.ref] = chapterId - this[ChapterMetaTable.key] = metaItem.key - this[ChapterMetaTable.value] = metaItem.value - } - } - } - } } + } - return dataFetchingEnvironment.getValuesFromDataLoader("ChapterDataLoader", ids) + fun updateChapter(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput): CompletableFuture { + val (clientMutationId, id, patch) = input + + updateChapters(listOf(id), patch) + + return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id).thenApply { chapter -> + UpdateChapterPayload( + clientMutationId = clientMutationId, + chapter = chapter + ) + } + } + + fun updateChapters(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChaptersInput): CompletableFuture { + val (clientMutationId, ids, patch) = input + + updateChapters(ids, patch) + + return dataFetchingEnvironment.getValuesFromDataLoader("ChapterDataLoader", ids).thenApply { chapters -> + UpdateChaptersPayload( + clientMutationId = clientMutationId, + chapters = chapters + ) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 821639fc..f3aedcfd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -46,14 +46,6 @@ import java.util.concurrent.CompletableFuture * TODO Queries * - Filter in library * - Get page list? - * - * TODO Mutations - * - Last page read - * - Read status - * - bookmark status - * - Check for updates? - * - Download - * - Delete download */ class ChapterQuery { fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { From 4577bbc572ac1a73e000e2cdbb52f447e0344ef2 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 28 Apr 2023 21:56:25 -0400 Subject: [PATCH 49/49] More mutations --- .../graphql/mutations/ChapterMutation.kt | 4 +- .../graphql/mutations/MangaMutation.kt | 84 +++++++++++++++++++ .../tachidesk/graphql/queries/MangaQuery.kt | 11 --- .../graphql/server/TachideskGraphQLSchema.kt | 4 +- 4 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt index 9e75b86e..808f4188 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -28,7 +28,7 @@ class ChapterMutation { val chapter: ChapterType ) data class UpdateChapterInput( - val clientMutationId: String?, + val clientMutationId: String? = null, val id: Int, val patch: UpdateChapterPatch ) @@ -38,7 +38,7 @@ class ChapterMutation { val chapters: List ) data class UpdateChaptersInput( - val clientMutationId: String?, + val clientMutationId: String? = null, val ids: List, val patch: UpdateChapterPatch ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt new file mode 100644 index 00000000..86612e14 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt @@ -0,0 +1,84 @@ +package suwayomi.tachidesk.graphql.mutations + +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update +import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.manga.model.table.MangaTable +import java.util.concurrent.CompletableFuture + +/** + * TODO Mutations + * - Add to category + * - Remove from category + * - Check for updates + * - Download x(all = -1) chapters + * - Delete read/all downloaded chapters + * - Add/update meta + * - Delete meta + */ +class MangaMutation { + data class UpdateMangaPatch( + val inLibrary: Boolean? = null + ) + + data class UpdateMangaPayload( + val clientMutationId: String?, + val manga: MangaType + ) + data class UpdateMangaInput( + val clientMutationId: String? = null, + val id: Int, + val patch: UpdateMangaPatch + ) + + data class UpdateMangasPayload( + val clientMutationId: String?, + val mangas: List + ) + data class UpdateMangasInput( + val clientMutationId: String?? = null, + val ids: List, + val patch: UpdateMangaPatch + ) + + private fun updateMangas(ids: List, patch: UpdateMangaPatch) { + transaction { + if (patch.inLibrary != null) { + MangaTable.update({ MangaTable.id inList ids }) { update -> + patch.inLibrary.also { + update[inLibrary] = it + } + } + } + } + } + + fun updateManga(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateMangaInput): CompletableFuture { + val (clientMutationId, id, patch) = input + + updateMangas(listOf(id), patch) + + return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id).thenApply { manga -> + UpdateMangaPayload( + clientMutationId = clientMutationId, + manga = manga + ) + } + } + + fun updateMangas(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateMangasInput): CompletableFuture { + val (clientMutationId, ids, patch) = input + + updateMangas(ids, patch) + + return dataFetchingEnvironment.getValuesFromDataLoader("MangaDataLoader", ids).thenApply { mangas -> + UpdateMangasPayload( + clientMutationId = clientMutationId, + mangas = mangas + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index b8375037..b8bf6140 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -45,17 +45,6 @@ import java.util.concurrent.CompletableFuture /** * TODO Queries - * - * TODO Mutations - * - Favorite - * - Unfavorite - * - Add to category - * - Remove from category - * - Check for updates - * - Download x(all = -1) chapters - * - Delete read/all downloaded chapters - * - Add/update meta - * - Delete meta */ class MangaQuery { fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index dbefa851..b8e1cb3e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -13,6 +13,7 @@ import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorH import com.expediagroup.graphql.generator.toSchema import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.ChapterMutation +import suwayomi.tachidesk.graphql.mutations.MangaMutation import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.ExtensionQuery @@ -49,7 +50,8 @@ val schema = toSchema( TopLevelObject(MetaQuery()) ), mutations = listOf( - TopLevelObject(ChapterMutation()) + TopLevelObject(ChapterMutation()), + TopLevelObject(MangaMutation()) ), subscriptions = listOf( TopLevelObject(DownloadSubscription())