Basically finish MangaQuery, only paging left

This commit is contained in:
Syer10
2023-04-03 22:04:46 -04:00
parent e8c2bad187
commit a6dddf311c
3 changed files with 455 additions and 89 deletions
@@ -9,23 +9,34 @@ package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.queries.util.GreaterOrLessThanLong
import suwayomi.tachidesk.graphql.queries.util.andWhereGreaterOrLessThen
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.getOp
import suwayomi.tachidesk.graphql.types.MangaNodeList
import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
import java.util.concurrent.CompletableFuture
/**
* TODO Queries
* - Query options(optionally query the title, description, or/and)
*
* TODO Mutations
* - Favorite
@@ -43,62 +54,130 @@ class MangaQuery {
return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id)
}
enum class MangaSort {
enum class MangaOrderBy {
ID,
TITLE,
IN_LIBRARY_AT,
LAST_FETCHED_AT
}
data class MangaQueryInput(
val ids: List<Int>? = null,
val categoryIds: List<Int>? = null,
val sourceIds: List<Long>? = null,
data class MangaCondition(
val id: Int? = null,
val sourceId: Long? = null,
val url: String? = null,
val title: String? = null,
val thumbnailUrl: String? = null,
val initialized: Boolean? = null,
val artist: String? = null,
val author: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: MangaStatus? = null,
val inLibrary: Boolean? = null,
val inLibraryAt: GreaterOrLessThanLong? = null,
val sort: MangaSort? = null,
val sortOrder: SortOrder? = null,
val page: Int? = null,
val count: Int? = null
)
val inLibraryAt: Long? = null,
val realUrl: String? = null,
var lastFetchedAt: Long? = null,
var chaptersLastFetchedAt: Long? = null
) {
fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
fun <T> eq(value: T?, column: Column<T>) = opAnd.andWhere(value) { column eq it }
fun <T : Comparable<T>> eq(value: T?, column: Column<EntityID<T>>) = opAnd.andWhere(value) { column eq it }
eq(id, MangaTable.id)
eq(sourceId, MangaTable.sourceReference)
eq(url, MangaTable.url)
eq(title, MangaTable.title)
eq(thumbnailUrl, MangaTable.thumbnail_url)
eq(initialized, MangaTable.initialized)
eq(artist, MangaTable.artist)
eq(author, MangaTable.author)
eq(description, MangaTable.description)
eq(genre?.joinToString(), MangaTable.genre)
eq(status?.value, MangaTable.status)
eq(inLibrary, MangaTable.inLibrary)
eq(inLibraryAt, MangaTable.inLibraryAt)
eq(realUrl, MangaTable.realUrl)
eq(lastFetchedAt, MangaTable.lastFetchedAt)
eq(chaptersLastFetchedAt, MangaTable.chaptersLastFetchedAt)
fun mangas(input: MangaQueryInput? = null): MangaNodeList {
return opAnd.op
}
}
data class MangaFilter(
val id: IntFilter? = null,
val sourceId: LongFilter? = null,
val url: StringFilter? = null,
val title: StringFilter? = null,
val thumbnailUrl: StringFilter? = null,
val initialized: BooleanFilter? = null,
val artist: StringFilter? = null,
val author: StringFilter? = null,
val description: StringFilter? = null,
// val genre: List<String>? = null, // todo
// val status: MangaStatus? = null, // todo
val inLibrary: BooleanFilter? = null,
val inLibraryAt: LongFilter? = null,
val realUrl: StringFilter? = null,
var lastFetchedAt: LongFilter? = null,
var chaptersLastFetchedAt: LongFilter? = null,
val category: IntFilter? = null,
override val and: List<MangaFilter>? = null,
override val or: List<MangaFilter>? = null,
override val not: MangaFilter? = null
) : Filter<MangaFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareEntity(MangaTable.id, id),
andFilterWithCompare(MangaTable.sourceReference, sourceId),
andFilterWithCompareString(MangaTable.url, url),
andFilterWithCompareString(MangaTable.title, title),
andFilterWithCompareString(MangaTable.thumbnail_url, thumbnailUrl),
andFilterWithCompare(MangaTable.initialized, initialized),
andFilterWithCompareString(MangaTable.artist, artist),
andFilterWithCompareString(MangaTable.author, author),
andFilterWithCompareString(MangaTable.description, description),
andFilterWithCompare(MangaTable.inLibrary, inLibrary),
andFilterWithCompare(MangaTable.inLibraryAt, inLibraryAt),
andFilterWithCompareString(MangaTable.realUrl, realUrl),
andFilterWithCompare(MangaTable.inLibraryAt, lastFetchedAt),
andFilterWithCompare(MangaTable.inLibraryAt, chaptersLastFetchedAt)
)
}
fun getCategoryOp() = andFilterWithCompareEntity(CategoryMangaTable.category, category)
}
fun mangas(
condition: MangaCondition? = null,
filter: MangaFilter? = null,
orderBy: MangaOrderBy? = null,
orderByType: SortOrder? = null
): MangaNodeList {
val results = transaction {
var res = MangaTable.selectAll()
if (input != null) {
if (input.categoryIds != null) {
res = MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category inList input.categoryIds }
}
if (input.ids != null) {
res.andWhere { MangaTable.id inList input.ids }
}
if (input.sourceIds != null) {
res.andWhere { MangaTable.sourceReference inList input.sourceIds }
}
if (input.inLibrary != null) {
res.andWhere { MangaTable.inLibrary eq input.inLibrary }
}
if (input.inLibraryAt != null) {
res.andWhereGreaterOrLessThen(
column = MangaTable.inLibraryAt,
greaterOrLessThan = input.inLibraryAt
)
}
if (input.sort != null) {
val orderBy = when (input.sort) {
MangaSort.ID -> MangaTable.id
MangaSort.TITLE -> MangaTable.title
MangaSort.IN_LIBRARY_AT -> MangaTable.inLibraryAt
MangaSort.LAST_FETCHED_AT -> MangaTable.lastFetchedAt
}
res.orderBy(orderBy, order = input.sortOrder ?: SortOrder.ASC)
}
if (input.count != null) {
val offset = if (input.page == null) 0 else (input.page * input.count).toLong()
res.limit(input.count, offset)
val categoryOp = filter?.getCategoryOp()
if (categoryOp != null) {
res = MangaTable.innerJoin(CategoryMangaTable)
.select { categoryOp }
}
val conditionOp = condition?.getOp()
if (conditionOp != null) {
res.andWhere { conditionOp }
}
val filterOp = filter?.getOp()
if (filterOp != null) {
res.andWhere { filterOp }
}
if (orderBy != null) {
val orderByColumn = when (orderBy) {
MangaOrderBy.ID -> MangaTable.id
MangaOrderBy.TITLE -> MangaTable.title
MangaOrderBy.IN_LIBRARY_AT -> MangaTable.inLibraryAt
MangaOrderBy.LAST_FETCHED_AT -> MangaTable.lastFetchedAt
}
res.orderBy(orderByColumn, order = orderByType ?: SortOrder.ASC)
}
res.toList()
@@ -0,0 +1,329 @@
package suwayomi.tachidesk.graphql.queries.filter
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.ComparisonOp
import org.jetbrains.exposed.sql.Expression
import org.jetbrains.exposed.sql.ExpressionWithColumnType
import org.jetbrains.exposed.sql.LikeEscapeOp
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.QueryBuilder
import org.jetbrains.exposed.sql.SqlExpressionBuilder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.wrap
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.not
import org.jetbrains.exposed.sql.or
import org.jetbrains.exposed.sql.stringParam
import org.jetbrains.exposed.sql.upperCase
class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, val escapeChar: Char?) : ComparisonOp(expr1, expr2, if (like) "ILIKE" else "NOT ILIKE") {
override fun toQueryBuilder(queryBuilder: QueryBuilder) {
super.toQueryBuilder(queryBuilder)
if (escapeChar != null) {
with(queryBuilder) {
+" ESCAPE "
+stringParam(escapeChar.toString())
}
}
}
companion object {
fun <T : String?> iLike(expression: Expression<T>, pattern: String): LikeEscapeOp = iLike(expression, LikePattern(pattern))
fun <T : String?> iNotLike(expression: Expression<T>, pattern: String): LikeEscapeOp = iNotLike(expression, LikePattern(pattern))
fun <T : String?> iLike(expression: Expression<T>, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar)
fun <T : String?> iNotLike(expression: Expression<T>, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), false, pattern.escapeChar)
}
}
class DistinctFromOp(expr1: Expression<*>, expr2: Expression<*>, not: Boolean) : ComparisonOp(expr1, expr2, if (not) "IS NOT DISTINCT FROM" else "IS DISTINCT FROM") {
companion object {
fun <T> distinctFrom(expression: ExpressionWithColumnType<T>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false
)
fun <T> notDistinctFrom(expression: ExpressionWithColumnType<T>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true
)
fun <T : Comparable<T>> distinctFrom(expression: ExpressionWithColumnType<EntityID<T>>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false
)
fun <T : Comparable<T>> notDistinctFrom(expression: ExpressionWithColumnType<EntityID<T>>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true
)
}
}
interface Filter<T : Filter<T>> {
val and: List<T>?
val or: List<T>?
val not: T?
fun getOpList(): List<Op<Boolean>>
}
interface ScalarFilter<T> {
val isNull: Boolean?
val equalTo: T?
val notEqualTo: T?
val distinctFrom: T?
val notDistinctFrom: T?
val `in`: List<T>?
val notIn: List<T>?
}
interface ComparableScalarFilter<T : Comparable<T>> : ScalarFilter<T> {
val lessThan: T?
val lessThanOrEqualTo: T?
val greaterThan: T?
val greaterThanOrEqualTo: T?
}
data class LongFilter(
override val isNull: Boolean? = null,
override val equalTo: Long? = null,
override val notEqualTo: Long? = null,
override val distinctFrom: Long? = null,
override val notDistinctFrom: Long? = null,
override val `in`: List<Long>? = null,
override val notIn: List<Long>? = null,
override val lessThan: Long? = null,
override val lessThanOrEqualTo: Long? = null,
override val greaterThan: Long? = null,
override val greaterThanOrEqualTo: Long? = null
) : ComparableScalarFilter<Long>
data class BooleanFilter(
override val isNull: Boolean? = null,
override val equalTo: Boolean? = null,
override val notEqualTo: Boolean? = null,
override val distinctFrom: Boolean? = null,
override val notDistinctFrom: Boolean? = null,
override val `in`: List<Boolean>? = null,
override val notIn: List<Boolean>? = null,
override val lessThan: Boolean? = null,
override val lessThanOrEqualTo: Boolean? = null,
override val greaterThan: Boolean? = null,
override val greaterThanOrEqualTo: Boolean? = null
) : ComparableScalarFilter<Boolean>
data class IntFilter(
override val isNull: Boolean? = null,
override val equalTo: Int? = null,
override val notEqualTo: Int? = null,
override val distinctFrom: Int? = null,
override val notDistinctFrom: Int? = null,
override val `in`: List<Int>? = null,
override val notIn: List<Int>? = null,
override val lessThan: Int? = null,
override val lessThanOrEqualTo: Int? = null,
override val greaterThan: Int? = null,
override val greaterThanOrEqualTo: Int? = null
) : ComparableScalarFilter<Int>
data class StringFilter(
override val isNull: Boolean? = null,
override val equalTo: String? = null,
override val notEqualTo: String? = null,
override val distinctFrom: String? = null,
override val notDistinctFrom: String? = null,
override val `in`: List<String>? = null,
override val notIn: List<String>? = null,
override val lessThan: String? = null,
override val lessThanOrEqualTo: String? = null,
override val greaterThan: String? = null,
override val greaterThanOrEqualTo: String? = null,
val includes: String? = null,
val notIncludes: String? = null,
val includesInsensitive: String? = null,
val notIncludesInsensitive: String? = null,
val startsWith: String? = null,
val notStartsWith: String? = null,
val startsWithInsensitive: String? = null,
val notStartsWithInsensitive: String? = null,
val endsWith: String? = null,
val notEndsWith: String? = null,
val endsWithInsensitive: String? = null,
val notEndsWithInsensitive: String? = null,
val like: String? = null,
val notLike: String? = null,
val likeInsensitive: String? = null,
val notLikeInsensitive: String? = null,
val distinctFromInsensitive: String? = null,
val notDistinctFromInsensitive: String? = null,
val inInsensitive: List<String>? = null,
val notInInsensitive: List<String>? = null,
val lessThanInsensitive: String? = null,
val lessThanOrEqualToInsensitive: String? = null,
val greaterThanInsensitive: String? = null,
val greaterThanOrEqualToInsensitive: String? = null
) : ComparableScalarFilter<String>
fun <T : String?> andFilterWithCompareString(
column: Column<T>,
filter: StringFilter?
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
opAnd.andWhere(filter.includes) { column like "%$it%" }
opAnd.andWhere(filter.notIncludes) { column notLike "%$it%" }
opAnd.andWhere(filter.includesInsensitive) { ILikeEscapeOp.iLike(column, "%$it%") }
opAnd.andWhere(filter.notIncludesInsensitive) { ILikeEscapeOp.iNotLike(column, "%$it%") }
opAnd.andWhere(filter.startsWith) { column like "$it%" }
opAnd.andWhere(filter.notStartsWith) { column notLike "$it%" }
opAnd.andWhere(filter.startsWithInsensitive) { ILikeEscapeOp.iLike(column, "$it%") }
opAnd.andWhere(filter.notStartsWithInsensitive) { ILikeEscapeOp.iNotLike(column, "$it%") }
opAnd.andWhere(filter.endsWith) { column like "%$it" }
opAnd.andWhere(filter.notEndsWith) { column notLike "%$it" }
opAnd.andWhere(filter.endsWithInsensitive) { ILikeEscapeOp.iLike(column, "%$it") }
opAnd.andWhere(filter.notEndsWithInsensitive) { ILikeEscapeOp.iNotLike(column, "%$it") }
opAnd.andWhere(filter.like) { column like it }
opAnd.andWhere(filter.notLike) { column notLike it }
opAnd.andWhere(filter.likeInsensitive) { ILikeEscapeOp.iLike(column, it) }
opAnd.andWhere(filter.notLikeInsensitive) { ILikeEscapeOp.iNotLike(column, it) }
opAnd.andWhere(filter.distinctFromInsensitive) { DistinctFromOp.distinctFrom(column.upperCase(), it.uppercase() as T) }
opAnd.andWhere(filter.notDistinctFromInsensitive) { DistinctFromOp.notDistinctFrom(column.upperCase(), it.uppercase() as T) }
opAnd.andWhere(filter.inInsensitive) { column.upperCase() inList (it.map { it.uppercase() } as List<T>) }
opAnd.andWhere(filter.notInInsensitive) { column.upperCase() notInList (it.map { it.uppercase() } as List<T>) }
opAnd.andWhere(filter.lessThanInsensitive) { column.upperCase() less it.uppercase() }
opAnd.andWhere(filter.lessThanOrEqualToInsensitive) { column.upperCase() lessEq it.uppercase() }
opAnd.andWhere(filter.greaterThanInsensitive) { column.upperCase() greater it.uppercase() }
opAnd.andWhere(filter.greaterThanOrEqualToInsensitive) { column.upperCase() greaterEq it.uppercase() }
return opAnd.op
}
class OpAnd(var op: Op<Boolean>? = null) {
fun <T> andWhere(value: T?, andPart: SqlExpressionBuilder.(T) -> Op<Boolean>) {
value ?: return
val expr = Op.build { andPart(value) }
op = if (op == null) expr else (op!! and expr)
}
}
fun <T : Comparable<T>> andFilterWithCompare(
column: Column<T>,
filter: ComparableScalarFilter<T>?
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd(andFilter(column, filter))
opAnd.andWhere(filter.lessThan) { column less it }
opAnd.andWhere(filter.lessThanOrEqualTo) { column lessEq it }
opAnd.andWhere(filter.greaterThan) { column greater it }
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it }
return opAnd.op
}
fun <T : Comparable<T>> andFilterWithCompareEntity(
column: Column<EntityID<T>>,
filter: ComparableScalarFilter<T>?
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd(andFilterEntity(column, filter))
opAnd.andWhere(filter.lessThan) { column less it }
opAnd.andWhere(filter.lessThanOrEqualTo) { column lessEq it }
opAnd.andWhere(filter.greaterThan) { column greater it }
opAnd.andWhere(filter.greaterThanOrEqualTo) { column greaterEq it }
return opAnd.op
}
fun <T : Comparable<T>> andFilter(
column: Column<T>,
filter: ScalarFilter<T>?
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
opAnd.andWhere(filter.isNull) { if (it) column.isNull() else column.isNotNull() }
opAnd.andWhere(filter.equalTo) { column eq it }
opAnd.andWhere(filter.notEqualTo) { column neq it }
opAnd.andWhere(filter.distinctFrom) { DistinctFromOp.distinctFrom(column, it) }
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) }
if (!filter.`in`.isNullOrEmpty()) {
opAnd.andWhere(filter.`in`) { column inList it }
}
if (!filter.notIn.isNullOrEmpty()) {
opAnd.andWhere(filter.notIn) { column notInList it }
}
return opAnd.op
}
fun <T : Comparable<T>> andFilterEntity(
column: Column<EntityID<T>>,
filter: ScalarFilter<T>?
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
opAnd.andWhere(filter.isNull) { if (filter.isNull!!) column.isNull() else column.isNotNull() }
opAnd.andWhere(filter.equalTo) { column eq filter.equalTo!! }
opAnd.andWhere(filter.notEqualTo) { column neq filter.notEqualTo!! }
opAnd.andWhere(filter.distinctFrom) { DistinctFromOp.distinctFrom(column, it) }
opAnd.andWhere(filter.notDistinctFrom) { DistinctFromOp.notDistinctFrom(column, it) }
if (!filter.`in`.isNullOrEmpty()) {
opAnd.andWhere(filter.`in`) { column inList filter.`in`!! }
}
if (!filter.notIn.isNullOrEmpty()) {
opAnd.andWhere(filter.notIn) { column notInList filter.notIn!! }
}
return opAnd.op
}
fun <T : Filter<T>> Filter<T>.getOp(): Op<Boolean>? {
var op: Op<Boolean>? = null
fun newOp(
otherOp: Op<Boolean>?,
operator: (Op<Boolean>, Op<Boolean>) -> Op<Boolean>
) {
when {
op == null && otherOp == null -> Unit
op == null && otherOp != null -> op = otherOp
op != null && otherOp == null -> Unit
op != null && otherOp != null -> op = operator(op!!, otherOp)
}
}
fun andOp(andOp: Op<Boolean>?) {
newOp(andOp, Op<Boolean>::and)
}
fun orOp(orOp: Op<Boolean>?) {
newOp(orOp, Op<Boolean>::or)
}
getOpList().forEach {
andOp(it)
}
and?.forEach {
andOp(it.getOp())
}
or?.forEach {
orOp(it.getOp())
}
if (not != null) {
andOp(not!!.getOp()?.let(::not))
}
return op
}
@@ -1,42 +0,0 @@
package suwayomi.tachidesk.graphql.queries.util
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.andWhere
interface GreaterOrLessThan<T : Comparable<T>> {
val value: T
val type: GreaterOrLessThanType
}
data class GreaterOrLessThanLong(
override val value: Long,
override val type: GreaterOrLessThanType
) : GreaterOrLessThan<Long>
enum class GreaterOrLessThanType {
GREATER_THAN,
GREATER_THAN_OR_EQ,
LESS_THAN,
LESS_THAN_OR_EQ
}
fun <T : Comparable<T>> Query.andWhereGreaterOrLessThen(
column: Column<T>,
greaterOrLessThan: GreaterOrLessThan<T>
) {
when (greaterOrLessThan.type) {
GreaterOrLessThanType.GREATER_THAN -> andWhere {
column greater greaterOrLessThan.value // toValue()
}
GreaterOrLessThanType.GREATER_THAN_OR_EQ -> andWhere {
column greaterEq greaterOrLessThan.value // toValue()
}
GreaterOrLessThanType.LESS_THAN -> andWhere {
column less greaterOrLessThan.value // toValue()
}
GreaterOrLessThanType.LESS_THAN_OR_EQ -> andWhere {
column lessEq greaterOrLessThan.value // toValue()
}
}
}