Add mutations to update multiple metas (#1874)

This commit is contained in:
schroda
2026-02-08 21:04:19 +01:00
committed by GitHub
parent c50b5e7448
commit f3d6bb4f22
6 changed files with 648 additions and 0 deletions
@@ -1,13 +1,17 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
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.like
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
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.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.or
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
@@ -16,6 +20,7 @@ import suwayomi.tachidesk.graphql.directives.RequireAuth
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
import suwayomi.tachidesk.graphql.types.MetaInput
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
@@ -88,6 +93,138 @@ class CategoryMutation {
DeleteCategoryMetaPayload(clientMutationId, meta, category) DeleteCategoryMetaPayload(clientMutationId, meta, category)
} }
data class SetCategoryMetasItem(
val categoryIds: List<Int>,
val metas: List<MetaInput>,
)
data class SetCategoryMetasInput(
val clientMutationId: String? = null,
val items: List<SetCategoryMetasItem>,
)
data class SetCategoryMetasPayload(
val clientMutationId: String?,
val metas: List<CategoryMetaType>,
val categories: List<CategoryType>,
)
@RequireAuth
fun setCategoryMetas(input: SetCategoryMetasInput): DataFetcherResult<SetCategoryMetasPayload?> =
asDataFetcherResult {
val (clientMutationId, items) = input
val metaByCategoryId =
items
.flatMap { item ->
val metaMap = item.metas.associate { it.key to it.value }
item.categoryIds.map { categoryId -> categoryId to metaMap }
}.groupBy({ it.first }, { it.second })
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
Category.modifyCategoriesMetas(metaByCategoryId)
val allCategoryIds = metaByCategoryId.keys
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
val (updatedMetas, categories) =
transaction {
val updatedMetas =
CategoryMetaTable
.selectAll()
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
.map { CategoryMetaType(it) }
val categories =
CategoryTable
.selectAll()
.where { CategoryTable.id inList allCategoryIds }
.map { CategoryType(it) }
.distinctBy { it.id }
updatedMetas to categories
}
SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
}
data class DeleteCategoryMetasItem(
val categoryIds: List<Int>,
val keys: List<String>? = null,
val prefixes: List<String>? = null,
)
data class DeleteCategoryMetasInput(
val clientMutationId: String? = null,
val items: List<DeleteCategoryMetasItem>,
)
data class DeleteCategoryMetasPayload(
val clientMutationId: String?,
val metas: List<CategoryMetaType>,
val categories: List<CategoryType>,
)
@RequireAuth
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DataFetcherResult<DeleteCategoryMetasPayload?> =
asDataFetcherResult {
val (clientMutationId, items) = input
items.forEach { item ->
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
"Either 'keys' or 'prefixes' must be provided for each item"
}
}
val (allDeletedMetas, allCategoryIds) =
transaction {
val deletedMetas = mutableListOf<CategoryMetaType>()
val categoryIds = mutableSetOf<Int>()
items.forEach { item ->
val keyCondition: Op<Boolean>? =
item.keys?.takeIf { it.isNotEmpty() }?.let { CategoryMetaTable.key inList it }
val prefixCondition: Op<Boolean>? =
item.prefixes
?.filter { it.isNotEmpty() }
?.map { (CategoryMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
?.reduceOrNull { acc, op -> acc or op }
val metaKeyCondition =
if (keyCondition != null && prefixCondition != null) {
keyCondition or prefixCondition
} else {
keyCondition ?: prefixCondition!!
}
val condition = (CategoryMetaTable.ref inList item.categoryIds) and metaKeyCondition
deletedMetas +=
CategoryMetaTable
.selectAll()
.where { condition }
.map { CategoryMetaType(it) }
CategoryMetaTable.deleteWhere { condition }
categoryIds += item.categoryIds
}
deletedMetas to categoryIds
}
val categories =
transaction {
CategoryTable
.selectAll()
.where { CategoryTable.id inList allCategoryIds }
.map { CategoryType(it) }
.distinctBy { it.id }
}
DeleteCategoryMetasPayload(clientMutationId, allDeletedMetas, categories)
}
data class UpdateCategoryPatch( data class UpdateCategoryPatch(
val name: String? = null, val name: String? = null,
val default: Boolean? = null, val default: Boolean? = null,
@@ -4,9 +4,14 @@ import graphql.execution.DataFetcherResult
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
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.like
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.or
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
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
@@ -15,6 +20,7 @@ import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
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.graphql.types.MetaInput
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
@@ -246,6 +252,138 @@ class ChapterMutation {
DeleteChapterMetaPayload(clientMutationId, meta, chapter) DeleteChapterMetaPayload(clientMutationId, meta, chapter)
} }
data class SetChapterMetasItem(
val chapterIds: List<Int>,
val metas: List<MetaInput>,
)
data class SetChapterMetasInput(
val clientMutationId: String? = null,
val items: List<SetChapterMetasItem>,
)
data class SetChapterMetasPayload(
val clientMutationId: String?,
val metas: List<ChapterMetaType>,
val chapters: List<ChapterType>,
)
@RequireAuth
fun setChapterMetas(input: SetChapterMetasInput): DataFetcherResult<SetChapterMetasPayload?> =
asDataFetcherResult {
val (clientMutationId, items) = input
val metaByChapterId =
items
.flatMap { item ->
val metaMap = item.metas.associate { it.key to it.value }
item.chapterIds.map { chapterId -> chapterId to metaMap }
}.groupBy({ it.first }, { it.second })
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
Chapter.modifyChaptersMetas(metaByChapterId)
val allChapterIds = metaByChapterId.keys
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
val (updatedMetas, chapters) =
transaction {
val updatedMetas =
ChapterMetaTable
.selectAll()
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
.map { ChapterMetaType(it) }
val chapters =
ChapterTable
.selectAll()
.where { ChapterTable.id inList allChapterIds }
.map { ChapterType(it) }
.distinctBy { it.id }
updatedMetas to chapters
}
SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
}
data class DeleteChapterMetasItem(
val chapterIds: List<Int>,
val keys: List<String>? = null,
val prefixes: List<String>? = null,
)
data class DeleteChapterMetasInput(
val clientMutationId: String? = null,
val items: List<DeleteChapterMetasItem>,
)
data class DeleteChapterMetasPayload(
val clientMutationId: String?,
val metas: List<ChapterMetaType>,
val chapters: List<ChapterType>,
)
@RequireAuth
fun deleteChapterMetas(input: DeleteChapterMetasInput): DataFetcherResult<DeleteChapterMetasPayload?> =
asDataFetcherResult {
val (clientMutationId, items) = input
items.forEach { item ->
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
"Either 'keys' or 'prefixes' must be provided for each item"
}
}
val (allDeletedMetas, allChapterIds) =
transaction {
val deletedMetas = mutableListOf<ChapterMetaType>()
val chapterIds = mutableSetOf<Int>()
items.forEach { item ->
val keyCondition: Op<Boolean>? =
item.keys?.takeIf { it.isNotEmpty() }?.let { ChapterMetaTable.key inList it }
val prefixCondition: Op<Boolean>? =
item.prefixes
?.filter { it.isNotEmpty() }
?.map { (ChapterMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
?.reduceOrNull { acc, op -> acc or op }
val metaKeyCondition =
if (keyCondition != null && prefixCondition != null) {
keyCondition or prefixCondition
} else {
keyCondition ?: prefixCondition!!
}
val condition = (ChapterMetaTable.ref inList item.chapterIds) and metaKeyCondition
deletedMetas +=
ChapterMetaTable
.selectAll()
.where { condition }
.map { ChapterMetaType(it) }
ChapterMetaTable.deleteWhere { condition }
chapterIds += item.chapterIds
}
deletedMetas to chapterIds
}
val chapters =
transaction {
ChapterTable
.selectAll()
.where { ChapterTable.id inList allChapterIds }
.map { ChapterType(it) }
.distinctBy { it.id }
}
DeleteChapterMetasPayload(clientMutationId, allDeletedMetas, chapters)
}
data class FetchChapterPagesInput( data class FetchChapterPagesInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val chapterId: Int, val chapterId: Int,
@@ -1,9 +1,14 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
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.like
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.or
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
@@ -11,6 +16,7 @@ import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
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.graphql.types.MetaInput
import suwayomi.tachidesk.manga.impl.Library import suwayomi.tachidesk.manga.impl.Library
import suwayomi.tachidesk.manga.impl.Manga import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
@@ -226,4 +232,138 @@ class MangaMutation {
DeleteMangaMetaPayload(clientMutationId, meta, manga) DeleteMangaMetaPayload(clientMutationId, meta, manga)
} }
} }
data class SetMangaMetasItem(
val mangaIds: List<Int>,
val metas: List<MetaInput>,
)
data class SetMangaMetasInput(
val clientMutationId: String? = null,
val items: List<SetMangaMetasItem>,
)
data class SetMangaMetasPayload(
val clientMutationId: String?,
val metas: List<MangaMetaType>,
val mangas: List<MangaType>,
)
@RequireAuth
fun setMangaMetas(input: SetMangaMetasInput): DataFetcherResult<SetMangaMetasPayload?> {
val (clientMutationId, items) = input
return asDataFetcherResult {
val metaByMangaId =
items
.flatMap { item ->
val metaMap = item.metas.associate { it.key to it.value }
item.mangaIds.map { mangaId -> mangaId to metaMap }
}.groupBy({ it.first }, { it.second })
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
Manga.modifyMangasMetas(metaByMangaId)
val allMangaIds = metaByMangaId.keys
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
val (updatedMetas, mangas) =
transaction {
val updatedMetas =
MangaMetaTable
.selectAll()
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
.map { MangaMetaType(it) }
val mangas =
MangaTable
.selectAll()
.where { MangaTable.id inList allMangaIds }
.map { MangaType(it) }
.distinctBy { it.id }
updatedMetas to mangas
}
SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
}
}
data class DeleteMangaMetasItem(
val mangaIds: List<Int>,
val keys: List<String>? = null,
val prefixes: List<String>? = null,
)
data class DeleteMangaMetasInput(
val clientMutationId: String? = null,
val items: List<DeleteMangaMetasItem>,
)
data class DeleteMangaMetasPayload(
val clientMutationId: String?,
val metas: List<MangaMetaType>,
val mangas: List<MangaType>,
)
@RequireAuth
fun deleteMangaMetas(input: DeleteMangaMetasInput): DataFetcherResult<DeleteMangaMetasPayload?> {
val (clientMutationId, items) = input
return asDataFetcherResult {
items.forEach { item ->
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
"Either 'keys' or 'prefixes' must be provided for each item"
}
}
val (allDeletedMetas, allMangaIds) =
transaction {
val deletedMetas = mutableListOf<MangaMetaType>()
val mangaIds = mutableSetOf<Int>()
items.forEach { item ->
val keyCondition: Op<Boolean>? =
item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it }
val prefixCondition: Op<Boolean>? =
item.prefixes
?.filter { it.isNotEmpty() }
?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
?.reduceOrNull { acc, op -> acc or op }
val metaKeyCondition =
if (keyCondition != null && prefixCondition != null) {
keyCondition or prefixCondition
} else {
keyCondition ?: prefixCondition!!
}
val condition = (MangaMetaTable.ref inList item.mangaIds) and metaKeyCondition
deletedMetas +=
MangaMetaTable
.selectAll()
.where { condition }
.map { MangaMetaType(it) }
MangaMetaTable.deleteWhere { condition }
mangaIds += item.mangaIds
}
deletedMetas to mangaIds
}
val mangas =
transaction {
MangaTable
.selectAll()
.where { MangaTable.id inList allMangaIds }
.map { MangaType(it) }
.distinctBy { it.id }
}
DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
}
}
} }
@@ -1,8 +1,13 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
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.like
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.or
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 suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
@@ -10,6 +15,7 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType
import suwayomi.tachidesk.graphql.types.MetaInput
class MetaMutation { class MetaMutation {
data class SetGlobalMetaInput( data class SetGlobalMetaInput(
@@ -68,4 +74,86 @@ class MetaMutation {
DeleteGlobalMetaPayload(clientMutationId, meta) DeleteGlobalMetaPayload(clientMutationId, meta)
} }
} }
data class SetGlobalMetasInput(
val clientMutationId: String? = null,
val metas: List<MetaInput>,
)
data class SetGlobalMetasPayload(
val clientMutationId: String?,
val metas: List<GlobalMetaType>,
)
@RequireAuth
fun setGlobalMetas(input: SetGlobalMetasInput): DataFetcherResult<SetGlobalMetasPayload?> {
val (clientMutationId, metas) = input
return asDataFetcherResult {
val metaMap = metas.associate { it.key to it.value }
GlobalMeta.modifyMetas(metaMap)
val updatedMetas =
transaction {
GlobalMetaTable
.selectAll()
.where { GlobalMetaTable.key inList metaMap.keys }
.map { GlobalMetaType(it) }
}
SetGlobalMetasPayload(clientMutationId, updatedMetas)
}
}
data class DeleteGlobalMetasInput(
val clientMutationId: String? = null,
val keys: List<String>? = null,
val prefixes: List<String>? = null,
)
data class DeleteGlobalMetasPayload(
val clientMutationId: String?,
val metas: List<GlobalMetaType>,
)
@RequireAuth
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DataFetcherResult<DeleteGlobalMetasPayload?> {
val (clientMutationId, keys, prefixes) = input
return asDataFetcherResult {
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
"Either 'keys' or 'prefixes' must be provided"
}
val metas =
transaction {
val keyCondition: Op<Boolean>? = keys?.takeIf { it.isNotEmpty() }?.let { GlobalMetaTable.key inList it }
val prefixCondition: Op<Boolean>? =
prefixes
?.filter { it.isNotEmpty() }
?.map { (GlobalMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
?.reduceOrNull { acc, op -> acc or op }
val finalCondition =
if (keyCondition != null && prefixCondition != null) {
keyCondition or prefixCondition
} else {
keyCondition ?: prefixCondition!!
}
val metas =
GlobalMetaTable
.selectAll()
.where { finalCondition }
.map { GlobalMetaType(it) }
GlobalMetaTable.deleteWhere { finalCondition }
metas
}
DeleteGlobalMetasPayload(clientMutationId, metas)
}
}
} }
@@ -6,15 +6,21 @@ 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 graphql.execution.DataFetcherResult
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
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.like
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.or
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 suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
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.MetaInput
import suwayomi.tachidesk.graphql.types.Preference import suwayomi.tachidesk.graphql.types.Preference
import suwayomi.tachidesk.graphql.types.SourceMetaType import suwayomi.tachidesk.graphql.types.SourceMetaType
import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.graphql.types.SourceType
@@ -98,6 +104,140 @@ class SourceMutation {
} }
} }
data class SetSourceMetasItem(
val sourceIds: List<Long>,
val metas: List<MetaInput>,
)
data class SetSourceMetasInput(
val clientMutationId: String? = null,
val items: List<SetSourceMetasItem>,
)
data class SetSourceMetasPayload(
val clientMutationId: String?,
val metas: List<SourceMetaType>,
val sources: List<SourceType>,
)
@RequireAuth
fun setSourceMetas(input: SetSourceMetasInput): DataFetcherResult<SetSourceMetasPayload?> {
val (clientMutationId, items) = input
return asDataFetcherResult {
val metaBySourceId =
items
.flatMap { item ->
val metaMap = item.metas.associate { it.key to it.value }
item.sourceIds.map { sourceId -> sourceId to metaMap }
}.groupBy({ it.first }, { it.second })
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
Source.modifySourceMetas(metaBySourceId)
val allSourceIds = metaBySourceId.keys
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
val (updatedMetas, sources) =
transaction {
val updatedMetas =
SourceMetaTable
.selectAll()
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
.map { SourceMetaType(it) }
val sources =
SourceTable
.selectAll()
.where { SourceTable.id inList allSourceIds }
.mapNotNull { SourceType(it) }
.distinctBy { it.id }
updatedMetas to sources
}
SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
}
}
data class DeleteSourceMetasItem(
val sourceIds: List<Long>,
val keys: List<String>? = null,
val prefixes: List<String>? = null,
)
data class DeleteSourceMetasInput(
val clientMutationId: String? = null,
val items: List<DeleteSourceMetasItem>,
)
data class DeleteSourceMetasPayload(
val clientMutationId: String?,
val metas: List<SourceMetaType>,
val sources: List<SourceType>,
)
@RequireAuth
fun deleteSourceMetas(input: DeleteSourceMetasInput): DataFetcherResult<DeleteSourceMetasPayload?> {
val (clientMutationId, items) = input
return asDataFetcherResult {
items.forEach { item ->
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
"Either 'keys' or 'prefixes' must be provided for each item"
}
}
val (allDeletedMetas, allSourceIds) =
transaction {
val deletedMetas = mutableListOf<SourceMetaType>()
val sourceIds = mutableSetOf<Long>()
items.forEach { item ->
val keyCondition: Op<Boolean>? =
item.keys?.takeIf { it.isNotEmpty() }?.let { SourceMetaTable.key inList it }
val prefixCondition: Op<Boolean>? =
item.prefixes
?.filter { it.isNotEmpty() }
?.map { (SourceMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
?.reduceOrNull { acc, op -> acc or op }
val metaKeyCondition =
if (keyCondition != null && prefixCondition != null) {
keyCondition or prefixCondition
} else {
keyCondition ?: prefixCondition!!
}
val condition = (SourceMetaTable.ref inList item.sourceIds) and metaKeyCondition
deletedMetas +=
SourceMetaTable
.selectAll()
.where { condition }
.map { SourceMetaType(it) }
SourceMetaTable.deleteWhere { condition }
sourceIds += item.sourceIds
}
deletedMetas to sourceIds
}
val sources =
transaction {
SourceTable
.selectAll()
.where { SourceTable.id inList allSourceIds }
.mapNotNull { SourceType(it) }
.distinctBy { it.id }
}
DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources)
}
}
enum class FetchSourceMangaType { enum class FetchSourceMangaType {
SEARCH, SEARCH,
POPULAR, POPULAR,
@@ -20,6 +20,11 @@ interface MetaType : Node {
val value: String val value: String
} }
data class MetaInput(
val key: String,
val value: String,
)
class ChapterMetaType( class ChapterMetaType(
override val key: String, override val key: String,
override val value: String, override val value: String,