From a6dddf311c4c6f89794305793983b02905279a74 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Mon, 3 Apr 2023 22:04:46 -0400 Subject: [PATCH] Basically finish MangaQuery, only paging left --- .../tachidesk/graphql/queries/MangaQuery.kt | 173 ++++++--- .../graphql/queries/filter/Filter.kt | 329 ++++++++++++++++++ .../graphql/queries/util/GreaterOrLessThan.kt | 42 --- 3 files changed, 455 insertions(+), 89 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt delete mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index b9158a74..de53dd70 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -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? = null, - val categoryIds: List? = null, - val sourceIds: List? = 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? = 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? { + val opAnd = OpAnd() + fun eq(value: T?, column: Column) = opAnd.andWhere(value) { column eq it } + fun > eq(value: T?, column: Column>) = 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? = 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? = null, + override val or: List? = null, + override val not: MangaFilter? = null + ) : Filter { + override fun getOpList(): List> { + 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() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt new file mode 100644 index 00000000..9f75f98e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/filter/Filter.kt @@ -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 iLike(expression: Expression, pattern: String): LikeEscapeOp = iLike(expression, LikePattern(pattern)) + fun iNotLike(expression: Expression, pattern: String): LikeEscapeOp = iNotLike(expression, LikePattern(pattern)) + fun iLike(expression: Expression, pattern: LikePattern): LikeEscapeOp = LikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar) + fun iNotLike(expression: Expression, 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 distinctFrom(expression: ExpressionWithColumnType, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + false + ) + fun notDistinctFrom(expression: ExpressionWithColumnType, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + true + ) + fun > distinctFrom(expression: ExpressionWithColumnType>, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + false + ) + fun > notDistinctFrom(expression: ExpressionWithColumnType>, t: T): DistinctFromOp = DistinctFromOp( + expression, + with(SqlExpressionBuilder) { + expression.wrap(t) + }, + true + ) + } +} + +interface Filter> { + val and: List? + val or: List? + val not: T? + + fun getOpList(): List> +} + +interface ScalarFilter { + val isNull: Boolean? + val equalTo: T? + val notEqualTo: T? + val distinctFrom: T? + val notDistinctFrom: T? + val `in`: List? + val notIn: List? +} + +interface ComparableScalarFilter> : ScalarFilter { + 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? = null, + override val notIn: List? = null, + override val lessThan: Long? = null, + override val lessThanOrEqualTo: Long? = null, + override val greaterThan: Long? = null, + override val greaterThanOrEqualTo: Long? = null +) : ComparableScalarFilter + +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? = null, + override val notIn: List? = null, + override val lessThan: Boolean? = null, + override val lessThanOrEqualTo: Boolean? = null, + override val greaterThan: Boolean? = null, + override val greaterThanOrEqualTo: Boolean? = null +) : ComparableScalarFilter + +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? = null, + override val notIn: List? = null, + override val lessThan: Int? = null, + override val lessThanOrEqualTo: Int? = null, + override val greaterThan: Int? = null, + override val greaterThanOrEqualTo: Int? = null +) : ComparableScalarFilter + +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? = null, + override val notIn: List? = 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? = null, + val notInInsensitive: List? = null, + val lessThanInsensitive: String? = null, + val lessThanOrEqualToInsensitive: String? = null, + val greaterThanInsensitive: String? = null, + val greaterThanOrEqualToInsensitive: String? = null +) : ComparableScalarFilter + +fun andFilterWithCompareString( + column: Column, + filter: StringFilter? +): Op? { + 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) } + opAnd.andWhere(filter.notInInsensitive) { column.upperCase() notInList (it.map { it.uppercase() } as List) } + + 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? = null) { + fun andWhere(value: T?, andPart: SqlExpressionBuilder.(T) -> Op) { + value ?: return + val expr = Op.build { andPart(value) } + op = if (op == null) expr else (op!! and expr) + } +} + +fun > andFilterWithCompare( + column: Column, + filter: ComparableScalarFilter? +): Op? { + 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 > andFilterWithCompareEntity( + column: Column>, + filter: ComparableScalarFilter? +): Op? { + 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 > andFilter( + column: Column, + filter: ScalarFilter? +): Op? { + 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 > andFilterEntity( + column: Column>, + filter: ScalarFilter? +): Op? { + 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 > Filter.getOp(): Op? { + var op: Op? = null + fun newOp( + otherOp: Op?, + operator: (Op, Op) -> Op + ) { + 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?) { + newOp(andOp, Op::and) + } + fun orOp(orOp: Op?) { + newOp(orOp, Op::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 +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt deleted file mode 100644 index 7be592fa..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/util/GreaterOrLessThan.kt +++ /dev/null @@ -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> { - val value: T - val type: GreaterOrLessThanType -} - -data class GreaterOrLessThanLong( - override val value: Long, - override val type: GreaterOrLessThanType -) : GreaterOrLessThan - -enum class GreaterOrLessThanType { - GREATER_THAN, - GREATER_THAN_OR_EQ, - LESS_THAN, - LESS_THAN_OR_EQ -} - -fun > Query.andWhereGreaterOrLessThen( - column: Column, - greaterOrLessThan: GreaterOrLessThan -) { - 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() - } - } -}