Category Mutations (#566)

* Complete Category mutations

* Remove TODO
This commit is contained in:
Mitchell Syer
2023-06-05 09:18:57 -04:00
committed by GitHub
parent 51bfdc0947
commit 300c0a8f35
5 changed files with 340 additions and 14 deletions
@@ -1,24 +1,29 @@
package suwayomi.tachidesk.graphql.mutations
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.batchInsert
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.graphql.types.MangaType
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.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable
/**
* TODO Mutations
* - Name
* - Order
* - Default
* - Create
* - Delete
*/
class CategoryMutation {
data class SetCategoryMetaInput(
val clientMutationId: String? = null,
@@ -72,4 +77,324 @@ class CategoryMutation {
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
* - Add to category
* - Remove from category
* - Download x(all = -1) 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.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.table.CategoryTable
import java.util.concurrent.CompletableFuture
@@ -22,13 +23,15 @@ class CategoryType(
val id: Int,
val order: Int,
val name: String,
val default: Boolean
val default: Boolean,
val includeInUpdate: IncludeInUpdate
) : Node {
constructor(row: ResultRow) : this(
row[CategoryTable.id].value,
row[CategoryTable.order],
row[CategoryTable.name],
row[CategoryTable.isDefault]
row[CategoryTable.isDefault],
IncludeInUpdate.fromValue(row[CategoryTable.includeInUpdate])
)
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
@@ -84,7 +84,7 @@ object Category {
}
/** make sure category order numbers starts from 1 and is consecutive */
private fun normalizeCategories() {
fun normalizeCategories() {
transaction {
CategoryTable.selectAll()
.orderBy(CategoryTable.order to SortOrder.ASC)
@@ -9,6 +9,6 @@ package suwayomi.tachidesk.manga.impl.util.lang
import org.jetbrains.exposed.sql.Query
fun Query.isEmpty() = this.count() == 0L
fun Query.isEmpty() = this.empty()
fun Query.isNotEmpty() = !this.isEmpty()