Support partial mutation responses (#954)

In case e.g. a mutation was made which looked like this

myMutation {
  mutationA { ... }
  mutationB { ... }
  mutationC { ... }
}

and mutation A and B succeeded while mutation C failed, the response only included the error of C and the successful mutation data response of A and B was missing
This commit is contained in:
schroda
2024-06-03 02:33:17 +02:00
committed by GitHub
parent fc2f5ffdf9
commit ff23f58a4f
11 changed files with 737 additions and 593 deletions

View File

@@ -0,0 +1,25 @@
package suwayomi.tachidesk.graphql
import com.expediagroup.graphql.server.extensions.toGraphQLError
import graphql.execution.DataFetcherResult
import mu.KotlinLogging
val logger = KotlinLogging.logger { }
inline fun <T> asDataFetcherResult(block: () -> T): DataFetcherResult<T?> {
val result =
runCatching {
block()
}
if (result.isFailure) {
logger.error(result.exceptionOrNull()) { "asDataFetcherResult: failed due to" }
return DataFetcherResult.newResult<T?>()
.error(result.exceptionOrNull()?.toGraphQLError())
.build()
}
return DataFetcherResult.newResult<T?>()
.data(result.getOrNull())
.build()
}

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
@@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.CategoryMetaType import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
@@ -36,12 +38,14 @@ class CategoryMutation {
val meta: CategoryMetaType, val meta: CategoryMetaType,
) )
fun setCategoryMeta(input: SetCategoryMetaInput): SetCategoryMetaPayload { fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult<SetCategoryMetaPayload?> {
val (clientMutationId, meta) = input return asDataFetcherResult {
val (clientMutationId, meta) = input
Category.modifyMeta(meta.categoryId, meta.key, meta.value) Category.modifyMeta(meta.categoryId, meta.key, meta.value)
return SetCategoryMetaPayload(clientMutationId, meta) SetCategoryMetaPayload(clientMutationId, meta)
}
} }
data class DeleteCategoryMetaInput( data class DeleteCategoryMetaInput(
@@ -56,30 +60,32 @@ class CategoryMutation {
val category: CategoryType, val category: CategoryType,
) )
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DeleteCategoryMetaPayload { fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult<DeleteCategoryMetaPayload?> {
val (clientMutationId, categoryId, key) = input return asDataFetcherResult {
val (clientMutationId, categoryId, key) = input
val (meta, category) = val (meta, category) =
transaction { transaction {
val meta = val meta =
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
.firstOrNull() .firstOrNull()
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
val category = val category =
transaction { transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first()) CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first())
} }
if (meta != null) { if (meta != null) {
CategoryMetaType(meta) CategoryMetaType(meta)
} else { } else {
null null
} to category } to category
} }
return DeleteCategoryMetaPayload(clientMutationId, meta, category) DeleteCategoryMetaPayload(clientMutationId, meta, category)
}
} }
data class UpdateCategoryPatch( data class UpdateCategoryPatch(
@@ -147,36 +153,40 @@ class CategoryMutation {
} }
} }
fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload { fun updateCategory(input: UpdateCategoryInput): DataFetcherResult<UpdateCategoryPayload?> {
val (clientMutationId, id, patch) = input return asDataFetcherResult {
val (clientMutationId, id, patch) = input
updateCategories(listOf(id), patch) updateCategories(listOf(id), patch)
val category = val category =
transaction { transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first()) CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
} }
return UpdateCategoryPayload( UpdateCategoryPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
category = category, category = category,
) )
}
} }
fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload { fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult<UpdateCategoriesPayload?> {
val (clientMutationId, ids, patch) = input return asDataFetcherResult {
val (clientMutationId, ids, patch) = input
updateCategories(ids, patch) updateCategories(ids, patch)
val categories = val categories =
transaction { transaction {
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) } CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
} }
return UpdateCategoriesPayload( UpdateCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
categories = categories, categories = categories,
) )
}
} }
data class UpdateCategoryOrderPayload( data class UpdateCategoryOrderPayload(
@@ -190,46 +200,48 @@ class CategoryMutation {
val position: Int, val position: Int,
) )
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload { fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult<UpdateCategoryOrderPayload?> {
val (clientMutationId, categoryId, position) = input return asDataFetcherResult {
require(position > 0) { val (clientMutationId, categoryId, position) = input
"'order' must not be <= 0" require(position > 0) {
} "'order' must not be <= 0"
transaction {
val currentOrder =
CategoryTable
.select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
if (currentOrder != position) {
if (position < currentOrder) {
CategoryTable.update({ CategoryTable.order greaterEq position }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
} else {
CategoryTable.update({ CategoryTable.order lessEq position }) {
it[CategoryTable.order] = CategoryTable.order - 1
}
}
CategoryTable.update({ CategoryTable.id eq categoryId }) {
it[CategoryTable.order] = position
}
} }
}
Category.normalizeCategories()
val categories =
transaction { transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) } val currentOrder =
CategoryTable
.select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
if (currentOrder != position) {
if (position < currentOrder) {
CategoryTable.update({ CategoryTable.order greaterEq position }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
} else {
CategoryTable.update({ CategoryTable.order lessEq position }) {
it[CategoryTable.order] = CategoryTable.order - 1
}
}
CategoryTable.update({ CategoryTable.id eq categoryId }) {
it[CategoryTable.order] = position
}
}
} }
return UpdateCategoryOrderPayload( Category.normalizeCategories()
clientMutationId = clientMutationId,
categories = categories, val categories =
) transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
}
UpdateCategoryOrderPayload(
clientMutationId = clientMutationId,
categories = categories,
)
}
} }
data class CreateCategoryInput( data class CreateCategoryInput(
@@ -246,51 +258,53 @@ class CategoryMutation {
val category: CategoryType, val category: CategoryType,
) )
fun createCategory(input: CreateCategoryInput): CreateCategoryPayload { fun createCategory(input: CreateCategoryInput): DataFetcherResult<CreateCategoryPayload?> {
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input return asDataFetcherResult {
transaction { val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
"'name' must be unique"
}
}
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
}
if (order != null) {
require(order > 0) {
"'order' must not be <= 0"
}
}
val category =
transaction { transaction {
if (order != null) { require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
CategoryTable.update({ CategoryTable.order greaterEq order }) { "'name' must be unique"
it[CategoryTable.order] = CategoryTable.order + 1 }
}
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
}
if (order != null) {
require(order > 0) {
"'order' must not be <= 0"
}
}
val category =
transaction {
if (order != null) {
CategoryTable.update({ CategoryTable.order greaterEq order }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
} }
val id =
CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
if (includeInDownload != null) {
it[CategoryTable.includeInDownload] = includeInDownload.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
} }
val id = CreateCategoryPayload(clientMutationId, category)
CategoryTable.insertAndGetId { }
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
if (includeInDownload != null) {
it[CategoryTable.includeInDownload] = includeInDownload.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return CreateCategoryPayload(clientMutationId, category)
} }
data class DeleteCategoryInput( data class DeleteCategoryInput(
@@ -304,41 +318,43 @@ class CategoryMutation {
val mangas: List<MangaType>, val mangas: List<MangaType>,
) )
fun deleteCategory(input: DeleteCategoryInput): DeleteCategoryPayload { fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult<DeleteCategoryPayload?> {
val (clientMutationId, categoryId) = input return asDataFetcherResult {
if (categoryId == 0) { // Don't delete default category val (clientMutationId, categoryId) = input
return DeleteCategoryPayload( if (categoryId == 0) { // Don't delete default category
clientMutationId, return@asDataFetcherResult DeleteCategoryPayload(
null, clientMutationId,
emptyList(), null,
) emptyList(),
} )
val (category, mangas) =
transaction {
val category =
CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val mangas =
transaction {
MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
} }
return DeleteCategoryPayload(clientMutationId, category, mangas) val (category, mangas) =
transaction {
val category =
CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val mangas =
transaction {
MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
}
DeleteCategoryPayload(clientMutationId, category, mangas)
}
} }
data class UpdateMangaCategoriesPatch( data class UpdateMangaCategoriesPatch(
@@ -406,35 +422,39 @@ class CategoryMutation {
} }
} }
fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload { fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult<UpdateMangaCategoriesPayload?> {
val (clientMutationId, id, patch) = input return asDataFetcherResult {
val (clientMutationId, id, patch) = input
updateMangas(listOf(id), patch) updateMangas(listOf(id), patch)
val manga = val manga =
transaction { transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first()) MangaType(MangaTable.select { MangaTable.id eq id }.first())
} }
return UpdateMangaCategoriesPayload( UpdateMangaCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
manga = manga, manga = manga,
) )
}
} }
fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload { fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult<UpdateMangasCategoriesPayload?> {
val (clientMutationId, ids, patch) = input return asDataFetcherResult {
val (clientMutationId, ids, patch) = input
updateMangas(ids, patch) updateMangas(ids, patch)
val mangas = val mangas =
transaction { transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) } MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
} }
return UpdateMangasCategoriesPayload( UpdateMangasCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
mangas = mangas, mangas = mangas,
) )
}
} }
} }

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
@@ -7,6 +8,7 @@ import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.ChapterMetaType
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -89,36 +91,40 @@ class ChapterMutation {
} }
} }
fun updateChapter(input: UpdateChapterInput): UpdateChapterPayload { fun updateChapter(input: UpdateChapterInput): DataFetcherResult<UpdateChapterPayload?> {
val (clientMutationId, id, patch) = input return asDataFetcherResult {
val (clientMutationId, id, patch) = input
updateChapters(listOf(id), patch) updateChapters(listOf(id), patch)
val chapter = val chapter =
transaction { transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq id }.first()) ChapterType(ChapterTable.select { ChapterTable.id eq id }.first())
} }
return UpdateChapterPayload( UpdateChapterPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapter = chapter, chapter = chapter,
) )
}
} }
fun updateChapters(input: UpdateChaptersInput): UpdateChaptersPayload { fun updateChapters(input: UpdateChaptersInput): DataFetcherResult<UpdateChaptersPayload?> {
val (clientMutationId, ids, patch) = input return asDataFetcherResult {
val (clientMutationId, ids, patch) = input
updateChapters(ids, patch) updateChapters(ids, patch)
val chapters = val chapters =
transaction { transaction {
ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) } ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) }
} }
return UpdateChaptersPayload( UpdateChaptersPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = chapters, chapters = chapters,
) )
}
} }
data class FetchChaptersInput( data class FetchChaptersInput(
@@ -131,23 +137,25 @@ class ChapterMutation {
val chapters: List<ChapterType>, val chapters: List<ChapterType>,
) )
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload> { fun fetchChapters(input: FetchChaptersInput): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
val (clientMutationId, mangaId) = input val (clientMutationId, mangaId) = input
return future { return future {
Chapter.fetchChapterList(mangaId) asDataFetcherResult {
}.thenApply { Chapter.fetchChapterList(mangaId)
val chapters =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder)
.map { ChapterType(it) }
}
FetchChaptersPayload( val chapters =
clientMutationId = clientMutationId, transaction {
chapters = chapters, ChapterTable.select { ChapterTable.manga eq mangaId }
) .orderBy(ChapterTable.sourceOrder)
.map { ChapterType(it) }
}
FetchChaptersPayload(
clientMutationId = clientMutationId,
chapters = chapters,
)
}
} }
} }
@@ -161,12 +169,14 @@ class ChapterMutation {
val meta: ChapterMetaType, val meta: ChapterMetaType,
) )
fun setChapterMeta(input: SetChapterMetaInput): SetChapterMetaPayload { fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult<SetChapterMetaPayload?> {
val (clientMutationId, meta) = input return asDataFetcherResult {
val (clientMutationId, meta) = input
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value) Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
return SetChapterMetaPayload(clientMutationId, meta) SetChapterMetaPayload(clientMutationId, meta)
}
} }
data class DeleteChapterMetaInput( data class DeleteChapterMetaInput(
@@ -181,30 +191,32 @@ class ChapterMutation {
val chapter: ChapterType, val chapter: ChapterType,
) )
fun deleteChapterMeta(input: DeleteChapterMetaInput): DeleteChapterMetaPayload { fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult<DeleteChapterMetaPayload?> {
val (clientMutationId, chapterId, key) = input return asDataFetcherResult {
val (clientMutationId, chapterId, key) = input
val (meta, chapter) = val (meta, chapter) =
transaction { transaction {
val meta = val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull() .firstOrNull()
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
val chapter = val chapter =
transaction { transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first()) ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first())
} }
if (meta != null) { if (meta != null) {
ChapterMetaType(meta) ChapterMetaType(meta)
} else { } else {
null null
} to chapter } to chapter
} }
return DeleteChapterMetaPayload(clientMutationId, meta, chapter) DeleteChapterMetaPayload(clientMutationId, meta, chapter)
}
} }
data class FetchChapterPagesInput( data class FetchChapterPagesInput(
@@ -218,20 +230,22 @@ class ChapterMutation {
val chapter: ChapterType, val chapter: ChapterType,
) )
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<FetchChapterPagesPayload> { fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
val (clientMutationId, chapterId) = input val (clientMutationId, chapterId) = input
return future { return future {
getChapterDownloadReadyById(chapterId) asDataFetcherResult {
}.thenApply { chapter -> val chapter = getChapterDownloadReadyById(chapterId)
FetchChapterPagesPayload(
clientMutationId = clientMutationId, FetchChapterPagesPayload(
pages = clientMutationId = clientMutationId,
List(chapter.pageCount) { index -> pages =
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index" List(chapter.pageCount) { index ->
}, "/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index"
chapter = ChapterType(chapter), },
) chapter = ChapterType(chapter),
)
}
} }
} }
} }

View File

@@ -1,9 +1,11 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -25,19 +27,21 @@ class DownloadMutation {
val chapters: List<ChapterType>, val chapters: List<ChapterType>,
) )
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload { fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
Chapter.deleteChapters(chapters) return asDataFetcherResult {
Chapter.deleteChapters(chapters)
return DeleteDownloadedChaptersPayload( DeleteDownloadedChaptersPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = chapters =
transaction { transaction {
ChapterTable.select { ChapterTable.id inList chapters } ChapterTable.select { ChapterTable.id inList chapters }
.map { ChapterType(it) } .map { ChapterType(it) }
}, },
) )
}
} }
data class DeleteDownloadedChapterInput( data class DeleteDownloadedChapterInput(
@@ -50,18 +54,20 @@ class DownloadMutation {
val chapters: ChapterType, val chapters: ChapterType,
) )
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload { fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult<DeleteDownloadedChapterPayload?> {
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
Chapter.deleteChapters(listOf(chapter)) return asDataFetcherResult {
Chapter.deleteChapters(listOf(chapter))
return DeleteDownloadedChapterPayload( DeleteDownloadedChapterPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = chapters =
transaction { transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first()) ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first())
}, },
) )
}
} }
data class EnqueueChapterDownloadsInput( data class EnqueueChapterDownloadsInput(
@@ -74,19 +80,23 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun enqueueChapterDownloads(input: EnqueueChapterDownloadsInput): CompletableFuture<EnqueueChapterDownloadsPayload> { fun enqueueChapterDownloads(
input: EnqueueChapterDownloadsInput,
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> {
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
return future { return future {
EnqueueChapterDownloadsPayload( asDataFetcherResult {
clientMutationId = clientMutationId, DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
downloadStatus =
withTimeout(30.seconds) { EnqueueChapterDownloadsPayload(
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } }) clientMutationId = clientMutationId,
}, downloadStatus =
) withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } })
},
)
}
} }
} }
@@ -100,19 +110,21 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<EnqueueChapterDownloadPayload> { fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
return future { return future {
EnqueueChapterDownloadPayload( asDataFetcherResult {
clientMutationId = clientMutationId, DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
downloadStatus =
withTimeout(30.seconds) { EnqueueChapterDownloadPayload(
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } }) clientMutationId = clientMutationId,
}, downloadStatus =
) withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } })
},
)
}
} }
} }
@@ -126,19 +138,23 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun dequeueChapterDownloads(input: DequeueChapterDownloadsInput): CompletableFuture<DequeueChapterDownloadsPayload> { fun dequeueChapterDownloads(
input: DequeueChapterDownloadsInput,
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> {
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
return future { return future {
DequeueChapterDownloadsPayload( asDataFetcherResult {
clientMutationId = clientMutationId, DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
downloadStatus =
withTimeout(30.seconds) { DequeueChapterDownloadsPayload(
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } }) clientMutationId = clientMutationId,
}, downloadStatus =
) withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } })
},
)
}
} }
} }
@@ -152,19 +168,21 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DequeueChapterDownloadPayload> { fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
return future { return future {
DequeueChapterDownloadPayload( asDataFetcherResult {
clientMutationId = clientMutationId, DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
downloadStatus =
withTimeout(30.seconds) { DequeueChapterDownloadPayload(
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } }) clientMutationId = clientMutationId,
}, downloadStatus =
) withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } })
},
)
}
} }
} }
@@ -177,19 +195,21 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload> { fun startDownloader(input: StartDownloaderInput): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> {
DownloadManager.start()
return future { return future {
StartDownloaderPayload( asDataFetcherResult {
input.clientMutationId, DownloadManager.start()
downloadStatus =
withTimeout(30.seconds) { StartDownloaderPayload(
DownloadStatus( input.clientMutationId,
DownloadManager.status.first { it.status == Status.Started }, downloadStatus =
) withTimeout(30.seconds) {
}, DownloadStatus(
) DownloadManager.status.first { it.status == Status.Started },
)
},
)
}
} }
} }
@@ -202,18 +222,21 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload> { fun stopDownloader(input: StopDownloaderInput): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> {
return future { return future {
DownloadManager.stop() asDataFetcherResult {
StopDownloaderPayload( DownloadManager.stop()
input.clientMutationId,
downloadStatus = StopDownloaderPayload(
withTimeout(30.seconds) { input.clientMutationId,
DownloadStatus( downloadStatus =
DownloadManager.status.first { it.status == Status.Stopped }, withTimeout(30.seconds) {
) DownloadStatus(
}, DownloadManager.status.first { it.status == Status.Stopped },
) )
},
)
}
} }
} }
@@ -226,18 +249,21 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload> { fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> {
return future { return future {
DownloadManager.clear() asDataFetcherResult {
ClearDownloaderPayload( DownloadManager.clear()
input.clientMutationId,
downloadStatus = ClearDownloaderPayload(
withTimeout(30.seconds) { input.clientMutationId,
DownloadStatus( downloadStatus =
DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() }, withTimeout(30.seconds) {
) DownloadStatus(
}, DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() },
) )
},
)
}
} }
} }
@@ -252,20 +278,23 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload> { fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
val (clientMutationId, chapter, to) = input val (clientMutationId, chapter, to) = input
DownloadManager.reorder(chapter, to)
return future { return future {
ReorderChapterDownloadPayload( asDataFetcherResult {
clientMutationId, DownloadManager.reorder(chapter, to)
downloadStatus =
withTimeout(30.seconds) { ReorderChapterDownloadPayload(
DownloadStatus( clientMutationId,
DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to }, downloadStatus =
) withTimeout(30.seconds) {
}, DownloadStatus(
) DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to },
)
},
)
}
} }
} }
} }

View File

@@ -1,9 +1,11 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import graphql.execution.DataFetcherResult
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
@@ -69,41 +71,45 @@ class ExtensionMutation {
} }
} }
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<UpdateExtensionPayload> { fun updateExtension(input: UpdateExtensionInput): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
return future { return future {
updateExtensions(listOf(id), patch) asDataFetcherResult {
}.thenApply { updateExtensions(listOf(id), patch)
val extension =
transaction {
ExtensionTable.select { ExtensionTable.pkgName eq id }.firstOrNull()
?.let { ExtensionType(it) }
}
UpdateExtensionPayload( val extension =
clientMutationId = clientMutationId, transaction {
extension = extension, ExtensionTable.select { ExtensionTable.pkgName eq id }.firstOrNull()
) ?.let { ExtensionType(it) }
}
UpdateExtensionPayload(
clientMutationId = clientMutationId,
extension = extension,
)
}
} }
} }
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<UpdateExtensionsPayload> { fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
return future { return future {
updateExtensions(ids, patch) asDataFetcherResult {
}.thenApply { updateExtensions(ids, patch)
val extensions =
transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
UpdateExtensionsPayload( val extensions =
clientMutationId = clientMutationId, transaction {
extensions = extensions, ExtensionTable.select { ExtensionTable.pkgName inList ids }
) .map { ExtensionType(it) }
}
UpdateExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions,
)
}
} }
} }
@@ -116,22 +122,24 @@ class ExtensionMutation {
val extensions: List<ExtensionType>, val extensions: List<ExtensionType>,
) )
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<FetchExtensionsPayload> { fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
val (clientMutationId) = input val (clientMutationId) = input
return future { return future {
ExtensionsList.fetchExtensions() asDataFetcherResult {
}.thenApply { ExtensionsList.fetchExtensions()
val extensions =
transaction {
ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
.map { ExtensionType(it) }
}
FetchExtensionsPayload( val extensions =
clientMutationId = clientMutationId, transaction {
extensions = extensions, ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
) .map { ExtensionType(it) }
}
FetchExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions,
)
}
} }
} }
@@ -145,18 +153,22 @@ class ExtensionMutation {
val extension: ExtensionType, val extension: ExtensionType,
) )
fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload> { fun installExternalExtension(
input: InstallExternalExtensionInput,
): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> {
val (clientMutationId, extensionFile) = input val (clientMutationId, extensionFile) = input
return future { return future {
Extension.installExternalExtension(extensionFile.content, extensionFile.filename) asDataFetcherResult {
}.thenApply { Extension.installExternalExtension(extensionFile.content, extensionFile.filename)
val dbExtension = transaction { ExtensionTable.select { ExtensionTable.apkName eq extensionFile.filename }.first() }
InstallExternalExtensionPayload( val dbExtension = transaction { ExtensionTable.select { ExtensionTable.apkName eq extensionFile.filename }.first() }
clientMutationId,
extension = ExtensionType(dbExtension), InstallExternalExtensionPayload(
) clientMutationId,
extension = ExtensionType(dbExtension),
)
}
} }
} }
} }

View File

@@ -1,7 +1,9 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
import suwayomi.tachidesk.graphql.types.UpdateState.IDLE import suwayomi.tachidesk.graphql.types.UpdateState.IDLE
@@ -22,50 +24,54 @@ class InfoMutation {
val updateStatus: WebUIUpdateStatus, val updateStatus: WebUIUpdateStatus,
) )
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload> { fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
return future { return future {
withTimeout(30.seconds) { asDataFetcherResult {
if (WebInterfaceManager.status.value.state === DOWNLOADING) { withTimeout(30.seconds) {
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value) if (WebInterfaceManager.status.value.state === DOWNLOADING) {
} return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
}
val flavor = WebUIFlavor.current val flavor = WebUIFlavor.current
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor) val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor)
if (!updateAvailable) { if (!updateAvailable) {
val didUpdateCheckFail = version.isEmpty() val didUpdateCheckFail = version.isEmpty()
return@withTimeout WebUIUpdatePayload( return@withTimeout WebUIUpdatePayload(
input.clientMutationId,
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
)
}
try {
WebInterfaceManager.startDownloadInScope(flavor, version)
} catch (e: Exception) {
// ignore since we use the status anyway
}
WebUIUpdatePayload(
input.clientMutationId, input.clientMutationId,
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE), updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
) )
} }
try {
WebInterfaceManager.startDownloadInScope(flavor, version)
} catch (e: Exception) {
// ignore since we use the status anyway
}
WebUIUpdatePayload(
input.clientMutationId,
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
)
} }
} }
} }
fun resetWebUIUpdateStatus(): CompletableFuture<WebUIUpdateStatus> { fun resetWebUIUpdateStatus(): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> {
return future { return future {
withTimeout(30.seconds) { asDataFetcherResult {
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING withTimeout(30.seconds) {
if (!isUpdateFinished) { val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"") if (!isUpdateFinished) {
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
}
WebInterfaceManager.resetStatus()
WebInterfaceManager.status.first { it.state == IDLE }
} }
WebInterfaceManager.resetStatus()
WebInterfaceManager.status.first { it.state == IDLE }
} }
} }
} }

View File

@@ -1,11 +1,13 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.graphql.types.MangaMetaType
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.impl.Library import suwayomi.tachidesk.manga.impl.Library
@@ -70,39 +72,43 @@ class MangaMutation {
} }
} }
fun updateManga(input: UpdateMangaInput): CompletableFuture<UpdateMangaPayload> { fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
return future { return future {
updateMangas(listOf(id), patch) asDataFetcherResult {
}.thenApply { updateMangas(listOf(id), patch)
val manga =
transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
UpdateMangaPayload( val manga =
clientMutationId = clientMutationId, transaction {
manga = manga, MangaType(MangaTable.select { MangaTable.id eq id }.first())
) }
UpdateMangaPayload(
clientMutationId = clientMutationId,
manga = manga,
)
}
} }
} }
fun updateMangas(input: UpdateMangasInput): CompletableFuture<UpdateMangasPayload> { fun updateMangas(input: UpdateMangasInput): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
return future { return future {
updateMangas(ids, patch) asDataFetcherResult {
}.thenApply { updateMangas(ids, patch)
val mangas =
transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
UpdateMangasPayload( val mangas =
clientMutationId = clientMutationId, transaction {
mangas = mangas, MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
) }
UpdateMangasPayload(
clientMutationId = clientMutationId,
mangas = mangas,
)
}
} }
} }
@@ -116,20 +122,22 @@ class MangaMutation {
val manga: MangaType, val manga: MangaType,
) )
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload> { fun fetchManga(input: FetchMangaInput): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
val (clientMutationId, id) = input val (clientMutationId, id) = input
return future { return future {
Manga.fetchManga(id) asDataFetcherResult {
}.thenApply { Manga.fetchManga(id)
val manga =
transaction { val manga =
MangaTable.select { MangaTable.id eq id }.first() transaction {
} MangaTable.select { MangaTable.id eq id }.first()
FetchMangaPayload( }
clientMutationId = clientMutationId, FetchMangaPayload(
manga = MangaType(manga), clientMutationId = clientMutationId,
) manga = MangaType(manga),
)
}
} }
} }
@@ -143,12 +151,14 @@ class MangaMutation {
val meta: MangaMetaType, val meta: MangaMetaType,
) )
fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload { fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult<SetMangaMetaPayload?> {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value) return asDataFetcherResult {
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
return SetMangaMetaPayload(clientMutationId, meta) SetMangaMetaPayload(clientMutationId, meta)
}
} }
data class DeleteMangaMetaInput( data class DeleteMangaMetaInput(
@@ -163,29 +173,31 @@ class MangaMutation {
val manga: MangaType, val manga: MangaType,
) )
fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload { fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult<DeleteMangaMetaPayload?> {
val (clientMutationId, mangaId, key) = input val (clientMutationId, mangaId, key) = input
val (meta, manga) = return asDataFetcherResult {
transaction { val (meta, manga) =
val meta = transaction {
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } val meta =
.firstOrNull() MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
val manga = val manga =
transaction { transaction {
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first()) MangaType(MangaTable.select { MangaTable.id eq mangaId }.first())
} }
if (meta != null) { if (meta != null) {
MangaMetaType(meta) MangaMetaType(meta)
} else { } else {
null null
} to manga } to manga
} }
return DeleteMangaMetaPayload(clientMutationId, meta, manga) DeleteMangaMetaPayload(clientMutationId, meta, manga)
}
} }
} }

View File

@@ -1,11 +1,13 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType
class MetaMutation { class MetaMutation {
@@ -19,12 +21,14 @@ class MetaMutation {
val meta: GlobalMetaType, val meta: GlobalMetaType,
) )
fun setGlobalMeta(input: SetGlobalMetaInput): SetGlobalMetaPayload { fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult<SetGlobalMetaPayload?> {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
GlobalMeta.modifyMeta(meta.key, meta.value) return asDataFetcherResult {
GlobalMeta.modifyMeta(meta.key, meta.value)
return SetGlobalMetaPayload(clientMutationId, meta) SetGlobalMetaPayload(clientMutationId, meta)
}
} }
data class DeleteGlobalMetaInput( data class DeleteGlobalMetaInput(
@@ -37,24 +41,26 @@ class MetaMutation {
val meta: GlobalMetaType?, val meta: GlobalMetaType?,
) )
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DeleteGlobalMetaPayload { fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult<DeleteGlobalMetaPayload?> {
val (clientMutationId, key) = input val (clientMutationId, key) = input
val meta = return asDataFetcherResult {
transaction { val meta =
val meta = transaction {
GlobalMetaTable.select { GlobalMetaTable.key eq key } val meta =
.firstOrNull() GlobalMetaTable.select { GlobalMetaTable.key eq key }
.firstOrNull()
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key } GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
if (meta != null) { if (meta != null) {
GlobalMetaType(meta) GlobalMetaType(meta)
} else { } else {
null null
}
} }
}
return DeleteGlobalMetaPayload(clientMutationId, meta) DeleteGlobalMetaPayload(clientMutationId, meta)
}
} }
} }

View File

@@ -5,11 +5,13 @@ import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.FilterChange
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.Preference import suwayomi.tachidesk.graphql.types.Preference
@@ -37,12 +39,14 @@ class SourceMutation {
val meta: SourceMetaType, val meta: SourceMetaType,
) )
fun setSourceMeta(input: SetSourceMetaInput): SetSourceMetaPayload { fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Source.modifyMeta(meta.sourceId, meta.key, meta.value) return asDataFetcherResult {
Source.modifyMeta(meta.sourceId, meta.key, meta.value)
return SetSourceMetaPayload(clientMutationId, meta) SetSourceMetaPayload(clientMutationId, meta)
}
} }
data class DeleteSourceMetaInput( data class DeleteSourceMetaInput(
@@ -57,31 +61,33 @@ class SourceMutation {
val source: SourceType?, val source: SourceType?,
) )
fun deleteSourceMeta(input: DeleteSourceMetaInput): DeleteSourceMetaPayload { fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
val (clientMutationId, sourceId, key) = input val (clientMutationId, sourceId, key) = input
val (meta, source) = return asDataFetcherResult {
transaction { val (meta, source) =
val meta = transaction {
SourceMetaTable.select { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) } val meta =
.firstOrNull() SourceMetaTable.select { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
.firstOrNull()
SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) } SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
val source = val source =
transaction { transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull() SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
?.let { SourceType(it) } ?.let { SourceType(it) }
} }
if (meta != null) { if (meta != null) {
SourceMetaType(meta) SourceMetaType(meta)
} else { } else {
null null
} to source } to source
} }
return DeleteSourceMetaPayload(clientMutationId, meta, source) DeleteSourceMetaPayload(clientMutationId, meta, source)
}
} }
enum class FetchSourceMangaType { enum class FetchSourceMangaType {
@@ -105,44 +111,46 @@ class SourceMutation {
val hasNextPage: Boolean, val hasNextPage: Boolean,
) )
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<FetchSourceMangaPayload> { fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
val (clientMutationId, sourceId, type, page, query, filters) = input val (clientMutationId, sourceId, type, page, query, filters) = input
return future { return future {
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!! asDataFetcherResult {
val mangasPage = val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
when (type) { val mangasPage =
FetchSourceMangaType.SEARCH -> { when (type) {
source.getSearchManga( FetchSourceMangaType.SEARCH -> {
page = page, source.getSearchManga(
query = query.orEmpty(), page = page,
filters = updateFilterList(source, filters), query = query.orEmpty(),
) filters = updateFilterList(source, filters),
)
}
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
} }
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page) val mangaIds = mangasPage.insertOrGet(sourceId)
val mangas =
transaction {
MangaTable.select { MangaTable.id inList mangaIds }
.map { MangaType(it) }
}.sortedBy {
mangaIds.indexOf(it.id)
} }
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
}
val mangaIds = mangasPage.insertOrGet(sourceId) FetchSourceMangaPayload(
clientMutationId = clientMutationId,
val mangas = mangas = mangas,
transaction { hasNextPage = mangasPage.hasNextPage,
MangaTable.select { MangaTable.id inList mangaIds } )
.map { MangaType(it) } }
}.sortedBy {
mangaIds.indexOf(it.id)
}
FetchSourceMangaPayload(
clientMutationId = clientMutationId,
mangas = mangas,
hasNextPage = mangasPage.hasNextPage,
)
} }
} }
@@ -167,27 +175,29 @@ class SourceMutation {
val source: SourceType, val source: SourceType,
) )
fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload { fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
val (clientMutationId, sourceId, change) = input val (clientMutationId, sourceId, change) = input
Source.setSourcePreference(sourceId, change.position, "") { preference -> return asDataFetcherResult {
when (preference) { Source.setSourcePreference(sourceId, change.position, "") { preference ->
is SwitchPreferenceCompat -> change.switchState when (preference) {
is CheckBoxPreference -> change.checkBoxState is SwitchPreferenceCompat -> change.switchState
is EditTextPreference -> change.editTextState is CheckBoxPreference -> change.checkBoxState
is ListPreference -> change.listState is EditTextPreference -> change.editTextState
is MultiSelectListPreference -> change.multiSelectState?.toSet() is ListPreference -> change.listState
else -> throw RuntimeException("sealed class cannot have more subtypes!") is MultiSelectListPreference -> change.multiSelectState?.toSet()
} ?: throw Exception("Expected change to ${preference::class.simpleName}") else -> throw RuntimeException("sealed class cannot have more subtypes!")
} } ?: throw Exception("Expected change to ${preference::class.simpleName}")
}
return UpdateSourcePreferencePayload( UpdateSourcePreferencePayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) }, preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
source = source =
transaction { transaction {
SourceType(SourceTable.select { SourceTable.id eq sourceId }.first())!! SourceType(SourceTable.select { SourceTable.id eq sourceId }.first())!!
}, },
) )
}
} }
} }

View File

@@ -2,9 +2,11 @@ package suwayomi.tachidesk.graphql.mutations
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.TrackRecordType import suwayomi.tachidesk.graphql.types.TrackRecordType
import suwayomi.tachidesk.graphql.types.TrackerType import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.Track
@@ -203,20 +205,22 @@ class TrackMutation {
val trackRecords: List<TrackRecordType>, val trackRecords: List<TrackRecordType>,
) )
fun trackProgress(input: TrackProgressInput): CompletableFuture<TrackProgressPayload> { fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
val (clientMutationId, mangaId) = input val (clientMutationId, mangaId) = input
return future { return future {
Track.trackChapter(mangaId) asDataFetcherResult {
val trackRecords = Track.trackChapter(mangaId)
transaction { val trackRecords =
TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId } transaction {
.toList() TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId }
} .toList()
TrackProgressPayload( }
clientMutationId, TrackProgressPayload(
trackRecords.map { TrackRecordType(it) }, clientMutationId,
) trackRecords.map { TrackRecordType(it) },
)
}
} }
} }

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -7,6 +8,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
@@ -28,7 +30,7 @@ class UpdateMutation {
val updateStatus: UpdateStatus, val updateStatus: UpdateStatus,
) )
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<UpdateLibraryMangaPayload> { fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
updater.addCategoriesToUpdateQueue( updater.addCategoriesToUpdateQueue(
Category.getCategoryList(), Category.getCategoryList(),
clear = true, clear = true,
@@ -36,13 +38,15 @@ class UpdateMutation {
) )
return future { return future {
UpdateLibraryMangaPayload( asDataFetcherResult {
input.clientMutationId, UpdateLibraryMangaPayload(
updateStatus = input.clientMutationId,
withTimeout(30.seconds) { updateStatus =
UpdateStatus(updater.status.first()) withTimeout(30.seconds) {
}, UpdateStatus(updater.status.first())
) },
)
}
} }
} }
@@ -56,7 +60,7 @@ class UpdateMutation {
val updateStatus: UpdateStatus, val updateStatus: UpdateStatus,
) )
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<UpdateCategoryMangaPayload> { fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
val categories = val categories =
transaction { transaction {
CategoryTable.select { CategoryTable.id inList input.categories }.map { CategoryTable.select { CategoryTable.id inList input.categories }.map {
@@ -66,13 +70,15 @@ class UpdateMutation {
updater.addCategoriesToUpdateQueue(categories, clear = true, forceAll = true) updater.addCategoriesToUpdateQueue(categories, clear = true, forceAll = true)
return future { return future {
UpdateCategoryMangaPayload( asDataFetcherResult {
input.clientMutationId, UpdateCategoryMangaPayload(
updateStatus = input.clientMutationId,
withTimeout(30.seconds) { updateStatus =
UpdateStatus(updater.status.first()) withTimeout(30.seconds) {
}, UpdateStatus(updater.status.first())
) },
)
}
} }
} }