mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 11:24:35 -05:00
@@ -1,24 +1,29 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
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.minus
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.batchInsert
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
|
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 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.manga.impl.Category
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
|
||||||
|
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
|
||||||
|
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
|
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO Mutations
|
|
||||||
* - Name
|
|
||||||
* - Order
|
|
||||||
* - Default
|
|
||||||
* - Create
|
|
||||||
* - Delete
|
|
||||||
*/
|
|
||||||
class CategoryMutation {
|
class CategoryMutation {
|
||||||
data class SetCategoryMetaInput(
|
data class SetCategoryMetaInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
@@ -72,4 +77,324 @@ class CategoryMutation {
|
|||||||
|
|
||||||
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UpdateCategoryPatch(
|
||||||
|
val name: String? = null,
|
||||||
|
val default: Boolean? = null,
|
||||||
|
val includeInUpdate: IncludeInUpdate? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateCategoryPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val category: CategoryType
|
||||||
|
)
|
||||||
|
data class UpdateCategoryInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val id: Int,
|
||||||
|
val patch: UpdateCategoryPatch
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateCategoriesPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val categories: List<CategoryType>
|
||||||
|
)
|
||||||
|
data class UpdateCategoriesInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val ids: List<Int>,
|
||||||
|
val patch: UpdateCategoryPatch
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun updateCategories(ids: List<Int>, patch: UpdateCategoryPatch) {
|
||||||
|
transaction {
|
||||||
|
if (patch.name != null) {
|
||||||
|
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
|
||||||
|
patch.name.also {
|
||||||
|
update[name] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (patch.default != null) {
|
||||||
|
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
|
||||||
|
patch.default.also {
|
||||||
|
update[isDefault] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (patch.includeInUpdate != null) {
|
||||||
|
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
|
||||||
|
patch.includeInUpdate.also {
|
||||||
|
update[includeInUpdate] = it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload {
|
||||||
|
val (clientMutationId, id, patch) = input
|
||||||
|
|
||||||
|
updateCategories(listOf(id), patch)
|
||||||
|
|
||||||
|
val category = transaction {
|
||||||
|
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateCategoryPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
category = category
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload {
|
||||||
|
val (clientMutationId, ids, patch) = input
|
||||||
|
|
||||||
|
updateCategories(ids, patch)
|
||||||
|
|
||||||
|
val categories = transaction {
|
||||||
|
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateCategoriesPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
categories = categories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateCategoryOrderPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val categories: List<CategoryType>
|
||||||
|
)
|
||||||
|
data class UpdateCategoryOrderInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val id: Int,
|
||||||
|
val position: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload {
|
||||||
|
val (clientMutationId, categoryId, position) = input
|
||||||
|
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 {
|
||||||
|
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateCategoryOrderPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
categories = categories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreateCategoryInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val order: Int? = null,
|
||||||
|
val default: Boolean? = null,
|
||||||
|
val includeInUpdate: IncludeInUpdate? = null
|
||||||
|
)
|
||||||
|
data class CreateCategoryPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val category: CategoryType
|
||||||
|
)
|
||||||
|
fun createCategory(
|
||||||
|
input: CreateCategoryInput
|
||||||
|
): CreateCategoryPayload {
|
||||||
|
val (clientMutationId, name, order, default, includeInUpdate) = input
|
||||||
|
transaction {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Category.normalizeCategories()
|
||||||
|
|
||||||
|
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateCategoryPayload(clientMutationId, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteCategoryInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val categoryId: Int
|
||||||
|
)
|
||||||
|
data class DeleteCategoryPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val category: CategoryType?,
|
||||||
|
val mangas: List<MangaType>
|
||||||
|
)
|
||||||
|
fun deleteCategory(
|
||||||
|
input: DeleteCategoryInput
|
||||||
|
): DeleteCategoryPayload {
|
||||||
|
val (clientMutationId, categoryId) = input
|
||||||
|
if (categoryId == 0) { // Don't delete default category
|
||||||
|
return DeleteCategoryPayload(
|
||||||
|
clientMutationId,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateMangaCategoriesPatch(
|
||||||
|
val clearCategories: Boolean? = null,
|
||||||
|
val addToCategories: List<Int>? = null,
|
||||||
|
val removeFromCategories: List<Int>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateMangaCategoriesPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val manga: MangaType
|
||||||
|
)
|
||||||
|
data class UpdateMangaCategoriesInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val id: Int,
|
||||||
|
val patch: UpdateMangaCategoriesPatch
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateMangasCategoriesPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val mangas: List<MangaType>
|
||||||
|
)
|
||||||
|
data class UpdateMangasCategoriesInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val ids: List<Int>,
|
||||||
|
val patch: UpdateMangaCategoriesPatch
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun updateMangas(ids: List<Int>, patch: UpdateMangaCategoriesPatch) {
|
||||||
|
transaction {
|
||||||
|
if (patch.clearCategories == true) {
|
||||||
|
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids }
|
||||||
|
} else if (!patch.removeFromCategories.isNullOrEmpty()) {
|
||||||
|
CategoryMangaTable.deleteWhere {
|
||||||
|
(CategoryMangaTable.manga inList ids) and (CategoryMangaTable.category inList patch.removeFromCategories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!patch.addToCategories.isNullOrEmpty()) {
|
||||||
|
val newCategories = buildList {
|
||||||
|
ids.forEach { mangaId ->
|
||||||
|
patch.addToCategories.forEach { categoryId ->
|
||||||
|
val existingMapping = CategoryMangaTable.select {
|
||||||
|
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
|
||||||
|
}.isNotEmpty()
|
||||||
|
|
||||||
|
if (!existingMapping) {
|
||||||
|
add(mangaId to categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryMangaTable.batchInsert(newCategories) { (manga, category) ->
|
||||||
|
this[CategoryMangaTable.manga] = manga
|
||||||
|
this[CategoryMangaTable.category] = category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload {
|
||||||
|
val (clientMutationId, id, patch) = input
|
||||||
|
|
||||||
|
updateMangas(listOf(id), patch)
|
||||||
|
|
||||||
|
val manga = transaction {
|
||||||
|
MangaType(MangaTable.select { MangaTable.id eq id }.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateMangaCategoriesPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
manga = manga
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload {
|
||||||
|
val (clientMutationId, ids, patch) = input
|
||||||
|
|
||||||
|
updateMangas(ids, patch)
|
||||||
|
|
||||||
|
val mangas = transaction {
|
||||||
|
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateMangasCategoriesPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
mangas = mangas
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import java.util.concurrent.CompletableFuture
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO Mutations
|
* TODO Mutations
|
||||||
* - Add to category
|
|
||||||
* - Remove from category
|
|
||||||
* - Download x(all = -1) chapters
|
* - Download x(all = -1) chapters
|
||||||
* - Delete read/all downloaded chapters
|
* - Delete read/all downloaded chapters
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Edge
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
|
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -22,13 +23,15 @@ class CategoryType(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val order: Int,
|
val order: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val default: Boolean
|
val default: Boolean,
|
||||||
|
val includeInUpdate: IncludeInUpdate
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
row[CategoryTable.id].value,
|
row[CategoryTable.id].value,
|
||||||
row[CategoryTable.order],
|
row[CategoryTable.order],
|
||||||
row[CategoryTable.name],
|
row[CategoryTable.name],
|
||||||
row[CategoryTable.isDefault]
|
row[CategoryTable.isDefault],
|
||||||
|
IncludeInUpdate.fromValue(row[CategoryTable.includeInUpdate])
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
|
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ object Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** make sure category order numbers starts from 1 and is consecutive */
|
/** make sure category order numbers starts from 1 and is consecutive */
|
||||||
private fun normalizeCategories() {
|
fun normalizeCategories() {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.selectAll()
|
CategoryTable.selectAll()
|
||||||
.orderBy(CategoryTable.order to SortOrder.ASC)
|
.orderBy(CategoryTable.order to SortOrder.ASC)
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ package suwayomi.tachidesk.manga.impl.util.lang
|
|||||||
|
|
||||||
import org.jetbrains.exposed.sql.Query
|
import org.jetbrains.exposed.sql.Query
|
||||||
|
|
||||||
fun Query.isEmpty() = this.count() == 0L
|
fun Query.isEmpty() = this.empty()
|
||||||
|
|
||||||
fun Query.isNotEmpty() = !this.isEmpty()
|
fun Query.isNotEmpty() = !this.isEmpty()
|
||||||
|
|||||||
Reference in New Issue
Block a user